Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3296fd06e2 | ||
|
|
9102630227 | ||
|
|
e72bf3e86f | ||
|
|
4a97f065af | ||
|
|
0bc279c387 | ||
|
|
6ce0707aae | ||
|
|
7c172d99a4 | ||
|
|
86e975ba58 | ||
|
|
1a3a7b7b10 | ||
|
|
4d923768fa | ||
|
|
7a1e66396d | ||
|
|
88a4f90569 | ||
|
|
03ed5c8f00 | ||
|
|
f22bf6a8f7 | ||
|
|
3c6f121493 | ||
|
|
68b26c4579 | ||
|
|
3183842375 | ||
|
|
2dcabf97dd | ||
|
|
efa513a569 | ||
|
|
dd67710848 | ||
|
|
2523040b08 |
+23
-107
@@ -17,7 +17,7 @@ on:
|
||||
- 'fvdb/**'
|
||||
- 'pendingchanges/**'
|
||||
- '**.md'
|
||||
pull_request_target:
|
||||
pull_request:
|
||||
# Add 'ready_for_review' to run CI when a draft PR is marked ready.
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
branches:
|
||||
@@ -49,30 +49,26 @@ jobs:
|
||||
# push OR non-draft PR OR matching manual run
|
||||
if: |
|
||||
(github.event_name == 'push' ||
|
||||
(github.event_name == 'pull_request_target' && github.event.pull_request.draft == false) ||
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
|
||||
(github.event_name == 'workflow_dispatch' &&
|
||||
(github.event.inputs.type == 'all' || github.event.inputs.type == 'linux')))
|
||||
runs-on: ${{ (github.repository_owner == 'AcademySoftwareFoundation' && 'ubuntu-22.04-8c-32g-300h') || 'ubuntu-latest' }}
|
||||
name: >
|
||||
linux-nanovdb:cxx:${{ matrix.config.cxx }}-${{ matrix.config.build }}
|
||||
# Build in the same image used by the AWS CodeBuild GPU runner so the
|
||||
# uploaded build tree is binary/runtime compatible when executed there.
|
||||
container:
|
||||
image: aswf/ci-openvdb:2024-clang17.2
|
||||
image: aswf/ci-openvdb:${{ matrix.config.image }}
|
||||
env:
|
||||
CXX: ${{ matrix.config.cxx }}
|
||||
strategy:
|
||||
matrix:
|
||||
config:
|
||||
- { cxx: g++, label: gcc, build: 'Release' }
|
||||
- { cxx: g++, label: gcc, build: 'Debug' }
|
||||
- { cxx: clang++, label: clang, build: 'Release' }
|
||||
- { cxx: clang++, label: clang, build: 'Debug' }
|
||||
- { cxx: g++, image: '2024-clang17.2', build: 'Release' }
|
||||
- { cxx: g++, image: '2024-clang17.2', build: 'Debug' }
|
||||
- { cxx: clang++, image: '2024-clang17.2', build: 'Release' }
|
||||
- { cxx: clang++, image: '2024-clang17.2', build: 'Debug' }
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/checkout@v3
|
||||
- name: setup_cuda_12
|
||||
run: |
|
||||
echo "/usr/local/cuda-12/bin" >> $GITHUB_PATH
|
||||
@@ -82,103 +78,26 @@ jobs:
|
||||
- name: build
|
||||
# NOTE: CMAKE_POSITION_INDEPENDENT_CODE set to fix default behaviour change in clang 14
|
||||
# https://github.com/AcademySoftwareFoundation/aswf-docker/issues/228
|
||||
# NOTE: CUDA architecture 86 targets the AWS CodeBuild A10G GPUs used by
|
||||
# the linux-nanovdb-gpu test stage below.
|
||||
run: >
|
||||
./ci/build.sh -v
|
||||
--build-type=${{ matrix.config.build }}
|
||||
--components=core,nano,nanotest,nanoexam,nanobench,nanotool,nanopython,nanopytest
|
||||
--cargs=\'
|
||||
-DUSE_EXPLICIT_INSTANTIATION=OFF
|
||||
-DOPENVDB_CORE_STATIC=OFF
|
||||
-DNANOVDB_USE_CUDA=ON
|
||||
-DCMAKE_CUDA_ARCHITECTURES="86"
|
||||
-DCMAKE_CUDA_ARCHITECTURES="80"
|
||||
-DNANOVDB_USE_OPENVDB=ON
|
||||
-DCMAKE_INSTALL_PREFIX=`pwd`
|
||||
-DUSE_BLOSC=ON
|
||||
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
|
||||
\'
|
||||
# Run the non-GPU tests here. GPU-only (cuda/mgpu) tests are executed in
|
||||
# the linux-nanovdb-gpu job. This step also generates the shared test
|
||||
# data files (e.g. data/*.nvdb) that the GPU tests consume, so it must
|
||||
# run before the build tree is uploaded.
|
||||
- name: test
|
||||
run: cd build && ctest -V -E ".*cuda.*|.*mgpu.*"
|
||||
- name: package_gpu_build
|
||||
# Record the absolute workspace path so the GPU runner can restore the
|
||||
# build tree to the same location, keeping RPATHs and CTest-generated
|
||||
# absolute paths valid.
|
||||
run: |
|
||||
META="$GITHUB_WORKSPACE/nanovdb-gpu-meta.env"
|
||||
echo "BUILD_WORKSPACE=$GITHUB_WORKSPACE" > "$META"
|
||||
echo "GITHUB_SHA=$GITHUB_SHA" >> "$META"
|
||||
echo "CONFIG=${{ matrix.config.label }}-${{ matrix.config.build }}" >> "$META"
|
||||
cat "$META"
|
||||
- name: upload_gpu_build
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: nanovdb-gpu-build-${{ matrix.config.label }}-${{ matrix.config.build }}
|
||||
path: |
|
||||
build
|
||||
nanovdb/nanovdb/python/test/TestNanoVDB.py
|
||||
nanovdb-gpu-meta.env
|
||||
retention-days: 1
|
||||
if-no-files-found: error
|
||||
|
||||
linux-nanovdb-gpu:
|
||||
# Execute the CUDA / multi-GPU tests on AWS CodeBuild (4x A10G) using the
|
||||
# prebuilt artifacts from linux-nanovdb. Repository secrets are not shared
|
||||
# with fork PRs, so restrict this job to same-repo events.
|
||||
if: |
|
||||
github.repository == 'AcademySoftwareFoundation/openvdb' &&
|
||||
(github.event_name == 'push' ||
|
||||
(github.event_name == 'pull_request_target' &&
|
||||
github.event.pull_request.draft == false &&
|
||||
github.event.pull_request.head.repo.full_name == github.repository) ||
|
||||
(github.event_name == 'workflow_dispatch' &&
|
||||
(github.event.inputs.type == 'all' || github.event.inputs.type == 'linux')))
|
||||
needs: linux-nanovdb
|
||||
runs-on: ubuntu-latest
|
||||
name: >
|
||||
linux-nanovdb-gpu:${{ matrix.config.label }}-${{ matrix.config.build }}
|
||||
# GITHUB_TOKEN needs actions:read to fetch the workflow-run artifacts.
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
strategy:
|
||||
matrix:
|
||||
config:
|
||||
- { label: gcc, build: 'Release' }
|
||||
- { label: gcc, build: 'Debug' }
|
||||
- { label: clang, build: 'Release' }
|
||||
- { label: clang, build: 'Debug' }
|
||||
fail-fast: false
|
||||
env:
|
||||
# Consumed by buildspec.yml on the CodeBuild side.
|
||||
ARTIFACT_NAME: nanovdb-gpu-build-${{ matrix.config.label }}-${{ matrix.config.build }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.CODEBUILD_ID }}
|
||||
aws-secret-access-key: ${{ secrets.CODEBUILD_SECRET }}
|
||||
aws-region: us-west-2
|
||||
- name: Run CodeBuild GPU tests
|
||||
uses: aws-actions/aws-codebuild-run-build@v1
|
||||
with:
|
||||
project-name: OpenVDB
|
||||
buildspec-override: ci/buildspec_nanovdb.yml
|
||||
image-override: aswf/ci-openvdb:2024-clang17.2
|
||||
# GITHUB_* variables (GITHUB_RUN_ID, GITHUB_REPOSITORY, ...) are
|
||||
# forwarded automatically. The job-scoped Actions runtime values let
|
||||
# CodeBuild upload report.tgz directly to this workflow run.
|
||||
env-vars-for-codebuild: ARTIFACT_NAME,GH_TOKEN,ACTIONS_RUNTIME_TOKEN,ACTIONS_RESULTS_URL
|
||||
run: cd build && sudo ctest -V -E ".*cuda.*|.*mgpu.*"
|
||||
|
||||
windows-nanovdb:
|
||||
if: |
|
||||
(github.event_name == 'push' ||
|
||||
(github.event_name == 'pull_request_target' && github.event.pull_request.draft == false) ||
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
|
||||
(github.event_name == 'workflow_dispatch' &&
|
||||
(github.event.inputs.type == 'all' || github.event.inputs.type == 'win')))
|
||||
runs-on: ${{ (github.repository_owner == 'AcademySoftwareFoundation' && 'windows-2022-8c-32g-300h') || 'windows-latest' }}
|
||||
@@ -189,9 +108,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/checkout@v3
|
||||
- name: path
|
||||
run: |
|
||||
# note: system path must be modified in a previous step to it's use
|
||||
@@ -203,6 +120,9 @@ jobs:
|
||||
- name: install
|
||||
shell: powershell
|
||||
run: .\ci\install_windows.ps1
|
||||
- name: install_numpy
|
||||
shell: powershell
|
||||
run: .\ci\install_windows_numpy.ps1
|
||||
- name: build
|
||||
# nvcc doesn't set _WIN32 when run in bash so we need to set it manually
|
||||
shell: bash
|
||||
@@ -228,7 +148,7 @@ jobs:
|
||||
macos-nanovdb:
|
||||
if: |
|
||||
(github.event_name == 'push' ||
|
||||
(github.event_name == 'pull_request_target' && github.event.pull_request.draft == false) ||
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
|
||||
(github.event_name == 'workflow_dispatch' &&
|
||||
(github.event.inputs.type == 'all' || github.event.inputs.type == 'mac')))
|
||||
runs-on: ${{ matrix.config.runner }}
|
||||
@@ -241,9 +161,7 @@ jobs:
|
||||
- { runner: 'macos-14', cxx: 'clang++', build: 'Debug' }
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/checkout@v3
|
||||
- name: install_deps
|
||||
run: |
|
||||
./ci/install_macos.sh
|
||||
@@ -260,23 +178,21 @@ jobs:
|
||||
nanovdb-lite:
|
||||
if: |
|
||||
(github.event_name == 'push' ||
|
||||
(github.event_name == 'pull_request_target' && github.event.pull_request.draft == false) ||
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
|
||||
(github.event_name == 'workflow_dispatch' &&
|
||||
(github.event.inputs.type == 'all' || github.event.inputs.type == 'linux')))
|
||||
runs-on: ${{ (github.repository_owner == 'AcademySoftwareFoundation' && 'ubuntu-22.04-8c-32g-300h') || 'ubuntu-latest' }}
|
||||
container:
|
||||
image: aswf/ci-openvdb:2024-clang17.2
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/checkout@v3
|
||||
- name: install_gtest
|
||||
run: ./ci/install_gtest.sh latest
|
||||
- name: build_and_test
|
||||
run: |
|
||||
cd nanovdb/nanovdb
|
||||
mkdir .build
|
||||
sudo mkdir .build
|
||||
cd .build
|
||||
cmake -DUSE_EXPLICIT_INSTANTIATION=OFF -DNANOVDB_BUILD_UNITTESTS=ON -DNANOVDB_USE_OPENVDB=OFF -DNANOVDB_USE_CUDA=OFF -DCMAKE_INSTALL_PREFIX="$(pwd)/install" ../
|
||||
make -j8 install
|
||||
ctest -V
|
||||
sudo cmake -DUSE_EXPLICIT_INSTANTIATION=OFF -DNANOVDB_BUILD_UNITTESTS=ON -DNANOVDB_USE_OPENVDB=OFF -DNANOVDB_USE_CUDA=OFF ../
|
||||
sudo make -j8 install
|
||||
sudo ctest -V
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
version: 0.2
|
||||
|
||||
# Runs the NanoVDB CUDA / multi-GPU unit tests on AWS CodeBuild (4x A10G).
|
||||
# This spec does NOT build the project. Instead it downloads the prebuilt
|
||||
# build tree produced by the matching GitHub Actions run (see the
|
||||
# linux-nanovdb / linux-nanovdb-gpu jobs in .github/workflows/nanovdb.yml),
|
||||
# restores it at its original absolute path, and executes the GPU and Python tests.
|
||||
#
|
||||
# Required environment variables (provided by aws-codebuild-run-build):
|
||||
# ARTIFACT_NAME - name of the GitHub Actions artifact to fetch
|
||||
# GH_TOKEN - ephemeral GITHUB_TOKEN with actions:read
|
||||
# GITHUB_REPOSITORY - owner/repo (auto-forwarded GITHUB_* variable)
|
||||
# GITHUB_RUN_ID - workflow run that produced the artifact (auto-forwarded)
|
||||
# ACTIONS_RUNTIME_TOKEN - job-scoped token used to upload the report
|
||||
# ACTIONS_RESULTS_URL - GitHub Actions artifact service endpoint
|
||||
|
||||
env:
|
||||
variables:
|
||||
CUDA_HOME: /usr/local/cuda-12
|
||||
DIAGNOSTIC_GIST_ID: c082e01bf80cd88d045e1e61bd78d43c
|
||||
DIAGNOSTIC_GIST_REVISION: dcd46d4e3a65c5d36e1cde96ade0c102464d8a45
|
||||
|
||||
phases:
|
||||
install:
|
||||
commands:
|
||||
# Rocky Linux 8 image: tools required to fetch and unpack artifacts and
|
||||
# clone the multi-GPU diagnostic gist.
|
||||
- dnf install -y jq unzip tree 'dnf-command(config-manager)'
|
||||
- dnf config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo
|
||||
- dnf install -y gh
|
||||
- dnf module reset -y nodejs
|
||||
- dnf module enable -y nodejs:20
|
||||
- dnf install -y nodejs
|
||||
- npm install --prefix /tmp/actions-artifact-uploader --omit=dev --no-audit --no-fund @actions/artifact@6.2.1
|
||||
|
||||
pre_build:
|
||||
commands:
|
||||
# Confirm the GPUs are visible (expect 4x A10G).
|
||||
- nvidia-smi
|
||||
- nvidia-smi topo -m
|
||||
- |
|
||||
echo "=== IOMMU diagnostics ==="
|
||||
|
||||
kernel_cmdline=$(cat /proc/cmdline)
|
||||
echo "Kernel command line:"
|
||||
echo "${kernel_cmdline}"
|
||||
|
||||
iommu_args=$(echo "${kernel_cmdline}" |
|
||||
grep -Eo '(^|[[:space:]])(iommu|amd_iommu|intel_iommu)=[^[:space:]]+' |
|
||||
xargs || true)
|
||||
if [ -n "${iommu_args}" ]; then
|
||||
echo "Explicit IOMMU kernel arguments: ${iommu_args}"
|
||||
else
|
||||
echo "No explicit IOMMU kernel arguments found"
|
||||
fi
|
||||
|
||||
if dmesg_output=$(dmesg 2>&1); then
|
||||
echo "IOMMU kernel messages:"
|
||||
echo "${dmesg_output}" |
|
||||
grep -Ei 'DMAR|IOMMU|AMD-Vi|amd_iommu|intel_iommu' ||
|
||||
echo "No IOMMU kernel messages found"
|
||||
|
||||
echo "IOMMU passthrough messages:"
|
||||
echo "${dmesg_output}" |
|
||||
grep -Ei 'iommu.*(pt|passthrough)|default domain type.*passthrough' ||
|
||||
echo "No IOMMU passthrough messages found"
|
||||
else
|
||||
echo "dmesg unavailable in this container: ${dmesg_output}"
|
||||
fi
|
||||
|
||||
if compgen -G '/sys/kernel/iommu_groups/*/devices/*' >/dev/null; then
|
||||
echo "Populated IOMMU groups are visible"
|
||||
group_count=0
|
||||
typed_group_count=0
|
||||
for group in /sys/kernel/iommu_groups/*; do
|
||||
[ -d "${group}" ] || continue
|
||||
group_count=$((group_count + 1))
|
||||
if [ -r "${group}/type" ]; then
|
||||
typed_group_count=$((typed_group_count + 1))
|
||||
echo "IOMMU group ${group##*/} type: $(cat "${group}/type")"
|
||||
fi
|
||||
done
|
||||
echo "Visible IOMMU group count: ${group_count}"
|
||||
if [ "${typed_group_count}" -eq 0 ]; then
|
||||
echo "Per-group IOMMU domain types are not exposed by this kernel"
|
||||
fi
|
||||
else
|
||||
echo "No populated IOMMU groups are visible"
|
||||
fi
|
||||
|
||||
if compgen -G '/sys/class/iommu/*' >/dev/null; then
|
||||
echo "IOMMU devices visible in /sys/class/iommu:"
|
||||
for iommu in /sys/class/iommu/*; do
|
||||
echo " ${iommu##*/}"
|
||||
done
|
||||
else
|
||||
echo "No IOMMU devices are visible in /sys/class/iommu"
|
||||
fi
|
||||
# Locate, download and restore the prebuilt build tree.
|
||||
- 'echo "Fetching artifact ${ARTIFACT_NAME} from run ${GITHUB_RUN_ID}"'
|
||||
- |
|
||||
set -euo pipefail
|
||||
api="https://api.github.com/repos/${GITHUB_REPOSITORY}"
|
||||
artifacts_json=$(curl -fsSL \
|
||||
-H "Authorization: Bearer ${GH_TOKEN}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"${api}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100")
|
||||
count=$(echo "${artifacts_json}" | jq --arg n "${ARTIFACT_NAME}" \
|
||||
'[.artifacts[] | select(.name==$n)] | length')
|
||||
if [ "${count}" != "1" ]; then
|
||||
echo "ERROR: expected exactly 1 artifact named '${ARTIFACT_NAME}', found ${count}"
|
||||
exit 1
|
||||
fi
|
||||
artifact_id=$(echo "${artifacts_json}" | jq -r --arg n "${ARTIFACT_NAME}" \
|
||||
'.artifacts[] | select(.name==$n) | .id')
|
||||
echo "Resolved artifact id ${artifact_id}"
|
||||
# The API returns a short-lived (60s) redirect to blob storage. Resolve
|
||||
# the redirect first, then download without the Authorization header so
|
||||
# it is not forwarded to the storage host.
|
||||
download_url=$(curl -sS -o /dev/null -w '%{redirect_url}' \
|
||||
-H "Authorization: Bearer ${GH_TOKEN}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"${api}/actions/artifacts/${artifact_id}/zip")
|
||||
curl -fsSL -o /tmp/nanovdb-gpu-build.zip "${download_url}"
|
||||
- rm -rf /tmp/nanovdb-gpu-artifact
|
||||
- mkdir -p /tmp/nanovdb-gpu-artifact
|
||||
- unzip -q /tmp/nanovdb-gpu-build.zip -d /tmp/nanovdb-gpu-artifact
|
||||
- |
|
||||
set -euo pipefail
|
||||
set -a
|
||||
. /tmp/nanovdb-gpu-artifact/nanovdb-gpu-meta.env
|
||||
set +a
|
||||
echo "Restoring build tree and Python tests to ${BUILD_WORKSPACE}"
|
||||
mkdir -p "${BUILD_WORKSPACE}"
|
||||
rm -rf "${BUILD_WORKSPACE}/build"
|
||||
mv /tmp/nanovdb-gpu-artifact/build "${BUILD_WORKSPACE}/build"
|
||||
python_test_dir="${BUILD_WORKSPACE}/nanovdb/nanovdb/python/test"
|
||||
mkdir -p "${python_test_dir}"
|
||||
mv /tmp/nanovdb-gpu-artifact/nanovdb/nanovdb/python/test/TestNanoVDB.py \
|
||||
"${python_test_dir}/TestNanoVDB.py"
|
||||
|
||||
build:
|
||||
commands:
|
||||
- |
|
||||
set -euo pipefail
|
||||
set -a
|
||||
. /tmp/nanovdb-gpu-artifact/nanovdb-gpu-meta.env
|
||||
set +a
|
||||
export PATH="${CUDA_HOME}/bin:${PATH}"
|
||||
export LD_LIBRARY_PATH="${CUDA_HOME}/lib64:${BUILD_WORKSPACE}/build/openvdb/openvdb:${BUILD_WORKSPACE}/build/nanovdb/nanovdb:${LD_LIBRARY_PATH:-}"
|
||||
cd "${BUILD_WORKSPACE}/build"
|
||||
ctest -V -R ".*cuda.*|.*mgpu.*|pytest_nanovdb" --output-on-failure
|
||||
|
||||
post_build:
|
||||
commands:
|
||||
- |
|
||||
set -euo pipefail
|
||||
diagnostic_dir="${CODEBUILD_SRC_DIR}/${DIAGNOSTIC_GIST_ID}"
|
||||
rm -rf "${diagnostic_dir}" "${CODEBUILD_SRC_DIR}/report.tgz"
|
||||
cd "${CODEBUILD_SRC_DIR}"
|
||||
gh gist clone "${DIAGNOSTIC_GIST_ID}"
|
||||
# Pin the executable diagnostic code rather than trusting the mutable
|
||||
# head of the public gist on every CI run.
|
||||
git -C "${diagnostic_dir}" checkout --detach "${DIAGNOSTIC_GIST_REVISION}"
|
||||
|
||||
diagnostic_status=0
|
||||
(
|
||||
cd "${diagnostic_dir}"
|
||||
bash ./run_on_runner.sh
|
||||
) || diagnostic_status=$?
|
||||
|
||||
if [ -d "${diagnostic_dir}/report" ]; then
|
||||
tar czf "${CODEBUILD_SRC_DIR}/report.tgz" \
|
||||
-C "${diagnostic_dir}" report/
|
||||
else
|
||||
echo "ERROR: the diagnostic did not produce report/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Upload directly into the current GitHub Actions job. These values are
|
||||
# short-lived and scoped to this job's artifact service.
|
||||
: "${ACTIONS_RUNTIME_TOKEN:?ACTIONS_RUNTIME_TOKEN was not forwarded}"
|
||||
: "${ACTIONS_RESULTS_URL:?ACTIONS_RESULTS_URL was not forwarded}"
|
||||
cat > /tmp/actions-artifact-uploader/upload.mjs <<'EOF'
|
||||
import {DefaultArtifactClient} from '@actions/artifact'
|
||||
|
||||
const reportPath = `${process.env.CODEBUILD_SRC_DIR}/report.tgz`
|
||||
const artifactName = process.env.ARTIFACT_NAME.replace(
|
||||
/^nanovdb-gpu-build-/,
|
||||
'nanovdb-gpu-report-'
|
||||
)
|
||||
const client = new DefaultArtifactClient()
|
||||
const result = await client.uploadArtifact(
|
||||
artifactName,
|
||||
[reportPath],
|
||||
process.env.CODEBUILD_SRC_DIR,
|
||||
{
|
||||
retentionDays: 7,
|
||||
compressionLevel: 0
|
||||
}
|
||||
)
|
||||
console.log(
|
||||
`Uploaded ${artifactName}: id=${result.id}, size=${result.size}, digest=${result.digest}`
|
||||
)
|
||||
EOF
|
||||
node /tmp/actions-artifact-uploader/upload.mjs
|
||||
exit "${diagnostic_status}"
|
||||
@@ -7,6 +7,7 @@ A lightweight GPU friendly version of VDB initially targeting rendering applicat
|
||||
* [Frequently asked questions](../../doc/nanovdb/FAQ.md)
|
||||
* [Source tree](../../doc/nanovdb/SourceTree.md)
|
||||
* [Examples](../../doc/nanovdb/HelloWorld.md)
|
||||
* [Python examples](python/examples/)
|
||||
|
||||
### Copyright Contributors to the OpenVDB Project
|
||||
### SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
@@ -1,368 +0,0 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
/// @file nanovdb/cuda/Buffer.h
|
||||
///
|
||||
/// @brief Typed containers for CUDA memory: the owning, resource-aware,
|
||||
/// stream-ordered cuda::Buffer and the non-owning cuda::BufferView.
|
||||
|
||||
#ifndef NANOVDB_CUDA_BUFFER_H_HAS_BEEN_INCLUDED
|
||||
#define NANOVDB_CUDA_BUFFER_H_HAS_BEEN_INCLUDED
|
||||
|
||||
#include <cuda_runtime_api.h>
|
||||
#include <nanovdb/cuda/DeviceResource.h>
|
||||
#include <nanovdb/util/cuda/Util.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
namespace nanovdb {
|
||||
|
||||
namespace cuda {
|
||||
|
||||
/// @brief Tag type selecting the Buffer constructors that skip element
|
||||
/// initialization, leaving the contents indeterminate.
|
||||
struct NoInit {};
|
||||
inline constexpr NoInit noInit{};
|
||||
|
||||
namespace detail {
|
||||
|
||||
/// @brief Conditional stream storage for Buffer. The async specialization
|
||||
/// retains the stream of the most recent allocation; the synchronous
|
||||
/// specialization is an empty base, so a Buffer over a synchronous
|
||||
/// resource carries no stream state and exposes no stream API.
|
||||
template<bool IsAsync>
|
||||
struct StreamHolder {};
|
||||
|
||||
template<>
|
||||
struct StreamHolder<true> { cudaStream_t mStream = 0; };
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/// @brief Owning, typed container of @c T elements allocated from a memory
|
||||
/// resource @c R held by value.
|
||||
/// @tparam T element type; sizes are expressed in elements, not bytes
|
||||
/// @tparam R memory resource, either stream-ordered (AsyncResource concept,
|
||||
/// see is_async_resource) or synchronous (Resource concept, see
|
||||
/// is_resource). When @c R provides both interfaces the stream-ordered
|
||||
/// one is used.
|
||||
/// @details With a stream-ordered resource the Buffer retains the stream of
|
||||
/// the most recent allocation (or the one supplied via setStream)
|
||||
/// and orders its deallocation on that stream. Buffer is move-only.
|
||||
template<typename T, typename R = DeviceResource>
|
||||
class Buffer : private detail::StreamHolder<is_async_resource<R>::value>
|
||||
{
|
||||
static_assert(is_async_resource<R>::value || is_resource<R>::value,
|
||||
"Buffer requires R to model the AsyncResource or the Resource concept");
|
||||
static_assert(std::is_trivially_copyable<T>::value,
|
||||
"Buffer requires a trivially copyable T: elements are copied bytewise and never constructed or destroyed");
|
||||
|
||||
static constexpr bool IsAsync = is_async_resource<R>::value;
|
||||
|
||||
R mResource;
|
||||
T* mData = nullptr;
|
||||
size_t mSize = 0; // element count
|
||||
|
||||
public:
|
||||
/// @brief Default c-tor of an empty buffer; performs no allocation.
|
||||
Buffer() = default;
|
||||
|
||||
/// @brief C-tor allocating @c count uninitialized elements, stream-ordered
|
||||
/// on @c stream. Parameter order follows cuda::buffer:
|
||||
/// (stream, resource, count, no_init).
|
||||
/// @param stream cuda stream the allocation is ordered on
|
||||
/// @param resource resource instance the buffer takes ownership of
|
||||
/// @param count number of elements
|
||||
template<typename S = R, std::enable_if_t<is_async_resource<S>::value, int> = 0>
|
||||
explicit Buffer(cudaStream_t stream, R resource, size_t count, NoInit)
|
||||
: detail::StreamHolder<true>{stream}
|
||||
, mResource(std::move(resource))
|
||||
{
|
||||
this->allocate(count, stream);
|
||||
}
|
||||
|
||||
/// @brief Convenience c-tor using a default-constructed resource.
|
||||
/// @param stream cuda stream the allocation is ordered on
|
||||
/// @param count number of elements
|
||||
/// @note There is deliberately no count c-tor without NoInit: implicit
|
||||
/// initialization of freshly allocated memory costs a hidden fill
|
||||
/// pass that the dominant allocate-then-overwrite pattern wastes,
|
||||
/// so initialization is always explicit (matching cuda::buffer,
|
||||
/// whose count c-tor likewise requires cuda::no_init).
|
||||
template<typename S = R, std::enable_if_t<is_async_resource<S>::value, int> = 0>
|
||||
Buffer(cudaStream_t stream, size_t count, NoInit) : Buffer(stream, R(), count, noInit) {}
|
||||
|
||||
/// @brief C-tor allocating @c count uninitialized elements from a
|
||||
/// synchronous resource: the stream-less analog of
|
||||
/// (stream, resource, count, no_init).
|
||||
/// @param resource resource instance the buffer takes ownership of
|
||||
/// @param count number of elements
|
||||
template<typename S = R, std::enable_if_t<!is_async_resource<S>::value && is_resource<S>::value, int> = 0>
|
||||
explicit Buffer(R resource, size_t count, NoInit)
|
||||
: mResource(std::move(resource))
|
||||
{
|
||||
this->allocate(count, cudaStream_t{0});
|
||||
}
|
||||
|
||||
/// @brief Convenience c-tor using a default-constructed resource.
|
||||
/// @param count number of elements
|
||||
template<typename S = R, std::enable_if_t<!is_async_resource<S>::value && is_resource<S>::value, int> = 0>
|
||||
Buffer(size_t count, NoInit) : Buffer(R(), count, noInit) {}
|
||||
|
||||
/// @brief Explicitly disallow copy construction and assignment operation
|
||||
Buffer(const Buffer&) = delete;
|
||||
Buffer& operator=(const Buffer&) = delete;
|
||||
|
||||
/// @brief Move c-tor; steals the allocation (and retained stream, if any)
|
||||
/// and leaves @c other empty.
|
||||
Buffer(Buffer&& other) noexcept
|
||||
: detail::StreamHolder<IsAsync>(other)
|
||||
, mResource(std::move(other.mResource))
|
||||
, mData(other.mData)
|
||||
, mSize(other.mSize)
|
||||
{
|
||||
other.mData = nullptr;
|
||||
other.mSize = 0;
|
||||
}
|
||||
|
||||
/// @brief Move assignment; frees the current allocation first, then steals
|
||||
/// from @c other and leaves it empty. Self-move is a no-op.
|
||||
Buffer& operator=(Buffer&& other) noexcept
|
||||
{
|
||||
if (this != &other) {
|
||||
this->clear();
|
||||
static_cast<detail::StreamHolder<IsAsync>&>(*this) = other;
|
||||
mResource = std::move(other.mResource);
|
||||
mData = other.mData;
|
||||
mSize = other.mSize;
|
||||
other.mData = nullptr;
|
||||
other.mSize = 0;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
/// @brief Returns a deep copy of this buffer, allocated from a copy of the
|
||||
/// resource; the allocation and element copy are ordered on @c stream,
|
||||
/// which becomes the copy's retained stream.
|
||||
/// @param stream cuda stream the allocation and element copy are ordered on
|
||||
template<typename S = R, std::enable_if_t<is_async_resource<S>::value, int> = 0>
|
||||
Buffer copy(cudaStream_t stream) const
|
||||
{
|
||||
Buffer out(stream, mResource, mSize, noInit);
|
||||
if (mData) cudaCheck(cudaMemcpyAsync(out.mData, mData, this->size_bytes(), cudaMemcpyDefault, stream));
|
||||
return out;
|
||||
}
|
||||
|
||||
/// @brief Returns a deep copy of this buffer, allocated from a copy of the
|
||||
/// synchronous resource.
|
||||
template<typename S = R, std::enable_if_t<!is_async_resource<S>::value && is_resource<S>::value, int> = 0>
|
||||
Buffer copy() const
|
||||
{
|
||||
Buffer out(mResource, mSize, noInit);
|
||||
if (mData) cudaCheck(cudaMemcpy(out.mData, mData, this->size_bytes(), cudaMemcpyDefault));
|
||||
return out;
|
||||
}
|
||||
|
||||
/// @brief D-tor. A stream-ordered resource frees on the retained stream;
|
||||
/// a synchronous resource frees immediately.
|
||||
~Buffer() { this->clear(); }
|
||||
|
||||
/// @brief Returns the retained stream, i.e. the stream the buffer's memory
|
||||
/// will be freed on.
|
||||
template<typename S = R, std::enable_if_t<is_async_resource<S>::value, int> = 0>
|
||||
cudaStream_t stream() const { return this->mStream; }
|
||||
|
||||
/// @brief Replaces the retained stream without synchronizing; subsequent
|
||||
/// deallocation (and destruction) is ordered on @c stream instead.
|
||||
/// @param stream cuda stream subsequent deallocation is ordered on
|
||||
/// @warning The caller is responsible for ordering @c stream after any
|
||||
/// in-flight work that uses the buffer's memory.
|
||||
template<typename S = R, std::enable_if_t<is_async_resource<S>::value, int> = 0>
|
||||
void setStream(cudaStream_t stream) { this->mStream = stream; }
|
||||
|
||||
/// @brief Resizes the buffer to @c count elements, preserving the leading
|
||||
/// min(old, new) elements. Every operation — the new allocation, the
|
||||
/// prefix copy, and the free of the old block — is ordered on
|
||||
/// @c stream, which becomes the retained stream: the prefix copy is
|
||||
/// the old block's last use, so that is the stream its free must be
|
||||
/// ordered on.
|
||||
/// @param count number of elements
|
||||
/// @param stream cuda stream the reallocation is ordered on
|
||||
/// @warning The caller is responsible for ordering @c stream after any
|
||||
/// in-flight work on the previously retained stream that uses the
|
||||
/// buffer's memory.
|
||||
template<typename S = R, std::enable_if_t<is_async_resource<S>::value, int> = 0>
|
||||
void resize(size_t count, cudaStream_t stream)
|
||||
{
|
||||
if (count != mSize) {
|
||||
// No member is mutated until every throwing operation has
|
||||
// succeeded, so a failed resize leaves the buffer untouched --
|
||||
// including its retained stream.
|
||||
T* newData = count ? static_cast<T*>(mResource.allocate_async(checkedBytes(count), R::DEFAULT_ALIGNMENT, stream))
|
||||
: nullptr;
|
||||
if (newData && mData) {
|
||||
const size_t prefix = count < mSize ? count : mSize;
|
||||
try {
|
||||
cudaCheck(cudaMemcpyAsync(newData, mData, prefix * sizeof(T), cudaMemcpyDefault, stream));
|
||||
}
|
||||
catch (...) {
|
||||
mResource.deallocate_async(newData, checkedBytes(count), R::DEFAULT_ALIGNMENT, stream);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
this->mStream = stream;
|
||||
this->deallocate(mData, mSize); // ordered on stream: after the prefix copy
|
||||
mData = newData;
|
||||
mSize = count;
|
||||
}
|
||||
else {
|
||||
this->mStream = stream; // no reallocation: setStream semantics
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief Resizes the buffer to @c count elements through the synchronous
|
||||
/// resource, preserving the leading min(old, new) elements.
|
||||
/// @param count number of elements
|
||||
template<typename S = R, std::enable_if_t<!is_async_resource<S>::value && is_resource<S>::value, int> = 0>
|
||||
void resize(size_t count)
|
||||
{
|
||||
if (count == mSize) return;
|
||||
// No member is mutated until every throwing operation has succeeded,
|
||||
// so a failed resize leaves the buffer untouched.
|
||||
T* newData = count ? static_cast<T*>(mResource.allocate(checkedBytes(count), R::DEFAULT_ALIGNMENT))
|
||||
: nullptr;
|
||||
if (newData && mData) {
|
||||
const size_t prefix = count < mSize ? count : mSize;
|
||||
try {
|
||||
cudaCheck(cudaMemcpy(newData, mData, prefix * sizeof(T), cudaMemcpyDefault));
|
||||
}
|
||||
catch (...) {
|
||||
mResource.deallocate(newData, checkedBytes(count), R::DEFAULT_ALIGNMENT);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
this->deallocate(mData, mSize);
|
||||
mData = newData;
|
||||
mSize = count;
|
||||
}
|
||||
|
||||
/// @brief Returns a pointer to the elements, or nullptr if empty.
|
||||
T* data() { return mData; }
|
||||
const T* data() const { return mData; }
|
||||
|
||||
/// @brief Returns the number of elements.
|
||||
size_t size() const { return mSize; }
|
||||
|
||||
/// @brief Returns the size of the buffer's allocation in bytes.
|
||||
size_t size_bytes() const { return mSize * sizeof(T); }
|
||||
|
||||
/// @brief Returns true if this buffer manages no memory.
|
||||
bool empty() const { return mSize == 0; }
|
||||
|
||||
/// @brief Frees the buffer memory (if any) and resets to the empty state.
|
||||
void clear()
|
||||
{
|
||||
this->deallocate(mData, mSize);
|
||||
mData = nullptr;
|
||||
mSize = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
/// @brief Returns @c count * sizeof(T), throwing std::runtime_error if the
|
||||
/// byte size would overflow size_t instead of silently wrapping into
|
||||
/// a tiny allocation.
|
||||
/// @param count number of elements
|
||||
static size_t checkedBytes(size_t count)
|
||||
{
|
||||
if (count > std::numeric_limits<size_t>::max() / sizeof(T))
|
||||
throw std::runtime_error("nanovdb::cuda::Buffer: element count overflows the byte size");
|
||||
return count * sizeof(T);
|
||||
}
|
||||
|
||||
/// @brief Allocates @c count elements through the resource and records the
|
||||
/// new extent. A zero count allocates nothing.
|
||||
/// @param count number of elements
|
||||
/// @param stream cuda stream the allocation is ordered on; used by the
|
||||
/// stream-ordered form and ignored by the synchronous one
|
||||
void allocate(size_t count, cudaStream_t stream)
|
||||
{
|
||||
if (count) {
|
||||
if constexpr (IsAsync)
|
||||
mData = static_cast<T*>(mResource.allocate_async(checkedBytes(count), R::DEFAULT_ALIGNMENT, stream));
|
||||
else
|
||||
mData = static_cast<T*>(mResource.allocate(checkedBytes(count), R::DEFAULT_ALIGNMENT));
|
||||
}
|
||||
mSize = count;
|
||||
}
|
||||
|
||||
/// @brief Frees @c count elements at @c p through the resource; the
|
||||
/// stream-ordered form frees on the retained stream. Null is a no-op.
|
||||
/// @param p pointer to the elements to free
|
||||
/// @param count number of elements
|
||||
void deallocate(T* p, size_t count)
|
||||
{
|
||||
if (!p) return;
|
||||
if constexpr (IsAsync)
|
||||
mResource.deallocate_async(p, count * sizeof(T), R::DEFAULT_ALIGNMENT, this->mStream);
|
||||
else
|
||||
mResource.deallocate(p, count * sizeof(T), R::DEFAULT_ALIGNMENT);
|
||||
}
|
||||
}; // Buffer<T, R> class
|
||||
|
||||
/// @brief Non-owning, trivially copyable view of a contiguous range of @c T
|
||||
/// elements, with span semantics.
|
||||
/// @tparam T element type; spell constness in the element type
|
||||
/// (e.g. BufferView<const std::byte> is the read-only form), since
|
||||
/// const on the view itself is shallow.
|
||||
template<typename T>
|
||||
class BufferView
|
||||
{
|
||||
T* mData = nullptr;
|
||||
size_t mSize = 0; // element count
|
||||
|
||||
public:
|
||||
/// @brief Default c-tor of an empty view.
|
||||
BufferView() = default;
|
||||
|
||||
/// @brief C-tor viewing a contiguous range; the caller guarantees the
|
||||
/// underlying storage outlives every use of the view.
|
||||
/// @param data pointer to the first element
|
||||
/// @param count number of elements
|
||||
/// @throw std::runtime_error if @c data is null while @c count is non-zero.
|
||||
BufferView(T* data, size_t count) : mData(data), mSize(count)
|
||||
{
|
||||
if (data == nullptr && count != 0)
|
||||
throw std::runtime_error("BufferView: null data with a non-zero element count");
|
||||
}
|
||||
|
||||
/// @brief Returns a pointer to the viewed elements, or nullptr if empty.
|
||||
T* data() const { return mData; }
|
||||
|
||||
/// @brief Returns the number of viewed elements.
|
||||
size_t size() const { return mSize; }
|
||||
|
||||
/// @brief Returns the size of the viewed range in bytes.
|
||||
size_t size_bytes() const { return mSize * sizeof(T); }
|
||||
|
||||
/// @brief Returns true if this view references no elements.
|
||||
bool empty() const { return mSize == 0; }
|
||||
|
||||
/// @brief Detaches the view (nulls the pointer and zeroes the size)
|
||||
/// without touching the underlying storage. This is the one
|
||||
/// deliberate deviation from std::span, required by the buffer
|
||||
/// static interface: GridHandle::reset() calls buffer.clear().
|
||||
void clear()
|
||||
{
|
||||
mData = nullptr;
|
||||
mSize = 0;
|
||||
}
|
||||
}; // BufferView<T> class
|
||||
|
||||
} // namespace cuda
|
||||
|
||||
} // namespace nanovdb
|
||||
|
||||
#endif // end of NANOVDB_CUDA_BUFFER_H_HAS_BEEN_INCLUDED
|
||||
@@ -18,61 +18,43 @@ namespace cuda {
|
||||
/// @brief Default stream-ordered device memory resource. Allocations are made
|
||||
/// with cudaMallocAsync and freed with cudaFreeAsync via the
|
||||
/// util::cuda wrappers.
|
||||
/// @note Models the AsyncResource concept, which refines the synchronous
|
||||
/// Resource concept (as in CCCL's cuda::mr): the instance methods
|
||||
/// provide both the stream-ordered allocate_async / deallocate_async
|
||||
/// and the synchronous allocate / deallocate. The type is stateless,
|
||||
/// so a default-constructed instance adds no overhead. The static
|
||||
/// allocateAsync / deallocateAsync methods are deprecated.
|
||||
/// @note Exposes both the legacy static methods (allocateAsync /
|
||||
/// deallocateAsync, retained for source compatibility) and the
|
||||
/// instance methods (allocate_async / deallocate_async) that model
|
||||
/// the AsyncResource concept. The instance methods forward to the
|
||||
/// static ones, so a default-constructed instance is stateless and
|
||||
/// adds no overhead.
|
||||
class DeviceResource
|
||||
{
|
||||
public:
|
||||
// cudaMalloc aligns memory to 256 bytes by default
|
||||
static constexpr size_t DEFAULT_ALIGNMENT = 256;
|
||||
|
||||
/// @brief Stream-ordered allocation.
|
||||
/// @param bytes number of bytes to allocate
|
||||
/// @param stream cuda stream the allocation is ordered on
|
||||
/// @note the alignment parameter is unnamed: cudaMallocAsync always
|
||||
/// 256B-aligns
|
||||
void* allocate_async(size_t bytes, size_t, cudaStream_t stream) {
|
||||
static void* allocateAsync(size_t bytes, size_t, cudaStream_t stream) {
|
||||
void* p = nullptr;
|
||||
cudaCheck(util::cuda::mallocAsync(&p, bytes, stream));
|
||||
return p;
|
||||
}
|
||||
|
||||
/// @brief Stream-ordered deallocation.
|
||||
/// @param p pointer previously returned by allocate_async
|
||||
/// @param stream cuda stream the deallocation is ordered on
|
||||
void deallocate_async(void* p, size_t, size_t, cudaStream_t stream) {
|
||||
static void deallocateAsync(void *p, size_t, size_t, cudaStream_t stream) {
|
||||
cudaCheck(util::cuda::freeAsync(p, stream));
|
||||
}
|
||||
|
||||
/// @brief Synchronous allocation; the returned memory is immediately
|
||||
/// valid on every stream.
|
||||
/// @brief Instance allocation entry point modelling the AsyncResource concept.
|
||||
/// @param bytes number of bytes to allocate
|
||||
/// @param alignment requested alignment
|
||||
void* allocate(size_t bytes, size_t alignment) {
|
||||
void* p = this->allocate_async(bytes, alignment, cudaStream_t(0));
|
||||
cudaCheck(cudaStreamSynchronize(cudaStream_t(0)));
|
||||
return p;
|
||||
/// @param alignment requested alignment (cudaMallocAsync always 256B-aligns)
|
||||
/// @param stream cuda stream the allocation is ordered on
|
||||
void* allocate_async(size_t bytes, size_t alignment, cudaStream_t stream) {
|
||||
return allocateAsync(bytes, alignment, stream);
|
||||
}
|
||||
|
||||
/// @brief Synchronous deallocation; the caller guarantees that device
|
||||
/// work touching the memory has completed.
|
||||
/// @param p pointer previously returned by allocate or allocate_async
|
||||
void deallocate(void* p, size_t bytes, size_t alignment) {
|
||||
this->deallocate_async(p, bytes, alignment, cudaStream_t(0));
|
||||
}
|
||||
|
||||
[[deprecated("use the instance method allocate_async")]]
|
||||
static void* allocateAsync(size_t bytes, size_t alignment, cudaStream_t stream) {
|
||||
return DeviceResource().allocate_async(bytes, alignment, stream);
|
||||
}
|
||||
|
||||
[[deprecated("use the instance method deallocate_async")]]
|
||||
static void deallocateAsync(void *p, size_t bytes, size_t alignment, cudaStream_t stream) {
|
||||
DeviceResource().deallocate_async(p, bytes, alignment, stream);
|
||||
/// @brief Instance deallocation entry point modelling the AsyncResource concept.
|
||||
/// @param p pointer previously returned by allocate_async
|
||||
/// @param bytes size passed to the matching allocate_async
|
||||
/// @param alignment alignment passed to the matching allocate_async
|
||||
/// @param stream cuda stream the deallocation is ordered on
|
||||
void deallocate_async(void* p, size_t bytes, size_t alignment, cudaStream_t stream) {
|
||||
deallocateAsync(p, bytes, alignment, stream);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -105,44 +87,13 @@ inline R& default_resource()
|
||||
/// return resource.allocate(bytes, alignment); // synchronous
|
||||
/// }
|
||||
/// @endcode
|
||||
/// @note AsyncResource refines the synchronous Resource concept, matching
|
||||
/// CCCL's cuda::mr: an async resource must also provide the
|
||||
/// synchronous allocate / deallocate, so is_async_resource<R> implies
|
||||
/// is_resource<R>. A synchronous-only resource (e.g. PinnedResource)
|
||||
/// models just is_resource. The synchronous methods of a stream-ordered
|
||||
/// resource are typically thin delegates (allocate_async on the null
|
||||
/// stream followed by a stream synchronize).
|
||||
template <class R, class = void>
|
||||
struct is_async_resource : std::false_type {};
|
||||
|
||||
template <class R>
|
||||
struct is_async_resource<R, std::void_t<
|
||||
decltype(std::declval<R&>().allocate_async(size_t{0}, size_t{0}, cudaStream_t{0})),
|
||||
decltype(std::declval<R&>().deallocate_async(std::declval<void*>(), size_t{0}, size_t{0}, cudaStream_t{0})),
|
||||
decltype(std::declval<R&>().allocate(size_t{0}, size_t{0})),
|
||||
decltype(std::declval<R&>().deallocate(std::declval<void*>(), size_t{0}, size_t{0}))>>
|
||||
: std::true_type {};
|
||||
|
||||
/// @brief Detection trait: @c is_resource<R>::value is true iff @c R models
|
||||
/// the synchronous Resource concept, i.e. exposes
|
||||
/// allocate(size_t, size_t) and deallocate(void*, size_t, size_t).
|
||||
/// @details Use it to constrain code paths that need a resource without a
|
||||
/// stream argument, e.g. host-side allocations:
|
||||
/// @code
|
||||
/// template<typename R>
|
||||
/// void* allocate(R& resource, size_t bytes, size_t alignment)
|
||||
/// {
|
||||
/// static_assert(nanovdb::cuda::is_resource<R>::value, "R must be a synchronous resource");
|
||||
/// return resource.allocate(bytes, alignment);
|
||||
/// }
|
||||
/// @endcode
|
||||
template <class R, class = void>
|
||||
struct is_resource : std::false_type {};
|
||||
|
||||
template <class R>
|
||||
struct is_resource<R, std::void_t<
|
||||
decltype(std::declval<R&>().allocate(size_t{0}, size_t{0})),
|
||||
decltype(std::declval<R&>().deallocate(std::declval<void*>(), size_t{0}, size_t{0}))>>
|
||||
decltype(std::declval<R&>().deallocate_async(std::declval<void*>(), size_t{0}, size_t{0}, cudaStream_t{0}))>>
|
||||
: std::true_type {};
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// X-macro list of NanoVDB BuildT types currently exposed to Python.
|
||||
//
|
||||
// NanoVDB coding style says "avoid macro functions; use inline and templates
|
||||
// instead" (docs/codingstyle.txt). This file is a deliberate exception: it
|
||||
// must emit top-level template instantiations and binding registrations into
|
||||
// several translation units from one canonical list, which a C++ template
|
||||
// cannot do. The pattern is contained to this file plus the matching
|
||||
// per-consumer macro definitions; no runtime behavior is hidden behind a
|
||||
// macro function.
|
||||
//
|
||||
// Consumers define one or more of the kind-specific macros below before
|
||||
// including this file. Each macro is invoked once per matching type; any
|
||||
// macro the consumer does not define is treated as a no-op and reset after
|
||||
// the file is processed. This is the single point that must be edited when
|
||||
// adding (or removing) a Python-visible BuildT.
|
||||
//
|
||||
// Macros:
|
||||
// NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum)
|
||||
// Scalar value types with arithmetic semantics — exposed with full
|
||||
// NodeInfo + setVoxel accessors. Suffix forms Python class names
|
||||
// (e.g. "Float" -> "FloatGrid"). GridTypeEnum names the
|
||||
// nanovdb::GridType:: enumerator a grid of this BuildT carries
|
||||
// (used by the polymorphic handle.grid(n) dispatch).
|
||||
//
|
||||
// NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum)
|
||||
// Vector value types — exposed with a setVoxel accessor but no
|
||||
// NodeInfo. AccessorName is passed explicitly because the legacy
|
||||
// Python class names for the original two (Vec3f, RGBA8) are
|
||||
// inconsistent — kept as-is for backwards compatibility.
|
||||
//
|
||||
// NANOVDB_PY_FOR_EACH_POINT_BUILDT(T, Suffix, GridTypeEnum)
|
||||
// The nanovdb::Point build type — exposed with a bare accessor.
|
||||
// Point grids carry GridType::PointIndex (NOT a GridType::Point;
|
||||
// that enumerator doesn't exist), so GridTypeEnum is given
|
||||
// explicitly. Polymorphic dispatch routes PointIndex grids to
|
||||
// NanoGrid<Point>.
|
||||
//
|
||||
// NANOVDB_PY_FOR_EACH_READONLY_BUILDT(T, Suffix, GridTypeEnum)
|
||||
// BuildTs whose nanovdb::SetVoxel<T> is unavailable (bool, the
|
||||
// quantized Fp4/Fp8/Fp16/FpN types, the index types ValueIndex /
|
||||
// ValueOnIndex, and ValueMask). nanovdb::BuildTraits<T>::is_special
|
||||
// is true for all of these. Exposed with a bare accessor only — no
|
||||
// setVoxel, no NodeInfo. The Python accessor's getValue() returns
|
||||
// the type given by nanovdb::BuildToValueMap<T>::Type — float for
|
||||
// Fp4 / Fp8 / Fp16 / FpN, uint64 for ValueIndex / ValueOnIndex,
|
||||
// bool for ValueMask and for the BuildT=bool (Boolean) grid.
|
||||
//
|
||||
// NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(T, Suffix)
|
||||
// Subset that has C++ sampler specializations (used by PyMath
|
||||
// samplers and PyCreateNanoGrid create*Grid factories).
|
||||
|
||||
#ifndef NANOVDB_PY_FOR_EACH_SCALAR_BUILDT
|
||||
#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum)
|
||||
#define NANOVDB_PY_LOCAL_DEFINED_SCALAR
|
||||
#endif
|
||||
#ifndef NANOVDB_PY_FOR_EACH_VECTOR_BUILDT
|
||||
#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum)
|
||||
#define NANOVDB_PY_LOCAL_DEFINED_VECTOR
|
||||
#endif
|
||||
#ifndef NANOVDB_PY_FOR_EACH_POINT_BUILDT
|
||||
#define NANOVDB_PY_FOR_EACH_POINT_BUILDT(T, Suffix, GridTypeEnum)
|
||||
#define NANOVDB_PY_LOCAL_DEFINED_POINT
|
||||
#endif
|
||||
#ifndef NANOVDB_PY_FOR_EACH_READONLY_BUILDT
|
||||
#define NANOVDB_PY_FOR_EACH_READONLY_BUILDT(T, Suffix, GridTypeEnum)
|
||||
#define NANOVDB_PY_LOCAL_DEFINED_READONLY
|
||||
#endif
|
||||
#ifndef NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT
|
||||
#define NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(T, Suffix)
|
||||
#define NANOVDB_PY_LOCAL_DEFINED_SAMPLEABLE
|
||||
#endif
|
||||
|
||||
NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(float, Float, Float)
|
||||
NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(double, Double, Double)
|
||||
NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(int16_t, Int16, Int16)
|
||||
NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(int32_t, Int32, Int32)
|
||||
NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(int64_t, Int64, Int64)
|
||||
NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(uint8_t, UInt8, UInt8)
|
||||
NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(uint32_t, UInt32, UInt32)
|
||||
// nanovdb::Half is intentionally not bound. The source declares it as
|
||||
// `class Half{};` (an empty placeholder for IEEE 754 half-precision, see
|
||||
// NanoVDB.h around line 180) and the C++ ProbeValue<Half> chain is
|
||||
// inconsistent — leaf storage carries Half but the ProbeValue specialization
|
||||
// expects float, causing a type mismatch during instantiation. When the
|
||||
// upstream Half implementation lands this can be added in a follow-up.
|
||||
|
||||
NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::Vec3f, Vec3f,
|
||||
"Vec3fReadVectorAccessor", Vec3f)
|
||||
NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::Vec3d, Vec3d,
|
||||
"Vec3dReadAccessor", Vec3d)
|
||||
NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::Vec4f, Vec4f,
|
||||
"Vec4fReadAccessor", Vec4f)
|
||||
NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::Vec4d, Vec4d,
|
||||
"Vec4dReadAccessor", Vec4d)
|
||||
NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::Vec3u8, Vec3u8,
|
||||
"Vec3u8ReadAccessor", Vec3u8)
|
||||
NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::Vec3u16, Vec3u16,
|
||||
"Vec3u16ReadAccessor", Vec3u16)
|
||||
NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::math::Rgba8, RGBA8,
|
||||
"RGBA8ReadAccessor", RGBA8)
|
||||
|
||||
NANOVDB_PY_FOR_EACH_POINT_BUILDT(::nanovdb::Point, Point, PointIndex)
|
||||
|
||||
NANOVDB_PY_FOR_EACH_READONLY_BUILDT(bool, Boolean, Boolean)
|
||||
NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::Fp4, Fp4, Fp4)
|
||||
NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::Fp8, Fp8, Fp8)
|
||||
NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::Fp16, Fp16, Fp16)
|
||||
NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::FpN, FpN, FpN)
|
||||
NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::ValueIndex, Index, Index)
|
||||
NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::ValueOnIndex, OnIndex, OnIndex)
|
||||
NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::ValueMask, Mask, Mask)
|
||||
|
||||
NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(float, Float)
|
||||
NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(double, Double)
|
||||
NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(int32_t, Int32)
|
||||
NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(::nanovdb::Vec3f, Vec3f)
|
||||
|
||||
#ifdef NANOVDB_PY_LOCAL_DEFINED_SCALAR
|
||||
#undef NANOVDB_PY_LOCAL_DEFINED_SCALAR
|
||||
#endif
|
||||
#ifdef NANOVDB_PY_LOCAL_DEFINED_VECTOR
|
||||
#undef NANOVDB_PY_LOCAL_DEFINED_VECTOR
|
||||
#endif
|
||||
#ifdef NANOVDB_PY_LOCAL_DEFINED_POINT
|
||||
#undef NANOVDB_PY_LOCAL_DEFINED_POINT
|
||||
#endif
|
||||
#ifdef NANOVDB_PY_LOCAL_DEFINED_READONLY
|
||||
#undef NANOVDB_PY_LOCAL_DEFINED_READONLY
|
||||
#endif
|
||||
#ifdef NANOVDB_PY_LOCAL_DEFINED_SAMPLEABLE
|
||||
#undef NANOVDB_PY_LOCAL_DEFINED_SAMPLEABLE
|
||||
#endif
|
||||
|
||||
#undef NANOVDB_PY_FOR_EACH_SCALAR_BUILDT
|
||||
#undef NANOVDB_PY_FOR_EACH_VECTOR_BUILDT
|
||||
#undef NANOVDB_PY_FOR_EACH_POINT_BUILDT
|
||||
#undef NANOVDB_PY_FOR_EACH_READONLY_BUILDT
|
||||
#undef NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT
|
||||
@@ -4,8 +4,19 @@ option(NANOVDB_BUILD_PYTHON_UNITTESTS [=[
|
||||
"Include the NanoVDB Python unit test. Requires a python interpreter]=]
|
||||
${NANOVDB_BUILD_UNITTESTS})
|
||||
|
||||
# Stubs are only consumed from an installed wheel (they ship next to the .so
|
||||
# in nanovdb/), so default ON only when SKBUILD is driving the build. In an
|
||||
# in-source dev / OpenVDB-CI build the .pyi is not used and stub generation
|
||||
# can be skipped — which also dodges the macOS CI's sanitizer-runtime
|
||||
# DYLD_INSERT_LIBRARIES wrapper, under which Python dlopen'ing the .so
|
||||
# triggers TSan "Interceptors are not working" and aborts stubgen.
|
||||
option(NANOVDB_BUILD_PYTHON_STUBS
|
||||
"Generate .pyi type stubs for the nanovdb Python module (requires nanobind 2.x or newer)."
|
||||
${SKBUILD})
|
||||
|
||||
nanobind_add_module(nanovdb_python NB_STATIC
|
||||
NanoVDBModule.cc
|
||||
PyBuildGrid.cc
|
||||
PyCreateNanoGrid.cc
|
||||
PyGridChecksum.cc
|
||||
PyGridHandle.cc
|
||||
@@ -18,6 +29,8 @@ nanobind_add_module(nanovdb_python NB_STATIC
|
||||
PyPrimitives.cc
|
||||
PySampleFromVoxels.cc
|
||||
PyTools.cc
|
||||
PyTree.cc
|
||||
PyVoxelBlockManager.cc
|
||||
cuda/PyDeviceBuffer.cc
|
||||
cuda/PyDeviceGridHandle.cu
|
||||
cuda/PyPointsToGrid.cu
|
||||
@@ -33,10 +46,47 @@ if(SKBUILD)
|
||||
set_target_properties(nanovdb_python PROPERTIES INSTALL_RPATH "$ORIGIN/../../openvdb/lib")
|
||||
install(TARGETS nanovdb_python DESTINATION ${NANOVDB_INSTALL_LIBDIR})
|
||||
install(FILES __init__.py DESTINATION nanovdb)
|
||||
# Ship the nanovdb C/C++ headers inside the Python wheel so downstream
|
||||
# extension authors can compile against the same NanoVDB the wheel was
|
||||
# built with. nanovdb.get_include() resolves to this directory at runtime.
|
||||
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../
|
||||
DESTINATION nanovdb/include/nanovdb
|
||||
FILES_MATCHING
|
||||
PATTERN "*.h"
|
||||
PATTERN "*.cuh"
|
||||
PATTERN "python" EXCLUDE
|
||||
PATTERN "examples" EXCLUDE
|
||||
PATTERN "unittest" EXCLUDE
|
||||
PATTERN "cmd" EXCLUDE
|
||||
PATTERN "docs" EXCLUDE)
|
||||
else()
|
||||
install(TARGETS nanovdb_python DESTINATION ${VDB_PYTHON_INSTALL_DIRECTORY})
|
||||
endif()
|
||||
|
||||
# .pyi type stubs for IDE / type-checker support. Driven by the CMake helper
|
||||
# that ships with nanobind 2.x; silently skipped on older nanobind where the
|
||||
# helper does not exist.
|
||||
if(NANOVDB_BUILD_PYTHON_STUBS AND COMMAND nanobind_add_stub)
|
||||
set(NANOVDB_STUB_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}")
|
||||
nanobind_add_stub(nanovdb_python_stub
|
||||
MODULE nanovdb
|
||||
OUTPUT "${NANOVDB_STUB_OUTPUT_DIR}/nanovdb.pyi"
|
||||
PYTHON_PATH $<TARGET_FILE_DIR:nanovdb_python>
|
||||
DEPENDS nanovdb_python
|
||||
MARKER_FILE "${NANOVDB_STUB_OUTPUT_DIR}/py.typed"
|
||||
INCLUDE_PRIVATE)
|
||||
if(SKBUILD)
|
||||
install(FILES
|
||||
"${NANOVDB_STUB_OUTPUT_DIR}/nanovdb.pyi"
|
||||
"${NANOVDB_STUB_OUTPUT_DIR}/py.typed"
|
||||
DESTINATION nanovdb)
|
||||
endif()
|
||||
elseif(NANOVDB_BUILD_PYTHON_STUBS)
|
||||
message(STATUS
|
||||
"NANOVDB_BUILD_PYTHON_STUBS is ON but nanobind_add_stub is unavailable "
|
||||
"(nanobind >= 2.0 required). Skipping stub generation.")
|
||||
endif()
|
||||
|
||||
# pytest
|
||||
if(NANOVDB_BUILD_PYTHON_UNITTESTS)
|
||||
|
||||
@@ -49,12 +99,19 @@ if(NANOVDB_BUILD_PYTHON_UNITTESTS)
|
||||
COMMAND ${Python_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/TestNanoVDB.py -v
|
||||
WORKING_DIRECTORY "${NANOVDB_PYTHON_WORKING_DIR}")
|
||||
|
||||
# Smoke-run every example script (they self-skip when optional
|
||||
# dependencies such as NumPy or OpenVDB are unavailable).
|
||||
add_test(NAME pytest_nanovdb_examples
|
||||
COMMAND ${Python_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/TestExamples.py -v
|
||||
WORKING_DIRECTORY "${NANOVDB_PYTHON_WORKING_DIR}")
|
||||
set_tests_properties(pytest_nanovdb_examples PROPERTIES TIMEOUT 300)
|
||||
|
||||
if(WIN32)
|
||||
set(PYTHONPATH "$ENV{PYTHONPATH};${NANOVDB_PYTHON_WORKING_DIR}")
|
||||
string(REPLACE "\\;" ";" PYTHONPATH "${PYTHONPATH}")
|
||||
string(REPLACE ";" "\\;" PYTHONPATH "${PYTHONPATH}")
|
||||
set_tests_properties(pytest_nanovdb PROPERTIES ENVIRONMENT "PYTHONPATH=${PYTHONPATH}")
|
||||
set_tests_properties(pytest_nanovdb pytest_nanovdb_examples PROPERTIES ENVIRONMENT "PYTHONPATH=${PYTHONPATH}")
|
||||
else()
|
||||
set_tests_properties(pytest_nanovdb PROPERTIES ENVIRONMENT "PYTHONPATH=$ENV{PYTHONPATH}:${NANOVDB_PYTHON_WORKING_DIR}")
|
||||
set_tests_properties(pytest_nanovdb pytest_nanovdb_examples PROPERTIES ENVIRONMENT "PYTHONPATH=$ENV{PYTHONPATH}:${NANOVDB_PYTHON_WORKING_DIR}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
+1001
-194
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,239 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "PyBuildGrid.h"
|
||||
|
||||
#include <nanobind/stl/array.h>
|
||||
#include <nanobind/stl/string.h>
|
||||
|
||||
#include <nanovdb/NanoVDB.h>
|
||||
#include <nanovdb/HostBuffer.h>
|
||||
#include <nanovdb/tools/CreateNanoGrid.h>
|
||||
#include <nanovdb/tools/GridBuilder.h>
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
|
||||
namespace nb = nanobind;
|
||||
using namespace nb::literals;
|
||||
using namespace nanovdb;
|
||||
|
||||
namespace pynanovdb {
|
||||
|
||||
// Bind nanovdb::tools::build::Grid<BuildT> together with its
|
||||
// ValueAccessor<BuildT> and Tree<BuildT>::WriteAccessor proxies, plus a
|
||||
// .to_nanovdb() shortcut that bakes the build grid into a host NanoGrid
|
||||
// via tools::createNanoGrid.
|
||||
//
|
||||
// One instantiation per writable BuildT in BuildTypes.def (scalars +
|
||||
// vectors). All three classes live under the nanovdb.tools.build submodule;
|
||||
// the Python class names mirror the existing typed-grid naming so
|
||||
// nanovdb.tools.build.FloatGrid is the mutable counterpart of the read-only
|
||||
// nanovdb.FloatGrid.
|
||||
template<typename BuildT>
|
||||
static void defineBuildGrid(nb::module_& m,
|
||||
const char* gridName,
|
||||
const char* valueAccName,
|
||||
const char* writeAccName)
|
||||
{
|
||||
using GridT = tools::build::Grid<BuildT>;
|
||||
using TreeT = tools::build::Tree<BuildT>;
|
||||
using AccT = tools::build::ValueAccessor<BuildT>;
|
||||
using WriteAccT = typename TreeT::WriteAccessor;
|
||||
using ValueT = typename GridT::ValueType;
|
||||
|
||||
// ----- build::Grid<BuildT> -----
|
||||
nb::class_<GridT>(m, gridName,
|
||||
"Mutable host-side grid builder for this BuildT. Write voxels via "
|
||||
"setValue / getAccessor / getWriteAccessor, then call to_nanovdb() "
|
||||
"to bake a frozen NanoGrid GridHandle.")
|
||||
.def(nb::init<const ValueT&, const std::string&, GridClass>(),
|
||||
"background"_a,
|
||||
"name"_a = std::string(""),
|
||||
"gridClass"_a = GridClass::Unknown,
|
||||
"Construct an empty mutable build grid. Voxels read as "
|
||||
"background until written.")
|
||||
.def("getValue",
|
||||
[](const GridT& self, const Coord& ijk) -> ValueT {
|
||||
return self.getValue(ijk);
|
||||
},
|
||||
"ijk"_a,
|
||||
"Return the value at ijk (background if no leaf covers it).")
|
||||
.def("setValue",
|
||||
[](GridT& self, const Coord& ijk, const ValueT& value) {
|
||||
self.setValue(ijk, value);
|
||||
},
|
||||
"ijk"_a, "value"_a,
|
||||
"Set the voxel value at ijk and mark the voxel active.")
|
||||
// build::Grid has no top-level isActive(ijk); the read path is via
|
||||
// ValueAccessor. Spin up a fresh accessor for the single query so
|
||||
// Python callers don't have to.
|
||||
.def("isActive",
|
||||
[](GridT& self, const Coord& ijk) {
|
||||
AccT acc = self.getAccessor();
|
||||
return acc.isActive(ijk);
|
||||
},
|
||||
"ijk"_a,
|
||||
"Return True iff ijk is in an active voxel. Equivalent to "
|
||||
"self.getAccessor().isActive(ijk), but allocates a fresh "
|
||||
"accessor for each call — for repeated queries use "
|
||||
"self.getAccessor() and reuse it.")
|
||||
.def("setValueOn",
|
||||
[](GridT& self, const Coord& ijk) {
|
||||
AccT acc = self.getAccessor();
|
||||
acc.setValueOn(ijk);
|
||||
},
|
||||
"ijk"_a,
|
||||
"Mark ijk active without changing the stored value. Equivalent "
|
||||
"to self.getAccessor().setValueOn(ijk).")
|
||||
.def("nodeCount",
|
||||
[](const GridT& self) -> std::array<size_t, 3> {
|
||||
return self.nodeCount();
|
||||
},
|
||||
"Return a 3-tuple (leaf_count, lower_count, upper_count).")
|
||||
.def("gridType", &GridT::gridType,
|
||||
"Return the GridType enumerator this BuildT carries.")
|
||||
.def("gridClass", &GridT::gridClass,
|
||||
"Return the GridClass assigned at construction time.")
|
||||
.def("getName", &GridT::getName,
|
||||
"Return the grid name (as passed at construction).")
|
||||
.def("setName", &GridT::setName, "name"_a,
|
||||
"Replace the grid name.")
|
||||
.def("setTransform", &GridT::setTransform,
|
||||
"scale"_a = 1.0,
|
||||
"translation"_a = Vec3d(0.0),
|
||||
"Set an affine index-to-world map from a uniform scale and "
|
||||
"translation. Replaces any prior transform.")
|
||||
.def_prop_ro("background",
|
||||
[](const GridT& self) -> ValueT {
|
||||
return self.mRoot.background();
|
||||
},
|
||||
"The background value supplied at construction.")
|
||||
.def("getAccessor", &GridT::getAccessor,
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Return a ValueAccessor wired to this grid's root. Thread-safe "
|
||||
"for reads, NOT thread-safe for writes. The accessor borrows "
|
||||
"from this grid — the grid must outlive it.")
|
||||
// WriteAccessor's defaulted move constructor would leave its
|
||||
// internal ValueAccessor's `mRoot&` reference dangling (it points
|
||||
// into the moved-from WriteAccessor's own mRoot field). Bypass
|
||||
// the move path entirely by heap-allocating and handing nanobind
|
||||
// ownership — the C++ object stays at a stable address for its
|
||||
// whole lifetime.
|
||||
.def("getWriteAccessor",
|
||||
[](GridT& self) -> WriteAccT* {
|
||||
return new WriteAccT(self.mRoot, self.mMutex);
|
||||
},
|
||||
nb::rv_policy::take_ownership,
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Return a WriteAccessor for thread-safe writes; the accessor "
|
||||
"buffers changes into a private root and merges them into the "
|
||||
"parent grid on destruction (or on an explicit merge() call). "
|
||||
"Held by nanobind on the heap so the accessor's internal "
|
||||
"references stay valid.")
|
||||
// Baking a large grid is the most expensive operation on this
|
||||
// class; release the GIL so other Python threads can run during
|
||||
// the conversion (the lambda only touches C++ state).
|
||||
.def("to_nanovdb",
|
||||
[](const GridT& self,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
int verbose) {
|
||||
return tools::createNanoGrid<GridT, BuildT, HostBuffer>(
|
||||
self, sMode, cMode, verbose);
|
||||
},
|
||||
nb::call_guard<nb::gil_scoped_release>(),
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"verbose"_a = 0,
|
||||
"Bake this mutable grid into a host NanoGrid<BuildT> and "
|
||||
"return its GridHandle. The build grid is unchanged. "
|
||||
"Releases the GIL during conversion.");
|
||||
|
||||
// ----- build::ValueAccessor<BuildT> -----
|
||||
//
|
||||
// Move-only (copy is deleted) — returned by getAccessor(). Caches the
|
||||
// last leaf / lower / upper node it touched, so repeated access to
|
||||
// neighboring coordinates is fast.
|
||||
nb::class_<AccT>(m, valueAccName,
|
||||
"Move-only read/write accessor returned by build.Grid.getAccessor(). "
|
||||
"Caches the most recently visited tree path for fast neighbor access.")
|
||||
.def("getValue",
|
||||
[](const AccT& self, const Coord& ijk) -> ValueT {
|
||||
return self.getValue(ijk);
|
||||
},
|
||||
"ijk"_a, "Return the value at ijk (uses the cache).")
|
||||
.def("setValue",
|
||||
[](AccT& self, const Coord& ijk, const ValueT& value) {
|
||||
self.setValue(ijk, value);
|
||||
},
|
||||
"ijk"_a, "value"_a,
|
||||
"Set the value at ijk and mark it active (uses the cache).")
|
||||
.def("setValueOn",
|
||||
[](AccT& self, const Coord& ijk) { self.setValueOn(ijk); },
|
||||
"ijk"_a,
|
||||
"Mark ijk active without changing the stored value.")
|
||||
.def("isActive",
|
||||
[](const AccT& self, const Coord& ijk) {
|
||||
return self.isActive(ijk);
|
||||
},
|
||||
"ijk"_a,
|
||||
"Return True iff ijk is in an active voxel.")
|
||||
.def("isValueOn",
|
||||
[](const AccT& self, const Coord& ijk) {
|
||||
return self.isValueOn(ijk);
|
||||
},
|
||||
"ijk"_a,
|
||||
"Alias for isActive(ijk).");
|
||||
|
||||
// ----- build::Tree<BuildT>::WriteAccessor -----
|
||||
//
|
||||
// Move-only. Holds its own root node + a reference to the parent root's
|
||||
// mutex; on destruction (or explicit merge()) it locks the mutex and
|
||||
// splices its buffered nodes into the parent. Designed for multi-thread
|
||||
// writes — one WriteAccessor per thread, no shared mutable state.
|
||||
nb::class_<WriteAccT>(m, writeAccName,
|
||||
"Thread-safe write accessor returned by build.Grid.getWriteAccessor(). "
|
||||
"Buffers writes into a private root and merges them into the parent "
|
||||
"grid on destruction or explicit merge().")
|
||||
.def("setValue",
|
||||
[](WriteAccT& self, const Coord& ijk, const ValueT& value) {
|
||||
self.setValue(ijk, value);
|
||||
},
|
||||
"ijk"_a, "value"_a,
|
||||
"Buffer a set into this accessor's private root.")
|
||||
.def("setValueOn",
|
||||
[](WriteAccT& self, const Coord& ijk) { self.setValueOn(ijk); },
|
||||
"ijk"_a,
|
||||
"Buffer an active-state set into this accessor's private root.")
|
||||
.def("merge", &WriteAccT::merge,
|
||||
"Lock the parent mutex and splice this accessor's buffered "
|
||||
"nodes into the parent grid. Called automatically when the "
|
||||
"accessor is destroyed; calling it explicitly is only "
|
||||
"necessary if you want the changes visible to the parent "
|
||||
"before the accessor goes out of scope.");
|
||||
}
|
||||
|
||||
void defineBuildGridModule(nb::module_& toolsModule)
|
||||
{
|
||||
nb::module_ buildModule = toolsModule.def_submodule("build");
|
||||
buildModule.doc() =
|
||||
"Mutable, voxel-by-voxel CPU grid builder mirroring "
|
||||
"nanovdb::tools::build::*. Construct a typed Grid (e.g. "
|
||||
"FloatGrid(0.0, 'mygrid')), populate it with setValue / "
|
||||
"ValueAccessor / WriteAccessor, then call .to_nanovdb() to bake "
|
||||
"a host NanoGrid handle.";
|
||||
|
||||
// X-macro instantiation over every writable BuildT: scalars (full
|
||||
// arithmetic) and vectors. Read-only special BuildTs (Boolean, Fp*,
|
||||
// Index, Mask) and Point are deliberately excluded — they have no
|
||||
// SetValue<T> specialization and can't be built voxel-by-voxel.
|
||||
#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
defineBuildGrid<T>(buildModule, #Suffix "Grid", \
|
||||
#Suffix "ValueAccessor", #Suffix "WriteAccessor");
|
||||
#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \
|
||||
defineBuildGrid<T>(buildModule, #Suffix "Grid", \
|
||||
#Suffix "ValueAccessor", #Suffix "WriteAccessor");
|
||||
#include "BuildTypes.def"
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#ifndef NANOVDB_PYBUILDGRID_HAS_BEEN_INCLUDED
|
||||
#define NANOVDB_PYBUILDGRID_HAS_BEEN_INCLUDED
|
||||
|
||||
#include <nanobind/nanobind.h>
|
||||
|
||||
namespace nb = nanobind;
|
||||
|
||||
namespace pynanovdb {
|
||||
|
||||
/// @brief Register the nanovdb.tools.build submodule and its per-BuildT
|
||||
/// Grid / ValueAccessor / WriteAccessor classes (one set per writable
|
||||
/// scalar and vector BuildT in BuildTypes.def). Constructs the submodule
|
||||
/// as `toolsModule.def_submodule("build")`.
|
||||
void defineBuildGridModule(nb::module_& toolsModule);
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
#endif
|
||||
@@ -11,6 +11,10 @@
|
||||
#include <nanovdb/tools/CreateNanoGrid.h>
|
||||
#include <nanovdb/tools/GridBuilder.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace nb = nanobind;
|
||||
using namespace nb::literals;
|
||||
using namespace nanovdb;
|
||||
@@ -35,20 +39,553 @@ GridHandle<HostBuffer> createNanoGridFromFunc(const BuildT&
|
||||
|
||||
template<typename BuildT> void defineCreateNanoGrid(nb::module_& m, const char* name)
|
||||
{
|
||||
m.def(name, &createNanoGridFromFunc<BuildT>, nb::call_guard<nb::gil_scoped_release>(), "background"_a, "name"_a, "gridClass"_a, "func"_a, "bbox"_a);
|
||||
m.def(name, &createNanoGridFromFunc<BuildT>, nb::call_guard<nb::gil_scoped_release>(), "background"_a, "name"_a, "gridClass"_a, "func"_a, "bbox"_a,
|
||||
"Construct a NanoGrid by evaluating func(Coord) over every voxel in bbox. "
|
||||
"Returns a GridHandle owning the freshly-built grid.");
|
||||
}
|
||||
|
||||
template<typename BufferT> void defineOpenToNanoVDB(nb::module_& m)
|
||||
{
|
||||
#ifdef NANOVDB_USE_OPENVDB
|
||||
m.def("openToNanoVDB", &tools::openToNanoVDB<BufferT>, "base"_a, "sMode"_a = tools::StatsMode::Default, "cMode"_a = CheckMode::Default, "verbose"_a = 0);
|
||||
m.def("openToNanoVDB", &tools::openToNanoVDB<BufferT>,
|
||||
"base"_a,
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"verbose"_a = 0,
|
||||
"Convert an OpenVDB base grid to a NanoVDB GridHandle.");
|
||||
#endif
|
||||
}
|
||||
|
||||
template void defineCreateNanoGrid<float>(nb::module_&, const char*);
|
||||
template void defineCreateNanoGrid<double>(nb::module_&, const char*);
|
||||
template void defineCreateNanoGrid<int32_t>(nb::module_&, const char*);
|
||||
template void defineCreateNanoGrid<Vec3f>(nb::module_&, const char*);
|
||||
// ============================================================================
|
||||
// Conversion bindings: AbsDiff/RelDiff oracle classes, and the polymorphic
|
||||
// createNanoGrid free functions for quantized + index destination BuildTs.
|
||||
// Each accepts source = NanoGrid<SrcBuildT> OR build::Grid<SrcBuildT>.
|
||||
// ============================================================================
|
||||
|
||||
namespace {
|
||||
|
||||
// ----- Quantized (Fp4/Fp8/Fp16) -----
|
||||
//
|
||||
// C++ signature: createNanoGrid<SrcGridT, DstBuildT, BufferT>(srcGrid,
|
||||
// sMode, cMode, ditherOn, verbose, buffer).
|
||||
//
|
||||
// Try SrcBuildT against both NanoGrid<SrcBuildT> and build::Grid<SrcBuildT>
|
||||
// and return an empty nb::object on no match so the caller can fall through
|
||||
// to the next SrcBuildT.
|
||||
//
|
||||
// GIL is held for the isinstance / cast dispatch (which touches the Python
|
||||
// object's type and reference graph) but released around the underlying
|
||||
// tools::createNanoGrid traversal — the source data lives in stable C++
|
||||
// storage whose lifetime is anchored by the Python wrapper passed in via
|
||||
// py_src, so it's safe to read without holding the GIL.
|
||||
template<typename SrcBuildT, typename DstBuildT>
|
||||
nb::object tryQuantizeFpX(nb::handle py_src,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
bool ditherOn,
|
||||
int verbose)
|
||||
{
|
||||
using NanoSrcT = NanoGrid<SrcBuildT>;
|
||||
using BuildSrcT = tools::build::Grid<SrcBuildT>;
|
||||
if (nb::isinstance<NanoSrcT>(py_src)) {
|
||||
const auto& src = nb::cast<const NanoSrcT&>(py_src);
|
||||
GridHandle<HostBuffer> handle;
|
||||
{
|
||||
nb::gil_scoped_release release;
|
||||
handle = tools::createNanoGrid<NanoSrcT, DstBuildT, HostBuffer>(
|
||||
src, sMode, cMode, ditherOn, verbose);
|
||||
}
|
||||
return nb::cast(std::move(handle));
|
||||
}
|
||||
if (nb::isinstance<BuildSrcT>(py_src)) {
|
||||
const auto& src = nb::cast<const BuildSrcT&>(py_src);
|
||||
GridHandle<HostBuffer> handle;
|
||||
{
|
||||
nb::gil_scoped_release release;
|
||||
handle = tools::createNanoGrid<BuildSrcT, DstBuildT, HostBuffer>(
|
||||
src, sMode, cMode, ditherOn, verbose);
|
||||
}
|
||||
return nb::cast(std::move(handle));
|
||||
}
|
||||
return nb::object();
|
||||
}
|
||||
|
||||
template<typename DstBuildT>
|
||||
nb::object createNanoGridFpX(nb::handle py_src,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
bool ditherOn,
|
||||
int verbose,
|
||||
const char* pyFnName)
|
||||
{
|
||||
// The C++ Fp{4,8,16,N} preProcess static_asserts SrcValueT == float;
|
||||
// double sources hit a compile-time error, so we accept float only.
|
||||
if (auto r = tryQuantizeFpX<float, DstBuildT>(
|
||||
py_src, sMode, cMode, ditherOn, verbose); r.is_valid()) return r;
|
||||
std::string msg(pyFnName);
|
||||
msg += ": source must be a FloatGrid or nanovdb.tools.build.FloatGrid "
|
||||
"(Fp4/Fp8/Fp16/FpN require a float source value type).";
|
||||
throw nb::type_error(msg.c_str());
|
||||
}
|
||||
|
||||
// ----- FpN (variable bit-width) -----
|
||||
//
|
||||
// C++ signature: createNanoGrid<SrcGridT, FpN, OracleT, BufferT>(srcGrid,
|
||||
// sMode, cMode, ditherOn, verbose, oracle, buffer). OracleT is AbsDiff or
|
||||
// RelDiff; the binding exposes both as separate Python overloads.
|
||||
template<typename SrcBuildT, typename OracleT>
|
||||
nb::object tryQuantizeFpN(nb::handle py_src,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
bool ditherOn,
|
||||
int verbose,
|
||||
const OracleT& oracle)
|
||||
{
|
||||
using NanoSrcT = NanoGrid<SrcBuildT>;
|
||||
using BuildSrcT = tools::build::Grid<SrcBuildT>;
|
||||
// Same GIL pattern as tryQuantizeFpX: hold the GIL through the
|
||||
// isinstance / cast dispatch, release it for the conversion.
|
||||
if (nb::isinstance<NanoSrcT>(py_src)) {
|
||||
const auto& src = nb::cast<const NanoSrcT&>(py_src);
|
||||
GridHandle<HostBuffer> handle;
|
||||
{
|
||||
nb::gil_scoped_release release;
|
||||
handle = tools::createNanoGrid<NanoSrcT, FpN, OracleT, HostBuffer>(
|
||||
src, sMode, cMode, ditherOn, verbose, oracle);
|
||||
}
|
||||
return nb::cast(std::move(handle));
|
||||
}
|
||||
if (nb::isinstance<BuildSrcT>(py_src)) {
|
||||
const auto& src = nb::cast<const BuildSrcT&>(py_src);
|
||||
GridHandle<HostBuffer> handle;
|
||||
{
|
||||
nb::gil_scoped_release release;
|
||||
handle = tools::createNanoGrid<BuildSrcT, FpN, OracleT, HostBuffer>(
|
||||
src, sMode, cMode, ditherOn, verbose, oracle);
|
||||
}
|
||||
return nb::cast(std::move(handle));
|
||||
}
|
||||
return nb::object();
|
||||
}
|
||||
|
||||
template<typename OracleT>
|
||||
nb::object createNanoGridFpNImpl(nb::handle py_src,
|
||||
const OracleT& oracle,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
bool ditherOn,
|
||||
int verbose)
|
||||
{
|
||||
if (auto r = tryQuantizeFpN<float, OracleT>(
|
||||
py_src, sMode, cMode, ditherOn, verbose, oracle); r.is_valid()) return r;
|
||||
throw nb::type_error(
|
||||
"createNanoGridFpN: source must be a FloatGrid or "
|
||||
"nanovdb.tools.build.FloatGrid (FpN requires a float source value type).");
|
||||
}
|
||||
|
||||
// ----- Index / OnIndex -----
|
||||
//
|
||||
// C++ signature: createNanoGrid<SrcGridT, DstBuildT, BufferT>(srcGrid,
|
||||
// channels, includeStats, includeTiles, verbose, buffer). DstBuildT is
|
||||
// ValueIndex or ValueOnIndex; the binding exposes both as separate
|
||||
// named functions. Source set is wider than the quantized variants —
|
||||
// any arithmetic or vector source can be re-cast as an index grid.
|
||||
template<typename SrcBuildT, typename DstBuildT>
|
||||
nb::object tryIndexify(nb::handle py_src,
|
||||
uint32_t channels,
|
||||
bool includeStats,
|
||||
bool includeTiles,
|
||||
int verbose)
|
||||
{
|
||||
using NanoSrcT = NanoGrid<SrcBuildT>;
|
||||
using BuildSrcT = tools::build::Grid<SrcBuildT>;
|
||||
// Same GIL pattern as tryQuantizeFpX.
|
||||
if (nb::isinstance<NanoSrcT>(py_src)) {
|
||||
const auto& src = nb::cast<const NanoSrcT&>(py_src);
|
||||
GridHandle<HostBuffer> handle;
|
||||
{
|
||||
nb::gil_scoped_release release;
|
||||
handle = tools::createNanoGrid<NanoSrcT, DstBuildT, HostBuffer>(
|
||||
src, channels, includeStats, includeTiles, verbose);
|
||||
}
|
||||
return nb::cast(std::move(handle));
|
||||
}
|
||||
if (nb::isinstance<BuildSrcT>(py_src)) {
|
||||
const auto& src = nb::cast<const BuildSrcT&>(py_src);
|
||||
GridHandle<HostBuffer> handle;
|
||||
{
|
||||
nb::gil_scoped_release release;
|
||||
handle = tools::createNanoGrid<BuildSrcT, DstBuildT, HostBuffer>(
|
||||
src, channels, includeStats, includeTiles, verbose);
|
||||
}
|
||||
return nb::cast(std::move(handle));
|
||||
}
|
||||
return nb::object();
|
||||
}
|
||||
|
||||
template<typename DstBuildT>
|
||||
nb::object createIndexImpl(nb::handle py_src,
|
||||
uint32_t channels,
|
||||
bool includeStats,
|
||||
bool includeTiles,
|
||||
int verbose,
|
||||
const char* pyFnName)
|
||||
{
|
||||
if (auto r = tryIndexify<float, DstBuildT>(py_src, channels, includeStats, includeTiles, verbose); r.is_valid()) return r;
|
||||
if (auto r = tryIndexify<double, DstBuildT>(py_src, channels, includeStats, includeTiles, verbose); r.is_valid()) return r;
|
||||
if (auto r = tryIndexify<int32_t, DstBuildT>(py_src, channels, includeStats, includeTiles, verbose); r.is_valid()) return r;
|
||||
if (auto r = tryIndexify<Vec3f, DstBuildT>(py_src, channels, includeStats, includeTiles, verbose); r.is_valid()) return r;
|
||||
std::string msg(pyFnName);
|
||||
msg += ": source must be a FloatGrid, DoubleGrid, Int32Grid, "
|
||||
"Vec3fGrid, or the matching nanovdb.tools.build.* mutable grid.";
|
||||
throw nb::type_error(msg.c_str());
|
||||
}
|
||||
|
||||
// ----- tools.CreateNanoGrid: converter class with blind-data authoring -----
|
||||
//
|
||||
// Mirrors nanovdb::tools::CreateNanoGrid<SrcGridT>. The C++ class is
|
||||
// templated on the source grid type, so this binding stores the source
|
||||
// Python object plus the recorded settings and addBlindData() calls, then
|
||||
// dispatches over the supported SrcBuildTs when getHandle() is called —
|
||||
// constructing the C++ converter, replaying the recorded state, and baking
|
||||
// the handle. The destination BuildT is the source BuildT (the C++
|
||||
// default); quantized and index destinations remain on the
|
||||
// createNanoGridFp* / createNanoGridIndex free functions above. Authored
|
||||
// channels come back zeroed — fill them through the writable NumPy view
|
||||
// returned by grid.getBlindData(n).
|
||||
|
||||
// Byte size of one element of the given blind-data GridType, or 0 when the
|
||||
// size cannot be derived (the caller must then pass it explicitly). Matches
|
||||
// the per-type size table enforced by GridBlindMetaData::isValid().
|
||||
uint32_t blindDataTypeSize(GridType dataType)
|
||||
{
|
||||
switch (dataType) {
|
||||
case GridType::Float: return 4u;
|
||||
case GridType::Double: return 8u;
|
||||
case GridType::Int16: return 2u;
|
||||
case GridType::Int32: return 4u;
|
||||
case GridType::Int64: return 8u;
|
||||
case GridType::UInt8: return 1u;
|
||||
case GridType::UInt32: return 4u;
|
||||
case GridType::Half: return 2u;
|
||||
case GridType::RGBA8: return 4u;
|
||||
case GridType::Fp8: return 1u;
|
||||
case GridType::Fp16: return 2u;
|
||||
case GridType::Vec3f: return 12u;
|
||||
case GridType::Vec3d: return 24u;
|
||||
case GridType::Vec4f: return 16u;
|
||||
case GridType::Vec4d: return 32u;
|
||||
case GridType::Vec3u8: return 3u;
|
||||
case GridType::Vec3u16: return 6u;
|
||||
default: return 0u;
|
||||
}
|
||||
}
|
||||
|
||||
class PyCreateNanoGrid
|
||||
{
|
||||
struct BlindDataSpec
|
||||
{
|
||||
std::string name;
|
||||
GridBlindDataSemantic semantic;
|
||||
GridBlindDataClass dataClass;
|
||||
GridType dataType;
|
||||
uint64_t count;
|
||||
uint32_t size;
|
||||
};
|
||||
|
||||
public:
|
||||
explicit PyCreateNanoGrid(nb::object src)
|
||||
: mSrc(std::move(src))
|
||||
{
|
||||
if (!(matches<float>() || matches<double>() ||
|
||||
matches<int32_t>() || matches<Vec3f>())) {
|
||||
throw nb::type_error(
|
||||
"CreateNanoGrid: source must be a FloatGrid, DoubleGrid, "
|
||||
"Int32Grid, Vec3fGrid, or the matching "
|
||||
"nanovdb.tools.build.* mutable grid.");
|
||||
}
|
||||
}
|
||||
|
||||
// Validates eagerly (the C++ ctor only NANOVDB_ASSERTs, which release
|
||||
// builds skip) so mistakes surface here rather than as an invalid grid.
|
||||
uint64_t addBlindData(const std::string& name,
|
||||
uint64_t count,
|
||||
GridType dataType,
|
||||
GridBlindDataSemantic semantic,
|
||||
GridBlindDataClass dataClass,
|
||||
uint32_t size)
|
||||
{
|
||||
if (name.size() >= GridBlindMetaData::MaxNameSize) {
|
||||
throw nb::value_error(
|
||||
"addBlindData: name exceeds the 255 character limit.");
|
||||
}
|
||||
if (size == 0u) size = blindDataTypeSize(dataType);
|
||||
if (size == 0u) {
|
||||
throw nb::value_error(
|
||||
"addBlindData: the element size cannot be derived from this "
|
||||
"dataType — pass size explicitly.");
|
||||
}
|
||||
const GridBlindMetaData meta(0, count, size, semantic, dataClass, dataType);
|
||||
if (!meta.isValid()) {
|
||||
throw nb::value_error(
|
||||
"addBlindData: invalid combination of dataSemantic, "
|
||||
"dataClass, dataType, and size.");
|
||||
}
|
||||
mBlind.push_back(BlindDataSpec{name, semantic, dataClass, dataType, count, size});
|
||||
return static_cast<uint64_t>(mBlind.size() - 1);
|
||||
}
|
||||
|
||||
void setStats(tools::StatsMode mode) { mStats = mode; }
|
||||
void setChecksum(CheckMode mode) { mChecksum = mode; }
|
||||
void setVerbose(int mode) { mVerbose = mode; }
|
||||
void enableDithering(bool on) { mDither = on; }
|
||||
|
||||
nb::object getHandle() const
|
||||
{
|
||||
if (auto r = tryGetHandle<float>(); r.is_valid()) return r;
|
||||
if (auto r = tryGetHandle<double>(); r.is_valid()) return r;
|
||||
if (auto r = tryGetHandle<int32_t>(); r.is_valid()) return r;
|
||||
if (auto r = tryGetHandle<Vec3f>(); r.is_valid()) return r;
|
||||
throw nb::type_error("CreateNanoGrid: unsupported source grid type.");
|
||||
}
|
||||
|
||||
private:
|
||||
template<typename SrcBuildT> bool matches() const
|
||||
{
|
||||
return nb::isinstance<NanoGrid<SrcBuildT>>(mSrc) ||
|
||||
nb::isinstance<tools::build::Grid<SrcBuildT>>(mSrc);
|
||||
}
|
||||
|
||||
template<typename SrcBuildT> nb::object tryGetHandle() const
|
||||
{
|
||||
using NanoSrcT = NanoGrid<SrcBuildT>;
|
||||
using BuildSrcT = tools::build::Grid<SrcBuildT>;
|
||||
if (nb::isinstance<NanoSrcT>(mSrc)) return this->bake(nb::cast<const NanoSrcT&>(mSrc));
|
||||
if (nb::isinstance<BuildSrcT>(mSrc)) return this->bake(nb::cast<const BuildSrcT&>(mSrc));
|
||||
return nb::object();
|
||||
}
|
||||
|
||||
// Same GIL pattern as tryQuantizeFpX: the dispatch above runs with the
|
||||
// GIL held, the traversal runs without it (the source's lifetime is
|
||||
// anchored by mSrc).
|
||||
template<typename SrcGridT> nb::object bake(const SrcGridT& src) const
|
||||
{
|
||||
GridHandle<HostBuffer> handle;
|
||||
{
|
||||
nb::gil_scoped_release release;
|
||||
tools::CreateNanoGrid<SrcGridT> converter(src);
|
||||
converter.setStats(mStats);
|
||||
converter.setChecksum(mChecksum);
|
||||
converter.setVerbose(mVerbose);
|
||||
converter.enableDithering(mDither);
|
||||
for (const auto& b : mBlind) {
|
||||
converter.addBlindData(b.name, b.semantic, b.dataClass, b.dataType,
|
||||
static_cast<size_t>(b.count),
|
||||
static_cast<size_t>(b.size));
|
||||
}
|
||||
handle = converter.getHandle();
|
||||
// The C++ converter allocates authored channels without clearing
|
||||
// them (C++ callers memcpy their payload in). Zero-fill here so
|
||||
// the NumPy view starts deterministic. The authored channels are
|
||||
// the first mBlind.size() blind-data entries — any converter-
|
||||
// added channel (e.g. a long grid name) is appended after them.
|
||||
if (!mBlind.empty()) {
|
||||
if (auto* dst = const_cast<GridData*>(handle.gridData())) {
|
||||
for (size_t i = 0; i < mBlind.size(); ++i) {
|
||||
const GridBlindMetaData* meta = dst->blindMetaData(uint32_t(i));
|
||||
std::memset(const_cast<void*>(meta->blindData()), 0,
|
||||
meta->blindDataSize());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nb::cast(std::move(handle));
|
||||
}
|
||||
|
||||
nb::object mSrc;
|
||||
std::vector<BlindDataSpec> mBlind;
|
||||
tools::StatsMode mStats = tools::StatsMode::Default;
|
||||
CheckMode mChecksum = CheckMode::Default;
|
||||
int mVerbose = 0;
|
||||
bool mDither = false;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void defineCreateNanoGridConversions(nb::module_& toolsModule)
|
||||
{
|
||||
// ------ Oracle classes ------
|
||||
nb::class_<tools::AbsDiff>(toolsModule, "AbsDiff",
|
||||
"Compression oracle for FpN: accept the approximation when "
|
||||
"|exact - approx| <= tolerance. A tolerance of -1.0 (the "
|
||||
"default) means uninitialized; any non-negative value (including "
|
||||
"0.0) is treated as initialized by the operator bool() check, "
|
||||
"or the C++ create function can fill it in via init().")
|
||||
.def(nb::init<float>(), "tolerance"_a = -1.0f,
|
||||
"Construct an AbsDiff oracle with the given absolute tolerance.")
|
||||
.def("getTolerance", &tools::AbsDiff::getTolerance,
|
||||
"Return the current absolute tolerance.")
|
||||
.def("setTolerance", &tools::AbsDiff::setTolerance, "tolerance"_a,
|
||||
"Replace the current absolute tolerance.")
|
||||
.def("__bool__",
|
||||
[](const tools::AbsDiff& self) { return bool(self); },
|
||||
"True iff the tolerance has been initialized (>= 0).");
|
||||
|
||||
nb::class_<tools::RelDiff>(toolsModule, "RelDiff",
|
||||
"Compression oracle for FpN: accept the approximation when "
|
||||
"|exact - approx| / max(|exact|, |approx|) <= tolerance.")
|
||||
.def(nb::init<float>(), "tolerance"_a = -1.0f,
|
||||
"Construct a RelDiff oracle with the given relative tolerance.")
|
||||
.def("getTolerance", &tools::RelDiff::getTolerance,
|
||||
"Return the current relative tolerance.")
|
||||
.def("setTolerance", &tools::RelDiff::setTolerance, "tolerance"_a,
|
||||
"Replace the current relative tolerance.")
|
||||
.def("__bool__",
|
||||
[](const tools::RelDiff& self) { return bool(self); },
|
||||
"True iff the tolerance has been initialized (>= 0).");
|
||||
|
||||
// ------ Quantized fixed-width: Fp4 / Fp8 / Fp16 ------
|
||||
toolsModule.def("createNanoGridFp4",
|
||||
[](nb::handle src, tools::StatsMode sMode, CheckMode cMode,
|
||||
bool ditherOn, int verbose) {
|
||||
return createNanoGridFpX<Fp4>(src, sMode, cMode, ditherOn, verbose,
|
||||
"createNanoGridFp4");
|
||||
},
|
||||
"src"_a, "sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default, "ditherOn"_a = false, "verbose"_a = 0,
|
||||
"Quantize a NanoGrid<float> or nanovdb.tools.build.FloatGrid into a "
|
||||
"NanoGrid<Fp4> (4 bits per voxel). ditherOn adds sub-quantum "
|
||||
"noise to break up banding.");
|
||||
|
||||
toolsModule.def("createNanoGridFp8",
|
||||
[](nb::handle src, tools::StatsMode sMode, CheckMode cMode,
|
||||
bool ditherOn, int verbose) {
|
||||
return createNanoGridFpX<Fp8>(src, sMode, cMode, ditherOn, verbose,
|
||||
"createNanoGridFp8");
|
||||
},
|
||||
"src"_a, "sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default, "ditherOn"_a = false, "verbose"_a = 0,
|
||||
"Quantize a NanoGrid<float> or nanovdb.tools.build.FloatGrid into a NanoGrid<Fp8> (8 bits "
|
||||
"per voxel). ditherOn adds sub-quantum noise.");
|
||||
|
||||
toolsModule.def("createNanoGridFp16",
|
||||
[](nb::handle src, tools::StatsMode sMode, CheckMode cMode,
|
||||
bool ditherOn, int verbose) {
|
||||
return createNanoGridFpX<Fp16>(src, sMode, cMode, ditherOn, verbose,
|
||||
"createNanoGridFp16");
|
||||
},
|
||||
"src"_a, "sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default, "ditherOn"_a = false, "verbose"_a = 0,
|
||||
"Quantize a NanoGrid<float> or nanovdb.tools.build.FloatGrid into a NanoGrid<Fp16> (16 bits "
|
||||
"per voxel). ditherOn adds sub-quantum noise.");
|
||||
|
||||
// ------ Variable bit-width: FpN with AbsDiff or RelDiff oracle ------
|
||||
//
|
||||
// Two overloads — one per oracle type. Python dispatch picks the
|
||||
// right one from the oracle argument's type. The createNanoGrid C++
|
||||
// template's parameter order is (src, sMode, cMode, ditherOn, verbose,
|
||||
// oracle, buffer); we reorder for Python so oracle comes second
|
||||
// (most callers want to specify it explicitly), then mode/dither
|
||||
// parameters as kwargs with defaults.
|
||||
toolsModule.def("createNanoGridFpN",
|
||||
[](nb::handle src, const tools::AbsDiff& oracle,
|
||||
tools::StatsMode sMode, CheckMode cMode, bool ditherOn, int verbose) {
|
||||
return createNanoGridFpNImpl(src, oracle, sMode, cMode, ditherOn, verbose);
|
||||
},
|
||||
"src"_a, "oracle"_a = tools::AbsDiff(),
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default, "ditherOn"_a = false, "verbose"_a = 0,
|
||||
"Quantize a NanoGrid<float> or nanovdb.tools.build.FloatGrid into a NanoGrid<FpN> (variable "
|
||||
"bits per voxel; each leaf picks the smallest N that satisfies "
|
||||
"the oracle's tolerance). Pass an AbsDiff oracle for absolute "
|
||||
"error bound, or use the RelDiff overload for relative error.");
|
||||
|
||||
toolsModule.def("createNanoGridFpN",
|
||||
[](nb::handle src, const tools::RelDiff& oracle,
|
||||
tools::StatsMode sMode, CheckMode cMode, bool ditherOn, int verbose) {
|
||||
return createNanoGridFpNImpl(src, oracle, sMode, cMode, ditherOn, verbose);
|
||||
},
|
||||
"src"_a, "oracle"_a,
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default, "ditherOn"_a = false, "verbose"_a = 0,
|
||||
"FpN overload accepting a RelDiff oracle for relative error.");
|
||||
|
||||
// ------ Index / OnIndex ------
|
||||
//
|
||||
// createNanoGridIndex / createNanoGridOnIndex are the canonical names
|
||||
// for the broad-source-coverage index conversion bindings. A narrower
|
||||
// createOnIndexGrid factory still lives in PyVoxelBlockManager.cc as
|
||||
// the test scaffolding entry point used by the VBM unit tests.
|
||||
toolsModule.def("createNanoGridIndex",
|
||||
[](nb::handle src, uint32_t channels, bool includeStats,
|
||||
bool includeTiles, int verbose) {
|
||||
return createIndexImpl<ValueIndex>(
|
||||
src, channels, includeStats, includeTiles, verbose,
|
||||
"createNanoGridIndex");
|
||||
},
|
||||
"src"_a, "channels"_a = 0u, "includeStats"_a = true,
|
||||
"includeTiles"_a = true, "verbose"_a = 0,
|
||||
"Convert a source grid into a NanoGrid<ValueIndex>. Every voxel "
|
||||
"(active or inactive) gets a unique uint64 sequential index, with "
|
||||
"the original values stored as blind data when channels > 0.");
|
||||
|
||||
toolsModule.def("createNanoGridOnIndex",
|
||||
[](nb::handle src, uint32_t channels, bool includeStats,
|
||||
bool includeTiles, int verbose) {
|
||||
return createIndexImpl<ValueOnIndex>(
|
||||
src, channels, includeStats, includeTiles, verbose,
|
||||
"createNanoGridOnIndex");
|
||||
},
|
||||
"src"_a, "channels"_a = 0u, "includeStats"_a = true,
|
||||
"includeTiles"_a = true, "verbose"_a = 0,
|
||||
"Convert a source grid into a NanoGrid<ValueOnIndex>. Only the "
|
||||
"active voxels get a sequential index — the canonical input to "
|
||||
"buildVoxelBlockManager.");
|
||||
|
||||
// ------ CreateNanoGrid converter class (blind-data authoring) ------
|
||||
nb::class_<PyCreateNanoGrid>(toolsModule, "CreateNanoGrid",
|
||||
"Reusable converter mirroring nanovdb::tools::CreateNanoGrid. "
|
||||
"Construct from a NanoGrid or nanovdb.tools.build.* grid (float, "
|
||||
"double, int32, or Vec3f), optionally declare blind-data channels "
|
||||
"with addBlindData(), then bake a fresh NanoGrid of the same BuildT "
|
||||
"with getHandle(). Authored channels come back zero-filled — write "
|
||||
"their contents through the writable NumPy view returned by "
|
||||
"grid.getBlindData(n). For quantized (Fp*) or index destinations "
|
||||
"use the createNanoGridFp* / createNanoGridIndex functions instead.")
|
||||
.def(nb::init<nb::object>(), "srcGrid"_a,
|
||||
"Construct a converter reading from srcGrid (a NanoGrid or "
|
||||
"nanovdb.tools.build.* grid of BuildT float, double, int32, or "
|
||||
"Vec3f). The converter keeps srcGrid alive.")
|
||||
.def("addBlindData", &PyCreateNanoGrid::addBlindData,
|
||||
"name"_a, "count"_a, "dataType"_a = GridType::Float,
|
||||
"dataSemantic"_a = GridBlindDataSemantic::Unknown,
|
||||
"dataClass"_a = GridBlindDataClass::AttributeArray,
|
||||
"size"_a = 0u,
|
||||
"Declare a blind-data channel of count elements of dataType to "
|
||||
"be allocated in the destination grid, and return its channel "
|
||||
"index. size (bytes per element) is derived from dataType when "
|
||||
"omitted; pass it explicitly for dataTypes without a fixed "
|
||||
"element size. The C++ signature orders the parameters (name, "
|
||||
"dataSemantic, dataClass, dataType, count, size) — reordered "
|
||||
"here so the common case reads addBlindData(name, count).")
|
||||
.def("setStats", &PyCreateNanoGrid::setStats, "mode"_a,
|
||||
"Set the StatsMode used when baking the destination grid.")
|
||||
.def("setChecksum", &PyCreateNanoGrid::setChecksum, "mode"_a,
|
||||
"Set the CheckMode used when baking the destination grid.")
|
||||
.def("setVerbose", &PyCreateNanoGrid::setVerbose, "mode"_a = 1,
|
||||
"Set the verbosity level used when baking the destination grid.")
|
||||
.def("enableDithering", &PyCreateNanoGrid::enableDithering, "on"_a = true,
|
||||
"Toggle dithering of the destination grid (only meaningful for "
|
||||
"quantized BuildTs; kept for parity with the C++ class).")
|
||||
.def("getHandle", &PyCreateNanoGrid::getHandle,
|
||||
"Bake and return a GridHandle owning a NanoGrid of the source's "
|
||||
"BuildT, including any channels declared via addBlindData(). "
|
||||
"Each call bakes a fresh grid.");
|
||||
}
|
||||
|
||||
#define NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(T, Suffix) \
|
||||
template void defineCreateNanoGrid<T>(nb::module_&, const char*);
|
||||
#include "BuildTypes.def"
|
||||
|
||||
template void defineOpenToNanoVDB<HostBuffer>(nb::module_&);
|
||||
|
||||
|
||||
@@ -15,6 +15,21 @@ template<typename BuildT> void defineCreateNanoGrid(nb::module_& m, const char*
|
||||
template<typename BufferT> void defineOpenToNanoVDB(nb::module_& m);
|
||||
#endif
|
||||
|
||||
/// @brief Bind the AbsDiff / RelDiff quantization oracle classes and the
|
||||
/// polymorphic createNanoGridFp4 / Fp8 / Fp16 / FpN / Index / OnIndex
|
||||
/// free functions on the nanovdb.tools submodule. Sources accepted
|
||||
/// include both NanoGrid<SrcBuildT> and tools::build::Grid<SrcBuildT>:
|
||||
/// the quantized createNanoGridFp* / FpN paths accept float only
|
||||
/// (C++ Fp{4,8,16,N}::preProcess static-asserts SrcValueT == float);
|
||||
/// the createNanoGridIndex / OnIndex paths accept float, double,
|
||||
/// int32_t, and Vec3f sources. Additional source BuildTs can be
|
||||
/// added by extending the explicit try-each-SrcBuildT chains in
|
||||
/// createNanoGridFpX / FpNImpl / createIndexImpl. Also binds the
|
||||
/// tools.CreateNanoGrid converter class, which mirrors the C++
|
||||
/// tools::CreateNanoGrid and adds blind-data authoring via
|
||||
/// addBlindData() (same source BuildT set as the index paths).
|
||||
void defineCreateNanoGridConversions(nb::module_& toolsModule);
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
#endif
|
||||
|
||||
@@ -14,7 +14,10 @@ namespace pynanovdb {
|
||||
|
||||
void defineCheckMode(nb::module_& m)
|
||||
{
|
||||
nb::enum_<CheckMode>(m, "CheckMode")
|
||||
nb::enum_<CheckMode>(m, "CheckMode",
|
||||
"Selector controlling how aggressively a grid checksum is computed: "
|
||||
"Disable skips checksumming, Partial covers only the header, Full "
|
||||
"covers the whole grid, and Default picks the recommended mode.")
|
||||
.value("Disable", CheckMode::Disable)
|
||||
.value("Partial", CheckMode::Partial)
|
||||
.value("Full", CheckMode::Full)
|
||||
@@ -24,13 +27,65 @@ void defineCheckMode(nb::module_& m)
|
||||
|
||||
void defineChecksum(nb::module_& m)
|
||||
{
|
||||
nb::class_<Checksum>(m, "Checksum").def(nb::self == nb::self, "rhs"_a).def(nb::self != nb::self, "rhs"_a);
|
||||
nb::class_<Checksum>(m, "Checksum",
|
||||
"64-bit checksum value stored in a grid header. Produced by "
|
||||
"tools.evalChecksum and compared against the stored one by "
|
||||
"tools.validateChecksum.")
|
||||
.def(nb::self == nb::self, "rhs"_a,
|
||||
"Equality of two Checksum values.")
|
||||
.def(nb::self != nb::self, "rhs"_a,
|
||||
"Inequality of two Checksum values.")
|
||||
.def("isEmpty", &Checksum::isEmpty,
|
||||
"True iff no checksum is stored (checksumming was disabled).")
|
||||
.def("isHalf", &Checksum::isHalf,
|
||||
"True iff only the header portion (grid + tree + root) is checksummed.")
|
||||
.def("isFull", &Checksum::isFull,
|
||||
"True iff both the header portion and all nodes are checksummed.")
|
||||
.def("mode", &Checksum::mode,
|
||||
"CheckMode this checksum was computed with (Disable, Partial, or Full).");
|
||||
}
|
||||
|
||||
void defineUpdateChecksum(nb::module_& m)
|
||||
{
|
||||
m.def(
|
||||
"updateChecksum", [](GridData* gridData, CheckMode mode) { tools::updateChecksum(gridData, mode); }, "gridData"_a, "mode"_a);
|
||||
"updateChecksum", [](GridData* gridData, CheckMode mode) { tools::updateChecksum(gridData, mode); }, "gridData"_a, "mode"_a,
|
||||
"Recompute and store the checksum of gridData using the given CheckMode.");
|
||||
}
|
||||
|
||||
void defineEvalChecksumModule(nb::module_& toolsModule)
|
||||
{
|
||||
// tools.evalChecksum(grid, mode) — compute a fresh checksum for the
|
||||
// given grid without writing it back. Mirrors the GridData* overload
|
||||
// in tools/GridChecksum.h; the polymorphism over BuildT is implicit
|
||||
// because every NanoGrid<T> is-a GridData in the C++ hierarchy (and
|
||||
// the Python class binding declares NanoGrid<T> as derived from
|
||||
// GridData).
|
||||
toolsModule.def("evalChecksum",
|
||||
[](const GridData* gridData, CheckMode mode) -> Checksum {
|
||||
if (gridData == nullptr) {
|
||||
throw nb::value_error("evalChecksum: grid is None.");
|
||||
}
|
||||
return tools::evalChecksum(gridData, mode);
|
||||
},
|
||||
"grid"_a, "mode"_a = CheckMode::Default,
|
||||
nb::call_guard<nb::gil_scoped_release>(),
|
||||
"Compute and return the Checksum for the given grid using the "
|
||||
"specified CheckMode. Does not modify the grid.");
|
||||
|
||||
// tools.validateChecksum(grid, mode) — compare the stored checksum
|
||||
// against a freshly computed one and return a bool.
|
||||
toolsModule.def("validateChecksum",
|
||||
[](const GridData* gridData, CheckMode mode) -> bool {
|
||||
if (gridData == nullptr) {
|
||||
throw nb::value_error("validateChecksum: grid is None.");
|
||||
}
|
||||
return tools::validateChecksum(gridData, mode);
|
||||
},
|
||||
"grid"_a, "mode"_a = CheckMode::Default,
|
||||
nb::call_guard<nb::gil_scoped_release>(),
|
||||
"Return True iff the grid's stored checksum matches a freshly "
|
||||
"computed one for the given CheckMode. A grid with no stored "
|
||||
"checksum (Checksum.isEmpty()) is considered valid.");
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
@@ -13,6 +13,11 @@ void defineCheckMode(nb::module_& m);
|
||||
void defineChecksum(nb::module_& m);
|
||||
void defineUpdateChecksum(nb::module_& m);
|
||||
|
||||
/// @brief Bind tools.evalChecksum and tools.validateChecksum on the
|
||||
/// nanovdb.tools submodule. Both accept any bound NanoGrid via
|
||||
/// GridData* upcast (handled by nanobind's class hierarchy).
|
||||
void defineEvalChecksumModule(nb::module_& toolsModule);
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
#endif
|
||||
|
||||
@@ -22,6 +22,7 @@ void defineHostGridHandle(nb::module_& m)
|
||||
new (&handle) GridHandle<BufferT>(std::move(buffer));
|
||||
},
|
||||
"t"_a.noconvert());
|
||||
defineGridHandleUtilities<BufferT>(m);
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
@@ -7,62 +7,226 @@
|
||||
#include <nanobind/stl/string.h>
|
||||
|
||||
#include <nanovdb/GridHandle.h>
|
||||
#include <nanovdb/NanoVDB.h>
|
||||
#include <nanovdb/tools/GridChecksum.h> // for tools::updateGridCount
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace nb = nanobind;
|
||||
|
||||
namespace pynanovdb {
|
||||
|
||||
// Polymorphic host-side `handle.grid(n)`: dispatch on gridType(n) to the
|
||||
// matching NanoGrid<BuildT> subclass currently bound in Python. Returns
|
||||
// None when the underlying BuildT is not bound in this build — see the
|
||||
// list of bound types in BuildTypes.def. The returned object is parented
|
||||
// to the handle so the handle is kept alive at least as long as the grid.
|
||||
template<typename BufferT>
|
||||
inline nb::object pyHostGrid(nb::handle py_handle, uint32_t n)
|
||||
{
|
||||
auto& handle = nb::cast<nanovdb::GridHandle<BufferT>&>(py_handle);
|
||||
if (n >= handle.gridCount()) return nb::none();
|
||||
switch (handle.gridType(n)) {
|
||||
#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
case nanovdb::GridType::GridTypeEnum: { \
|
||||
auto* grid = handle.template grid<T>(n); \
|
||||
return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \
|
||||
: nb::none(); \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \
|
||||
case nanovdb::GridType::GridTypeEnum: { \
|
||||
auto* grid = handle.template grid<T>(n); \
|
||||
return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \
|
||||
: nb::none(); \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_POINT_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
case nanovdb::GridType::GridTypeEnum: { \
|
||||
auto* grid = handle.template grid<T>(n); \
|
||||
return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \
|
||||
: nb::none(); \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_READONLY_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
case nanovdb::GridType::GridTypeEnum: { \
|
||||
auto* grid = handle.template grid<T>(n); \
|
||||
return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \
|
||||
: nb::none(); \
|
||||
}
|
||||
#include "BuildTypes.def"
|
||||
default:
|
||||
return nb::none();
|
||||
}
|
||||
}
|
||||
|
||||
// Free functions splitGrids / mergeGrids exposed at module scope. Templated
|
||||
// on BufferT so the same definitions work for both the host GridHandle and
|
||||
// the device GridHandle bindings.
|
||||
template<typename BufferT> void defineGridHandleUtilities(nb::module_& m)
|
||||
{
|
||||
using HandleT = nanovdb::GridHandle<BufferT>;
|
||||
m.def("splitGrids", [](const HandleT& handle) {
|
||||
auto handles = nanovdb::splitGrids(handle);
|
||||
nb::list out;
|
||||
for (auto& h : handles) {
|
||||
out.append(std::move(h));
|
||||
}
|
||||
return out;
|
||||
}, nb::arg("handle"),
|
||||
"Split a multi-grid handle into a list of single-grid handles, "
|
||||
"each owning a freshly-allocated buffer.");
|
||||
// mergeGrids: walk the Python sequence by CONST ref to each handle and
|
||||
// concatenate buffer bytes into a freshly-allocated output. The original
|
||||
// nanovdb::mergeGrids takes a `const std::vector<GridHandle>&`, but
|
||||
// building such a vector from a Python list requires moving from each
|
||||
// wrapper (GridHandle is move-only) — which would silently empty the
|
||||
// caller's `h1`/`h2` Python objects. Instead we inline the merge logic
|
||||
// here so we never need to move from the inputs.
|
||||
m.def("mergeGrids", [](nb::sequence handles) {
|
||||
// Collect const refs so we touch each Python wrapper exactly once.
|
||||
std::vector<const HandleT*> sources;
|
||||
sources.reserve(nb::len(handles));
|
||||
for (nb::handle item : handles) {
|
||||
sources.push_back(&nb::cast<const HandleT&>(item));
|
||||
}
|
||||
|
||||
uint64_t totalSize = 0;
|
||||
uint32_t totalGrids = 0;
|
||||
for (const HandleT* h : sources) {
|
||||
totalGrids += h->gridCount();
|
||||
for (uint32_t n = 0; n < h->gridCount(); ++n) {
|
||||
totalSize += h->gridSize(n);
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing to merge (empty sequence, or only empty handles): return an
|
||||
// empty handle. BufferT::create(0) is ill-defined — for HostBuffer it
|
||||
// yields a non-null data() over a zero-byte region, and the
|
||||
// GridHandle(buffer) ctor would then read a full GridData header out of
|
||||
// it (heap overflow / "invalid host buffer" throw).
|
||||
if (totalGrids == 0) return HandleT();
|
||||
|
||||
auto buffer = BufferT::create(totalSize);
|
||||
uint8_t* dst = static_cast<uint8_t*>(buffer.data());
|
||||
uint32_t writeIndex = 0;
|
||||
for (const HandleT* h : sources) {
|
||||
for (uint32_t n = 0; n < h->gridCount(); ++n) {
|
||||
// gridData(n) is the authoritative per-grid start pointer (it
|
||||
// applies mMetaData[n].offset), so we don't assume the source
|
||||
// grids are laid out contiguously in the buffer.
|
||||
const uint64_t gs = h->gridSize(n);
|
||||
std::memcpy(dst, h->gridData(n), gs);
|
||||
auto* gd = reinterpret_cast<nanovdb::GridData*>(dst);
|
||||
nanovdb::tools::updateGridCount(gd, writeIndex++, totalGrids);
|
||||
dst += gs;
|
||||
}
|
||||
}
|
||||
return HandleT(std::move(buffer));
|
||||
}, nb::arg("handles"),
|
||||
"Combine a list of GridHandles into a single multi-grid GridHandle. "
|
||||
"Input handles are read by const reference; the new handle owns a "
|
||||
"freshly-allocated buffer and the inputs are left untouched.");
|
||||
}
|
||||
|
||||
template<typename BufferT> nb::class_<nanovdb::GridHandle<BufferT>> defineGridHandle(nb::module_& m, const char* name)
|
||||
{
|
||||
return nb::class_<nanovdb::GridHandle<BufferT>>(m, name)
|
||||
.def(nb::init<>())
|
||||
.def("reset", &nanovdb::GridHandle<BufferT>::reset)
|
||||
.def("size", &nanovdb::GridHandle<BufferT>::bufferSize)
|
||||
.def("isEmpty", &nanovdb::GridHandle<BufferT>::isEmpty)
|
||||
.def("empty", &nanovdb::GridHandle<BufferT>::empty)
|
||||
return nb::class_<nanovdb::GridHandle<BufferT>>(m, name,
|
||||
"Owns a buffer holding one or more serialized NanoVDB grids. "
|
||||
"Construct via nanovdb.tools.create* factories or nanovdb.io.readGrid(s); "
|
||||
"access individual grids via handle.grid(n).")
|
||||
.def(nb::init<>(),
|
||||
"Construct an empty handle. Use the nanovdb.tools.create* "
|
||||
"factories or nanovdb.io.readGrid(s) instead in normal use.")
|
||||
.def("reset", &nanovdb::GridHandle<BufferT>::reset,
|
||||
"Drop the underlying buffer; the handle becomes empty.")
|
||||
.def("size", &nanovdb::GridHandle<BufferT>::bufferSize,
|
||||
"Total byte size of the buffer backing this handle (sum of "
|
||||
"every grid plus any internal padding).")
|
||||
.def("isEmpty", &nanovdb::GridHandle<BufferT>::isEmpty,
|
||||
"True iff the handle owns no buffer.")
|
||||
.def("empty", &nanovdb::GridHandle<BufferT>::empty,
|
||||
"Same as isEmpty(). Retained for parity with the C++ "
|
||||
"GridHandle::empty() member.")
|
||||
.def(
|
||||
"__bool__", [](const nanovdb::GridHandle<BufferT>& handle) { handle.empty(); }, nb::is_operator())
|
||||
.def("floatGrid", nb::overload_cast<uint32_t>(&nanovdb::GridHandle<BufferT>::template grid<float>), nb::arg("n") = 0, nb::rv_policy::reference_internal)
|
||||
.def("doubleGrid",
|
||||
nb::overload_cast<uint32_t>(&nanovdb::GridHandle<BufferT>::template grid<double>),
|
||||
nb::arg("n") = 0,
|
||||
nb::rv_policy::reference_internal)
|
||||
.def("int32Grid",
|
||||
nb::overload_cast<uint32_t>(&nanovdb::GridHandle<BufferT>::template grid<int32_t>),
|
||||
nb::arg("n") = 0,
|
||||
nb::rv_policy::reference_internal)
|
||||
.def("vec3fGrid",
|
||||
nb::overload_cast<uint32_t>(&nanovdb::GridHandle<BufferT>::template grid<nanovdb::Vec3f>),
|
||||
nb::arg("n") = 0,
|
||||
nb::rv_policy::reference_internal)
|
||||
.def("rgba8Grid",
|
||||
nb::overload_cast<uint32_t>(&nanovdb::GridHandle<BufferT>::template grid<nanovdb::math::Rgba8>),
|
||||
nb::arg("n") = 0,
|
||||
nb::rv_policy::reference_internal)
|
||||
.def("isPadded", &nanovdb::GridHandle<BufferT>::isPadded)
|
||||
.def("gridCount", &nanovdb::GridHandle<BufferT>::gridCount)
|
||||
.def("gridSize", &nanovdb::GridHandle<BufferT>::gridSize, nb::arg("n") = 0)
|
||||
.def("gridType", &nanovdb::GridHandle<BufferT>::gridType, nb::arg("n") = 0)
|
||||
"__bool__",
|
||||
[](const nanovdb::GridHandle<BufferT>& handle) { return !handle.empty(); },
|
||||
nb::is_operator(),
|
||||
"True iff the handle owns a non-empty buffer (`not isEmpty()`).")
|
||||
.def("copy",
|
||||
[](const nanovdb::GridHandle<BufferT>& handle) {
|
||||
return handle.template copy<BufferT>();
|
||||
},
|
||||
"Return a deep copy of this GridHandle backed by a freshly-allocated buffer.")
|
||||
.def("grid", &pyHostGrid<BufferT>, nb::arg("n") = 0,
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Return the n-th grid as a typed Grid subclass selected by "
|
||||
"gridType(n), or None if the BuildT is not bound in Python. "
|
||||
"The returned grid keeps this handle alive.")
|
||||
.def("isPadded", &nanovdb::GridHandle<BufferT>::isPadded,
|
||||
"True iff this handle's buffer is aligned past the natural "
|
||||
"GridData alignment (used by the I/O code path).")
|
||||
.def("gridCount", &nanovdb::GridHandle<BufferT>::gridCount,
|
||||
"Number of grids stored in this handle.")
|
||||
.def(
|
||||
"gridSize",
|
||||
[](const nanovdb::GridHandle<BufferT>& handle, uint32_t n) {
|
||||
// GridHandle::gridSize(n) indexes mMetaData[n] unchecked, so an
|
||||
// out-of-range n (including any n on an empty handle) is UB.
|
||||
if (n >= handle.gridCount())
|
||||
throw nb::index_error("gridSize: grid index out of range [0, gridCount()).");
|
||||
return handle.gridSize(n);
|
||||
},
|
||||
nb::arg("n") = 0,
|
||||
"Byte size of the n-th grid (without padding).")
|
||||
.def(
|
||||
"gridType",
|
||||
[](const nanovdb::GridHandle<BufferT>& handle, uint32_t n) {
|
||||
// GridHandle::gridType(n) indexes mMetaData[n] unchecked, so an
|
||||
// out-of-range n (including any n on an empty handle) is UB.
|
||||
if (n >= handle.gridCount())
|
||||
throw nb::index_error("gridType: grid index out of range [0, gridCount()).");
|
||||
return handle.gridType(n);
|
||||
},
|
||||
nb::arg("n") = 0,
|
||||
"GridType enumerator of the n-th grid (e.g. GridType.Float). "
|
||||
"Cheap to query — does not require materializing the grid.")
|
||||
.def(
|
||||
"gridData",
|
||||
[](nanovdb::GridHandle<BufferT>& handle, uint32_t n) { return nb::bytes(handle.gridData(n), handle.gridSize(n)); },
|
||||
[](nanovdb::GridHandle<BufferT>& handle, uint32_t n) {
|
||||
// gridData(n) returns nullptr for out-of-range n, but gridSize(n)
|
||||
// below indexes mMetaData[n] unchecked — guard both here.
|
||||
if (n >= handle.gridCount())
|
||||
throw nb::index_error("gridData: grid index out of range [0, gridCount()).");
|
||||
return nb::bytes(handle.gridData(n), handle.gridSize(n));
|
||||
},
|
||||
nb::arg("n") = 0,
|
||||
nb::rv_policy::reference_internal)
|
||||
.def("write", nb::overload_cast<const std::string&>(&nanovdb::GridHandle<BufferT>::write, nb::const_), nb::arg("fileName"))
|
||||
.def("write", nb::overload_cast<const std::string&, uint32_t>(&nanovdb::GridHandle<BufferT>::write, nb::const_), nb::arg("fileName"), nb::arg("n"))
|
||||
nb::rv_policy::reference_internal,
|
||||
"Raw byte contents of the n-th grid as a Python bytes object. "
|
||||
"Useful for hashing or for handing off to non-NanoVDB tooling.")
|
||||
.def("write", nb::overload_cast<const std::string&>(&nanovdb::GridHandle<BufferT>::write, nb::const_),
|
||||
nb::arg("fileName"),
|
||||
"Write every grid in this handle to the given .nvdb file.")
|
||||
.def("write", nb::overload_cast<const std::string&, uint32_t>(&nanovdb::GridHandle<BufferT>::write, nb::const_),
|
||||
nb::arg("fileName"), nb::arg("n"),
|
||||
"Write just the n-th grid in this handle to the given .nvdb file.")
|
||||
.def(
|
||||
"read", [](nanovdb::GridHandle<BufferT>& handle, const std::string& fileName) { handle.read(fileName); }, nb::arg("fileName"))
|
||||
"read", [](nanovdb::GridHandle<BufferT>& handle, const std::string& fileName) { handle.read(fileName); },
|
||||
nb::arg("fileName"),
|
||||
"Replace this handle's contents with every grid read from the "
|
||||
"given .nvdb file.")
|
||||
.def(
|
||||
"read",
|
||||
[](nanovdb::GridHandle<BufferT>& handle, const std::string& fileName, uint32_t n) { handle.read(fileName, n); },
|
||||
nb::arg("fileName"),
|
||||
nb::arg("n"))
|
||||
nb::arg("n"),
|
||||
"Replace this handle's contents with the n-th grid read from "
|
||||
"the given .nvdb file.")
|
||||
.def(
|
||||
"read",
|
||||
[](nanovdb::GridHandle<BufferT>& handle, const std::string& fileName, const std::string& gridName) { handle.read(fileName, gridName); },
|
||||
nb::arg("fileName"),
|
||||
nb::arg("gridName"));
|
||||
|
||||
nb::arg("gridName"),
|
||||
"Replace this handle's contents with the grid of the given "
|
||||
"name read from the .nvdb file.");
|
||||
}
|
||||
|
||||
void defineHostGridHandle(nb::module_& m);
|
||||
|
||||
@@ -2,16 +2,26 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "PyGridStats.h"
|
||||
|
||||
#include <nanobind/stl/string.h>
|
||||
|
||||
#include <nanovdb/NanoVDB.h>
|
||||
#include <nanovdb/tools/GridStats.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace nb = nanobind;
|
||||
using namespace nb::literals;
|
||||
using namespace nanovdb;
|
||||
|
||||
namespace pynanovdb {
|
||||
|
||||
void defineStatsMode(nb::module_& m)
|
||||
{
|
||||
nb::enum_<tools::StatsMode>(m, "StatsMode")
|
||||
nb::enum_<tools::StatsMode>(m, "StatsMode",
|
||||
"Selector controlling which per-node statistics are computed by "
|
||||
"tools.updateGridStats: Disable skips stats, BBox refreshes only "
|
||||
"bounding boxes, MinMax adds min/max, and All adds average and "
|
||||
"standard deviation as well.")
|
||||
.value("Disable", tools::StatsMode::Disable)
|
||||
.value("BBox", tools::StatsMode::BBox)
|
||||
.value("MinMax", tools::StatsMode::MinMax)
|
||||
@@ -20,4 +30,201 @@ void defineStatsMode(nb::module_& m)
|
||||
.value("End", tools::StatsMode::End);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// ----- Extrema<ValueT> binding (rank 0 and rank 1 share the same surface) -----
|
||||
template<typename BuildT>
|
||||
static void defineExtrema(nb::module_& m, const char* name)
|
||||
{
|
||||
using ValueT = typename NanoGrid<BuildT>::ValueType;
|
||||
using ExtremaT = tools::Extrema<ValueT>;
|
||||
|
||||
nb::class_<ExtremaT>(m, name,
|
||||
"Running minimum / maximum accumulator over a stream of values. "
|
||||
"Build via repeated add(v) calls or via tools.getExtrema(grid, bbox).")
|
||||
.def(nb::init<>(),
|
||||
"Default-construct an Extrema with min = numeric_limits::max and "
|
||||
"max = numeric_limits::lowest, so any subsequent .add(v) gives "
|
||||
"exact min/max.")
|
||||
.def("min",
|
||||
[](const ExtremaT& self) -> ValueT { return self.min(); },
|
||||
"Return the minimum value observed so far.")
|
||||
.def("max",
|
||||
[](const ExtremaT& self) -> ValueT { return self.max(); },
|
||||
"Return the maximum value observed so far.")
|
||||
.def("add",
|
||||
[](ExtremaT& self, const ValueT& v) { self.add(v); },
|
||||
"value"_a,
|
||||
"Update min/max with a single sample.")
|
||||
.def("__bool__",
|
||||
[](const ExtremaT& self) { return bool(self); },
|
||||
"True iff the Extrema has accumulated at least one sample "
|
||||
"(i.e. min <= max).")
|
||||
.def_static("hasMinMax", &ExtremaT::hasMinMax,
|
||||
"True for value types where min/max is meaningful "
|
||||
"(everything except bool).")
|
||||
.def_static("hasAverage", &ExtremaT::hasAverage,
|
||||
"Always False — Extrema does not compute averages; "
|
||||
"use Stats for that.")
|
||||
.def_static("hasStdDeviation", &ExtremaT::hasStdDeviation,
|
||||
"Always False — Extrema does not compute standard "
|
||||
"deviation; use Stats for that.")
|
||||
.def_static("hasStats", &ExtremaT::hasStats,
|
||||
"True iff the value type supports the min/max bookkeeping "
|
||||
"(everything except bool).");
|
||||
}
|
||||
|
||||
// ----- Stats<ValueT> binding (inherits Extrema) -----
|
||||
template<typename BuildT>
|
||||
static void defineStats(nb::module_& m, const char* name)
|
||||
{
|
||||
using ValueT = typename NanoGrid<BuildT>::ValueType;
|
||||
using BaseT = tools::Extrema<ValueT>;
|
||||
using StatsT = tools::Stats<ValueT>;
|
||||
|
||||
nb::class_<StatsT, BaseT>(m, name,
|
||||
"Running min/max/mean/variance/std accumulator over a stream of "
|
||||
"values. Extends Extrema with sample-count-weighted moments.")
|
||||
.def(nb::init<>(),
|
||||
"Default-construct a Stats accumulator with zero samples.")
|
||||
.def("add",
|
||||
[](StatsT& self, const ValueT& v) { self.add(v); },
|
||||
"value"_a,
|
||||
"Add a single sample.")
|
||||
.def("size",
|
||||
[](const StatsT& self) -> size_t { return self.size(); },
|
||||
"Number of samples accumulated so far.")
|
||||
.def("avg",
|
||||
[](const StatsT& self) -> double { return self.avg(); },
|
||||
"Arithmetic mean of all samples.")
|
||||
.def("mean",
|
||||
[](const StatsT& self) -> double { return self.mean(); },
|
||||
"Alias for avg().")
|
||||
.def("var",
|
||||
[](const StatsT& self) -> double { return self.var(); },
|
||||
"Population variance (Sum(x-mean)^2 / N). Returns 0 if "
|
||||
"fewer than two samples have been added.")
|
||||
.def("variance",
|
||||
[](const StatsT& self) -> double { return self.variance(); },
|
||||
"Alias for var().")
|
||||
.def("std",
|
||||
[](const StatsT& self) -> double { return self.std(); },
|
||||
"Standard deviation = sqrt(var()).")
|
||||
.def("stdDev",
|
||||
[](const StatsT& self) -> double { return self.stdDev(); },
|
||||
"Alias for std().")
|
||||
.def_static("hasMinMax", &StatsT::hasMinMax,
|
||||
"True for value types where min/max is meaningful.")
|
||||
.def_static("hasAverage", &StatsT::hasAverage,
|
||||
"True for value types that support mean/variance.")
|
||||
.def_static("hasStdDeviation", &StatsT::hasStdDeviation,
|
||||
"True for value types that support standard deviation.")
|
||||
.def_static("hasStats", &StatsT::hasStats,
|
||||
"True for value types where full statistics is meaningful.");
|
||||
}
|
||||
|
||||
// ----- updateGridStats polymorphic dispatch ----------------------------------
|
||||
//
|
||||
// Match the IsNanoGridValid/callNanoGrid pattern: an Op struct with `known<T>`
|
||||
// (called for every BuildT that's in scope) and `unknown` (fallback). Only
|
||||
// scalar + vector BuildTs have a meaningful Stats specialization, so the
|
||||
// other arms raise.
|
||||
struct UpdateGridStatsOp
|
||||
{
|
||||
template<typename BuildT>
|
||||
static void known(GridData* gridData, tools::StatsMode mode)
|
||||
{
|
||||
using GridT = NanoGrid<BuildT>;
|
||||
using ValueT = typename GridT::ValueType;
|
||||
if constexpr (BuildTraits<BuildT>::is_special &&
|
||||
!util::is_same<bool, BuildT>::value) {
|
||||
// Special / quantized / index / mask BuildTs don't have an
|
||||
// arithmetic ValueT, so tools::updateGridStats's MinMax / All
|
||||
// branches would instantiate Stats<ValueT> / Extrema<ValueT>
|
||||
// with no meaningful semantics (and may not even compile).
|
||||
// The Disable and BBox branches use NoopStats, which works
|
||||
// for any ValueT — drive that directly here so the BBox path
|
||||
// remains available on special grids (it just recomputes
|
||||
// node bounding boxes without touching min/max/avg).
|
||||
if (mode == tools::StatsMode::Disable) {
|
||||
return;
|
||||
} else if (mode == tools::StatsMode::BBox) {
|
||||
tools::GridStats<GridT, tools::NoopStats<ValueT>> stats;
|
||||
stats.update(*static_cast<GridT*>(gridData));
|
||||
} else {
|
||||
throw nb::value_error(
|
||||
"updateGridStats: this grid's BuildT (special / "
|
||||
"quantized / index / mask) has no arithmetic value "
|
||||
"type — only StatsMode.Disable and StatsMode.BBox "
|
||||
"are supported.");
|
||||
}
|
||||
} else {
|
||||
tools::updateGridStats(static_cast<GridT*>(gridData), mode);
|
||||
}
|
||||
}
|
||||
static void unknown(GridData*, tools::StatsMode) {
|
||||
throw nb::value_error(
|
||||
"updateGridStats: unsupported GridType / BuildT combination.");
|
||||
}
|
||||
};
|
||||
|
||||
// ----- getExtrema (per-BuildT factory) ---------------------------------------
|
||||
template<typename BuildT>
|
||||
static tools::Extrema<typename NanoGrid<BuildT>::ValueType>
|
||||
pyGetExtrema(const NanoGrid<BuildT>& grid, const CoordBBox& bbox)
|
||||
{
|
||||
return tools::getExtrema<BuildT>(grid, bbox);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void defineGridStatsModule(nb::module_& toolsModule)
|
||||
{
|
||||
// Per-BuildT Extrema + Stats. One pair per scalar/vector BuildT — the
|
||||
// value types are all distinct so we get N pairs of new Python classes.
|
||||
#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
defineExtrema<T>(toolsModule, #Suffix "Extrema"); \
|
||||
defineStats<T>(toolsModule, #Suffix "Stats");
|
||||
#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \
|
||||
defineExtrema<T>(toolsModule, #Suffix "Extrema"); \
|
||||
defineStats<T>(toolsModule, #Suffix "Stats");
|
||||
#include "BuildTypes.def"
|
||||
|
||||
// Polymorphic updateGridStats. Accepts any bound NanoGrid<BuildT> (via
|
||||
// upcast to GridData*) and dispatches on its mGridType.
|
||||
toolsModule.def("updateGridStats",
|
||||
[](GridData* gridData, tools::StatsMode mode) {
|
||||
if (gridData == nullptr) {
|
||||
throw nb::value_error("updateGridStats: grid is None.");
|
||||
}
|
||||
callNanoGrid<UpdateGridStatsOp>(gridData, mode);
|
||||
},
|
||||
"grid"_a, "mode"_a = tools::StatsMode::Default,
|
||||
nb::call_guard<nb::gil_scoped_release>(),
|
||||
"Recompute and write per-node statistics into the given grid in "
|
||||
"place. Polymorphic over BuildT. Scalar, vector, and Boolean "
|
||||
"grids accept every StatsMode (Disable / BBox / MinMax / All); "
|
||||
"Boolean grids use the C++ NoopStats path internally regardless "
|
||||
"of mode because there's no arithmetic min/max/avg/dev on bool. "
|
||||
"Other special (quantized / index / mask) grids accept Disable "
|
||||
"and BBox (the latter recomputes node bounding boxes only); "
|
||||
"MinMax and All raise ValueError because their value type has "
|
||||
"no arithmetic semantics.");
|
||||
|
||||
// Per-BuildT getExtrema. We expose one overload per scalar/vector
|
||||
// BuildT — they each return a Python-side Extrema<ValueT> of the
|
||||
// matching name.
|
||||
#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
toolsModule.def("getExtrema", &pyGetExtrema<T>, \
|
||||
"grid"_a, "bbox"_a, nb::call_guard<nb::gil_scoped_release>(), \
|
||||
"Return the Extrema of all values in the grid that intersect " \
|
||||
"the given bbox.");
|
||||
#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \
|
||||
toolsModule.def("getExtrema", &pyGetExtrema<T>, \
|
||||
"grid"_a, "bbox"_a, nb::call_guard<nb::gil_scoped_release>(), \
|
||||
"Return the Extrema of all values in the grid that intersect " \
|
||||
"the given bbox.");
|
||||
#include "BuildTypes.def"
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
@@ -11,6 +11,12 @@ namespace pynanovdb {
|
||||
|
||||
void defineStatsMode(nb::module_& m);
|
||||
|
||||
/// @brief Register per-BuildT Extrema and Stats classes (one set per
|
||||
/// scalar / vector BuildT in BuildTypes.def) and the polymorphic
|
||||
/// tools.updateGridStats / tools.getExtrema helpers under the
|
||||
/// nanovdb.tools submodule.
|
||||
void defineGridStatsModule(nb::module_& toolsModule);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -2,13 +2,21 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "PyGridValidator.h"
|
||||
|
||||
#include <nanobind/operators.h>
|
||||
#include <nanobind/stl/pair.h>
|
||||
#include <nanobind/stl/string.h>
|
||||
|
||||
#include <nanovdb/GridHandle.h>
|
||||
#include <nanovdb/HostBuffer.h>
|
||||
#include <nanovdb/tools/GridValidator.h>
|
||||
#ifdef NANOVDB_USE_CUDA
|
||||
#include <nanovdb/cuda/DeviceBuffer.h>
|
||||
#endif
|
||||
|
||||
#include <nanobind/operators.h>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace nb = nanobind;
|
||||
using namespace nb::literals;
|
||||
@@ -18,7 +26,8 @@ namespace pynanovdb {
|
||||
|
||||
template<typename BufferT> void defineValidateGrids(nb::module_& m)
|
||||
{
|
||||
m.def("validateGrids", &tools::validateGrids<GridHandle<BufferT>>, "handle"_a, "mode"_a, "verbose"_a);
|
||||
m.def("validateGrids", &tools::validateGrids<GridHandle<BufferT>>,
|
||||
"handle"_a, "mode"_a, "verbose"_a);
|
||||
}
|
||||
|
||||
template void defineValidateGrids<HostBuffer>(nb::module_&);
|
||||
@@ -26,4 +35,126 @@ template void defineValidateGrids<HostBuffer>(nb::module_&);
|
||||
template void defineValidateGrids<cuda::DeviceBuffer>(nb::module_&);
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
// callNanoGrid op for checkGrid: writes into a fixed-size buffer and returns
|
||||
// it as a std::pair<bool, std::string> for nb::make_tuple consumption.
|
||||
//
|
||||
// tools::checkGrid writes error messages with util::sprint / util::strcpy,
|
||||
// neither of which is bounded — they trust the caller's buffer is big
|
||||
// enough. Size the buffer well above what any current error message
|
||||
// produces (the longest formatted message in GridValidator.h is on the
|
||||
// order of ~80 characters once both GridType and GridClass enumerator
|
||||
// names are stringified) and leave generous headroom for future
|
||||
// additions.
|
||||
struct CheckGridOp
|
||||
{
|
||||
static constexpr size_t kErrorBufSize = 4096;
|
||||
|
||||
template<typename BuildT>
|
||||
static std::pair<bool, std::string> known(const GridData* gridData,
|
||||
CheckMode mode)
|
||||
{
|
||||
char buf[kErrorBufSize];
|
||||
buf[0] = '\0';
|
||||
tools::checkGrid<BuildT>(
|
||||
static_cast<const NanoGrid<BuildT>*>(gridData), buf, mode);
|
||||
const bool ok = (buf[0] == '\0');
|
||||
return {ok, std::string(buf)};
|
||||
}
|
||||
static std::pair<bool, std::string> unknown(const GridData* gridData,
|
||||
CheckMode /*mode*/)
|
||||
{
|
||||
(void)gridData;
|
||||
return {false, "Unsupported GridType for checkGrid"};
|
||||
}
|
||||
};
|
||||
|
||||
// callNanoGrid op for isValid — wraps tools::isValid<BuildT> for every
|
||||
// switched-over BuildT.
|
||||
struct IsValidOp
|
||||
{
|
||||
template<typename BuildT>
|
||||
static bool known(const GridData* gridData, CheckMode mode, bool verbose)
|
||||
{
|
||||
return tools::isValid<BuildT>(
|
||||
static_cast<const NanoGrid<BuildT>*>(gridData), mode, verbose);
|
||||
}
|
||||
static bool unknown(const GridData* gridData, CheckMode /*mode*/, bool verbose)
|
||||
{
|
||||
if (verbose && gridData != nullptr) {
|
||||
char str[16];
|
||||
std::cerr << "Validation failed: Unsupported GridType: \""
|
||||
<< toStr(str, gridData->mGridType) << "\""
|
||||
<< std::endl;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void defineGridValidatorModule(nb::module_& toolsModule)
|
||||
{
|
||||
// Single-grid validate. Takes a handle, a grid index, and the
|
||||
// usual mode + verbose flags. Returns true iff the grid passes all
|
||||
// tests for the given mode. Bound for both host and device handles
|
||||
// (when CUDA is enabled), matching validateGrids' coverage —
|
||||
// tools::validateGrid does host-side dispatch via callNanoGrid on
|
||||
// the host-resident gridData() pointer that DeviceGridHandle also
|
||||
// exposes, so the same overload pair is appropriate.
|
||||
toolsModule.def("validateGrid",
|
||||
&tools::validateGrid<GridHandle<HostBuffer>>,
|
||||
"handle"_a, "gridID"_a,
|
||||
"mode"_a = CheckMode::Default, "verbose"_a = false,
|
||||
nb::call_guard<nb::gil_scoped_release>(),
|
||||
"Validate the gridID'th grid in the handle against the given "
|
||||
"CheckMode. Returns False (without raising) if gridID is out "
|
||||
"of range or the grid fails any check. CheckMode.Disable is a "
|
||||
"short-circuit that always returns True without inspecting "
|
||||
"the grid (even when gridID is out of range), matching the "
|
||||
"C++ behavior. Complements validateGrids() which checks the "
|
||||
"whole handle.");
|
||||
#ifdef NANOVDB_USE_CUDA
|
||||
toolsModule.def("validateGrid",
|
||||
&tools::validateGrid<GridHandle<cuda::DeviceBuffer>>,
|
||||
"handle"_a, "gridID"_a,
|
||||
"mode"_a = CheckMode::Default, "verbose"_a = false,
|
||||
nb::call_guard<nb::gil_scoped_release>(),
|
||||
"Validate the gridID'th grid in the device handle (uses the "
|
||||
"host-resident copy of the grid metadata for the actual "
|
||||
"checks). Same semantics as the host-handle overload.");
|
||||
#endif
|
||||
|
||||
// Polymorphic checkGrid — returns (ok, error_message). Mirrors the C++
|
||||
// char-buffer-out signature, but the buffer is hidden inside the
|
||||
// binding so Python callers get a Python str.
|
||||
toolsModule.def("checkGrid",
|
||||
[](const GridData* gridData, CheckMode mode)
|
||||
-> std::pair<bool, std::string> {
|
||||
if (gridData == nullptr) {
|
||||
return {false, "Grid is None"};
|
||||
}
|
||||
return callNanoGrid<CheckGridOp>(gridData, mode);
|
||||
},
|
||||
"grid"_a, "mode"_a = CheckMode::Full,
|
||||
nb::call_guard<nb::gil_scoped_release>(),
|
||||
"Run structural validation checks on the grid for the given "
|
||||
"CheckMode. Returns a (ok, error_message) tuple — error_message "
|
||||
"is empty when ok is True.");
|
||||
|
||||
// Polymorphic isValid — convenience wrapper. Same as checkGrid + a
|
||||
// checksum check, returning just the bool.
|
||||
toolsModule.def("isValid",
|
||||
[](const GridData* gridData, CheckMode mode, bool verbose) {
|
||||
if (gridData == nullptr) return false;
|
||||
return callNanoGrid<IsValidOp>(gridData, mode, verbose);
|
||||
},
|
||||
"grid"_a, "mode"_a = CheckMode::Default, "verbose"_a = false,
|
||||
nb::call_guard<nb::gil_scoped_release>(),
|
||||
"Return True iff the grid passes structural validation AND its "
|
||||
"stored checksum matches a freshly computed one for the given "
|
||||
"CheckMode.");
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
@@ -11,6 +11,12 @@ namespace pynanovdb {
|
||||
|
||||
template<typename BufferT> void defineValidateGrids(nb::module_& m);
|
||||
|
||||
/// @brief Register tools.validateGrid (single grid in a handle),
|
||||
/// tools.checkGrid (polymorphic, returns (bool, error_str)) and
|
||||
/// tools.isValid (polymorphic shortcut) under the nanovdb.tools
|
||||
/// submodule. Bound once; polymorphic dispatch uses callNanoGrid.
|
||||
void defineGridValidatorModule(nb::module_& toolsModule);
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
#endif
|
||||
|
||||
@@ -11,7 +11,10 @@ namespace pynanovdb {
|
||||
|
||||
void defineHostBuffer(nb::module_& m)
|
||||
{
|
||||
nb::class_<HostBuffer>(m, "HostBuffer");
|
||||
nb::class_<HostBuffer>(m, "HostBuffer",
|
||||
"Default host-side buffer used to back a GridHandle. Memory is "
|
||||
"owned by this buffer and freed when the handle (and therefore "
|
||||
"the buffer) is destroyed.");
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
@@ -21,40 +21,67 @@ namespace {
|
||||
|
||||
void defineFileGridMetaData(nb::module_& m)
|
||||
{
|
||||
nb::class_<io::FileMetaData>(m, "FileMetaData")
|
||||
.def_ro("gridSize", &io::FileMetaData::gridSize)
|
||||
.def_ro("fileSize", &io::FileMetaData::fileSize)
|
||||
.def_ro("nameKey", &io::FileMetaData::nameKey)
|
||||
.def_ro("voxelCount", &io::FileMetaData::voxelCount)
|
||||
.def_ro("gridType", &io::FileMetaData::gridType)
|
||||
.def_ro("gridClass", &io::FileMetaData::gridClass)
|
||||
.def_ro("indexBBox", &io::FileMetaData::indexBBox)
|
||||
.def_ro("worldBBox", &io::FileMetaData::worldBBox)
|
||||
.def_ro("voxelSize", &io::FileMetaData::voxelSize)
|
||||
.def_ro("nameSize", &io::FileMetaData::nameSize)
|
||||
nb::class_<io::FileMetaData>(m, "FileMetaData",
|
||||
"Per-grid header read from the .nvdb file index. Mirrors the C++ "
|
||||
"io::FileMetaData layout; subclassed by FileGridMetaData which adds "
|
||||
"the grid name string.")
|
||||
.def_ro("gridSize", &io::FileMetaData::gridSize,
|
||||
"Uncompressed grid size in bytes.")
|
||||
.def_ro("fileSize", &io::FileMetaData::fileSize,
|
||||
"On-disk byte size of this grid (post-codec).")
|
||||
.def_ro("nameKey", &io::FileMetaData::nameKey,
|
||||
"Hash of the grid name used as a fast lookup key.")
|
||||
.def_ro("voxelCount", &io::FileMetaData::voxelCount,
|
||||
"Number of active voxels in this grid.")
|
||||
.def_ro("gridType", &io::FileMetaData::gridType,
|
||||
"GridType enumerator naming the BuildT of this grid.")
|
||||
.def_ro("gridClass", &io::FileMetaData::gridClass,
|
||||
"GridClass enumerator (LevelSet, FogVolume, ...).")
|
||||
.def_ro("indexBBox", &io::FileMetaData::indexBBox,
|
||||
"Axis-aligned bounding box of active voxels in index space.")
|
||||
.def_ro("worldBBox", &io::FileMetaData::worldBBox,
|
||||
"Axis-aligned bounding box of active voxels in world space.")
|
||||
.def_ro("voxelSize", &io::FileMetaData::voxelSize,
|
||||
"World-space size of a single voxel.")
|
||||
.def_ro("nameSize", &io::FileMetaData::nameSize,
|
||||
"Length of the grid name string including the null terminator.")
|
||||
.def_prop_ro("nodeCount",
|
||||
[](io::FileMetaData& metaData) {
|
||||
return std::make_tuple(metaData.nodeCount[0], metaData.nodeCount[1], metaData.nodeCount[2], metaData.nodeCount[3]);
|
||||
})
|
||||
},
|
||||
"Tuple (leaf, lower, upper, root) of node counts in this grid.")
|
||||
.def_prop_ro("tileCount",
|
||||
[](io::FileMetaData& metaData) { return std::make_tuple(metaData.tileCount[0], metaData.tileCount[1], metaData.tileCount[2]); })
|
||||
.def_ro("codec", &io::FileMetaData::codec)
|
||||
.def_ro("blindDataCount", &io::FileMetaData::blindDataCount)
|
||||
.def_ro("version", &io::FileMetaData::version);
|
||||
[](io::FileMetaData& metaData) { return std::make_tuple(metaData.tileCount[0], metaData.tileCount[1], metaData.tileCount[2]); },
|
||||
"Tuple (lower-tile, upper-tile, root-tile) of active-tile counts.")
|
||||
.def_ro("codec", &io::FileMetaData::codec,
|
||||
"Codec used to compress this grid on disk.")
|
||||
.def_ro("blindDataCount", &io::FileMetaData::blindDataCount,
|
||||
"Number of blind-data channels attached to this grid.")
|
||||
.def_ro("version", &io::FileMetaData::version,
|
||||
"NanoVDB version stored in the file when this grid was written.");
|
||||
|
||||
nb::bind_vector<std::vector<io::FileMetaData>>(m, "FileMetaDataVector");
|
||||
nb::bind_vector<std::vector<io::FileMetaData>>(m, "FileMetaDataVector",
|
||||
"List of FileMetaData entries, one per grid in a .nvdb file.");
|
||||
|
||||
nb::class_<io::FileGridMetaData, io::FileMetaData>(m, "FileGridMetaData")
|
||||
.def_ro("gridName", &io::FileGridMetaData::gridName)
|
||||
.def("memUsage", &io::FileGridMetaData::memUsage);
|
||||
nb::class_<io::FileGridMetaData, io::FileMetaData>(m, "FileGridMetaData",
|
||||
"FileMetaData extended with the grid name. Returned by "
|
||||
"readGridMetaData() so callers can identify grids by name without "
|
||||
"materializing them.")
|
||||
.def_ro("gridName", &io::FileGridMetaData::gridName,
|
||||
"Grid name as a Python string.")
|
||||
.def("memUsage", &io::FileGridMetaData::memUsage,
|
||||
"Byte size of this metadata record in memory.");
|
||||
|
||||
nb::bind_vector<std::vector<io::FileGridMetaData>>(m, "FileGridMetaDataVector");
|
||||
nb::bind_vector<std::vector<io::FileGridMetaData>>(m, "FileGridMetaDataVector",
|
||||
"List of FileGridMetaData entries, one per grid in a .nvdb file.");
|
||||
}
|
||||
|
||||
template<typename BufferT> void defineReadWriteGrid(nb::module_& m)
|
||||
{
|
||||
m.def("hasGrid", nb::overload_cast<const std::string&, const std::string&>(&io::hasGrid), "fileName"_a, "gridName"_a);
|
||||
m.def("readGridMetaData", nb::overload_cast<const std::string&>(&io::readGridMetaData), "fileName"_a);
|
||||
m.def("hasGrid", nb::overload_cast<const std::string&, const std::string&>(&io::hasGrid), "fileName"_a, "gridName"_a,
|
||||
"Return True iff the .nvdb file at fileName contains a grid named gridName.");
|
||||
m.def("readGridMetaData", nb::overload_cast<const std::string&>(&io::readGridMetaData), "fileName"_a,
|
||||
"Return a FileGridMetaDataVector describing every grid stored in the .nvdb file.");
|
||||
}
|
||||
|
||||
template<typename BufferT> nb::list readGrids(const std::string& fileName, int verbose, const BufferT& buffer)
|
||||
@@ -91,21 +118,26 @@ void defineHostReadWriteGrid(nb::module_& m)
|
||||
"fileName"_a,
|
||||
"handle"_a,
|
||||
"codec"_a = io::Codec::NONE,
|
||||
"verbose"_a = 0);
|
||||
m.def("writeGrids", &writeGrids<BufferT>, "fileName"_a, "handles"_a, "codec"_a = io::Codec::NONE, "verbose"_a = 0);
|
||||
"verbose"_a = 0,
|
||||
"Write the grids in handle to the .nvdb file at fileName using the given codec.");
|
||||
m.def("writeGrids", &writeGrids<BufferT>, "fileName"_a, "handles"_a, "codec"_a = io::Codec::NONE, "verbose"_a = 0,
|
||||
"Write every GridHandle in the handles list to the .nvdb file at fileName.");
|
||||
m.def("readGrid",
|
||||
nb::overload_cast<const std::string&, int, int, const BufferT&>(&io::template readGrid<BufferT>),
|
||||
"fileName"_a,
|
||||
"n"_a = 0,
|
||||
"verbose"_a = 0,
|
||||
"buffer"_a = BufferT());
|
||||
"buffer"_a = BufferT(),
|
||||
"Read the n-th grid from the .nvdb file at fileName into a fresh GridHandle.");
|
||||
m.def("readGrid",
|
||||
nb::overload_cast<const std::string&, const std::string&, int, const BufferT&>(&io::template readGrid<BufferT>),
|
||||
"fileName"_a,
|
||||
"gridName"_a,
|
||||
"verbose"_a = 0,
|
||||
"buffer"_a = BufferT());
|
||||
m.def("readGrids", &readGrids<BufferT>, "fileName"_a, "verbose"_a = 0, "buffer"_a = BufferT());
|
||||
"buffer"_a = BufferT(),
|
||||
"Read the grid named gridName from the .nvdb file at fileName into a fresh GridHandle.");
|
||||
m.def("readGrids", &readGrids<BufferT>, "fileName"_a, "verbose"_a = 0, "buffer"_a = BufferT(),
|
||||
"Read every grid from the .nvdb file at fileName, returning a list of GridHandles.");
|
||||
}
|
||||
|
||||
#ifdef NANOVDB_USE_CUDA
|
||||
@@ -119,21 +151,26 @@ void defineDeviceReadWriteGrid(nb::module_& m)
|
||||
"fileName"_a,
|
||||
"handle"_a,
|
||||
"codec"_a = io::Codec::NONE,
|
||||
"verbose"_a = 0);
|
||||
m.def("deviceWriteGrids", &writeGrids<BufferT>, "fileName"_a, "handles"_a, "codec"_a = io::Codec::NONE, "verbose"_a = 0);
|
||||
"verbose"_a = 0,
|
||||
"Write the grids in a device-backed handle to the .nvdb file at fileName.");
|
||||
m.def("deviceWriteGrids", &writeGrids<BufferT>, "fileName"_a, "handles"_a, "codec"_a = io::Codec::NONE, "verbose"_a = 0,
|
||||
"Write every device-backed GridHandle in handles to the .nvdb file at fileName.");
|
||||
m.def("deviceReadGrid",
|
||||
nb::overload_cast<const std::string&, int, int, const BufferT&>(&io::template readGrid<BufferT>),
|
||||
"fileName"_a,
|
||||
"n"_a = 0,
|
||||
"verbose"_a = 0,
|
||||
"buffer"_a = BufferT());
|
||||
"buffer"_a = BufferT(),
|
||||
"Read the n-th grid from the .nvdb file at fileName into a fresh DeviceGridHandle.");
|
||||
m.def("deviceReadGrid",
|
||||
nb::overload_cast<const std::string&, const std::string&, int, const BufferT&>(&io::template readGrid<BufferT>),
|
||||
"fileName"_a,
|
||||
"gridName"_a,
|
||||
"verbose"_a = 0,
|
||||
"buffer"_a = BufferT());
|
||||
m.def("deviceReadGrids", &readGrids<BufferT>, "fileName"_a, "verbose"_a = 0, "buffer"_a = BufferT());
|
||||
"buffer"_a = BufferT(),
|
||||
"Read the grid named gridName from the .nvdb file at fileName into a fresh DeviceGridHandle.");
|
||||
m.def("deviceReadGrids", &readGrids<BufferT>, "fileName"_a, "verbose"_a = 0, "buffer"_a = BufferT(),
|
||||
"Read every grid from the .nvdb file at fileName into device-backed handles.");
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -141,16 +178,19 @@ void defineDeviceReadWriteGrid(nb::module_& m)
|
||||
|
||||
void defineIOModule(nb::module_& m)
|
||||
{
|
||||
nb::enum_<io::Codec>(m, "Codec")
|
||||
nb::enum_<io::Codec>(m, "Codec",
|
||||
"Compression codec selector used when writing a .nvdb file. NONE "
|
||||
"writes raw bytes; ZIP uses zlib; BLOSC uses the blosc codec when "
|
||||
"compiled in.")
|
||||
.value("NONE", io::Codec::NONE)
|
||||
.value("ZIP", io::Codec::ZIP)
|
||||
.value("BLOSC", io::Codec::BLOSC)
|
||||
.export_values();
|
||||
// .def("__repr__", [](const io::Codec& codec) {
|
||||
// char str[strlen<io::Codec>()];
|
||||
// toStr(str, codec);
|
||||
// return std::string(str);
|
||||
// });
|
||||
.export_values()
|
||||
.def("__repr__", [](const io::Codec& codec) {
|
||||
char str[strlen<io::Codec>()];
|
||||
toStr(str, codec);
|
||||
return std::string(str);
|
||||
});
|
||||
|
||||
defineFileGridMetaData(m);
|
||||
defineHostReadWriteGrid(m);
|
||||
|
||||
+325
-187
@@ -11,7 +11,6 @@
|
||||
#include <nanobind/stl/bind_vector.h>
|
||||
|
||||
#include "PySampleFromVoxels.h"
|
||||
#include "cuda/PySampleFromVoxels.h"
|
||||
|
||||
namespace nb = nanobind;
|
||||
using namespace nb::literals;
|
||||
@@ -26,18 +25,27 @@ void defineCoord(nb::module_& m)
|
||||
using ValueType = math::Coord::ValueType;
|
||||
|
||||
nb::class_<math::Coord>(m, "Coord", "Signed (i, j, k) 32-bit integer coordinate class, similar to openvdb::math::Coord")
|
||||
.def(nb::init<>())
|
||||
.def(nb::init<ValueType>(), "n"_a)
|
||||
.def(nb::init<ValueType, ValueType, ValueType>(), "i"_a, "j"_a, "k"_a)
|
||||
.def(nb::init<>(),
|
||||
"Construct (0, 0, 0).")
|
||||
.def(nb::init<ValueType>(), "n"_a,
|
||||
"Construct (n, n, n).")
|
||||
.def(nb::init<ValueType, ValueType, ValueType>(), "i"_a, "j"_a, "k"_a,
|
||||
"Construct (i, j, k).")
|
||||
.def_prop_rw(
|
||||
"x", [](const math::Coord& ijk) { return ijk.x(); }, [](math::Coord& ijk, int32_t i) { ijk.x() = i; })
|
||||
"x", [](const math::Coord& ijk) { return ijk.x(); }, [](math::Coord& ijk, int32_t i) { ijk.x() = i; },
|
||||
"First component of the (i, j, k) triple.")
|
||||
.def_prop_rw(
|
||||
"y", [](const math::Coord& ijk) { return ijk.y(); }, [](math::Coord& ijk, int32_t j) { ijk.y() = j; })
|
||||
"y", [](const math::Coord& ijk) { return ijk.y(); }, [](math::Coord& ijk, int32_t j) { ijk.y() = j; },
|
||||
"Second component of the (i, j, k) triple.")
|
||||
.def_prop_rw(
|
||||
"z", [](const math::Coord& ijk) { return ijk.z(); }, [](math::Coord& ijk, int32_t k) { ijk.z() = k; })
|
||||
.def_static("max", &math::Coord::max)
|
||||
.def_static("min", &math::Coord::min)
|
||||
.def_static("memUsage", &math::Coord::memUsage)
|
||||
"z", [](const math::Coord& ijk) { return ijk.z(); }, [](math::Coord& ijk, int32_t k) { ijk.z() = k; },
|
||||
"Third component of the (i, j, k) triple.")
|
||||
.def_static("max", &math::Coord::max,
|
||||
"Largest representable Coord (INT32_MAX in every component).")
|
||||
.def_static("min", &math::Coord::min,
|
||||
"Smallest representable Coord (INT32_MIN in every component).")
|
||||
.def_static("memUsage", &math::Coord::memUsage,
|
||||
"Byte size of a Coord instance.")
|
||||
.def(
|
||||
"__getitem__",
|
||||
[](const math::Coord& ijk, size_t i) {
|
||||
@@ -46,7 +54,8 @@ void defineCoord(nb::module_& m)
|
||||
}
|
||||
return ijk[static_cast<math::Coord::IndexType>(i)];
|
||||
},
|
||||
"i"_a)
|
||||
"i"_a,
|
||||
"Read the i-th component (0=x, 1=y, 2=z).")
|
||||
.def(
|
||||
"__setitem__",
|
||||
[](math::Coord& ijk, size_t i, ValueType value) {
|
||||
@@ -56,41 +65,69 @@ void defineCoord(nb::module_& m)
|
||||
ijk[static_cast<math::Coord::IndexType>(i)] = value;
|
||||
},
|
||||
"i"_a,
|
||||
"value"_a)
|
||||
"value"_a,
|
||||
"Write the i-th component (0=x, 1=y, 2=z).")
|
||||
.def(
|
||||
"__and__", [](const math::Coord& a, math::Coord::IndexType b) { return a & b; }, nb::is_operator(), "n"_a)
|
||||
"__and__", [](const math::Coord& a, math::Coord::IndexType b) { return a & b; }, nb::is_operator(), "n"_a,
|
||||
"Component-wise bitwise AND with the scalar n.")
|
||||
.def(
|
||||
"__lshift__", [](const math::Coord& a, math::Coord::IndexType b) { return a << b; }, nb::is_operator(), "n"_a)
|
||||
"__lshift__", [](const math::Coord& a, math::Coord::IndexType b) { return a << b; }, nb::is_operator(), "n"_a,
|
||||
"Component-wise left shift by n bits.")
|
||||
.def(
|
||||
"__rshift__", [](const math::Coord& a, math::Coord::IndexType b) { return a >> b; }, nb::is_operator(), "n"_a)
|
||||
.def(nb::self < nb::self, "rhs"_a)
|
||||
.def(nb::self == nb::self, "rhs"_a)
|
||||
.def(nb::self != nb::self, "rhs"_a)
|
||||
"__rshift__", [](const math::Coord& a, math::Coord::IndexType b) { return a >> b; }, nb::is_operator(), "n"_a,
|
||||
"Component-wise right shift by n bits.")
|
||||
.def(nb::self < nb::self, "rhs"_a,
|
||||
"Lexicographic less-than comparison.")
|
||||
.def(nb::self == nb::self, "rhs"_a,
|
||||
"Equality of all three components.")
|
||||
.def(nb::self != nb::self, "rhs"_a,
|
||||
"Inequality of any one component.")
|
||||
.def(
|
||||
"__iand__", [](math::Coord& a, int b) { return a &= b; }, nb::is_operator(), "n"_a)
|
||||
"__iand__", [](math::Coord& a, int b) { return a &= b; }, nb::is_operator(), "n"_a,
|
||||
"In-place component-wise bitwise AND with the scalar n.")
|
||||
.def(
|
||||
"__ilshift__", [](math::Coord& a, uint32_t b) { return a <<= b; }, nb::is_operator(), "n"_a)
|
||||
"__ilshift__", [](math::Coord& a, uint32_t b) { return a <<= b; }, nb::is_operator(), "n"_a,
|
||||
"In-place component-wise left shift by n bits.")
|
||||
.def(
|
||||
"__irshift__", [](math::Coord& a, uint32_t b) { return a >>= b; }, nb::is_operator(), "n"_a)
|
||||
"__irshift__", [](math::Coord& a, uint32_t b) { return a >>= b; }, nb::is_operator(), "n"_a,
|
||||
"In-place component-wise right shift by n bits.")
|
||||
.def(
|
||||
"__iadd__", [](math::Coord& a, int b) { return a += b; }, nb::is_operator(), "n"_a)
|
||||
.def(nb::self + nb::self, "rhs"_a)
|
||||
.def(nb::self - nb::self, "rhs"_a)
|
||||
.def(-nb::self)
|
||||
.def(nb::self += nb::self, "rhs"_a)
|
||||
.def(nb::self -= nb::self, "rhs"_a)
|
||||
.def("minComponent", &math::Coord::minComponent, "other"_a)
|
||||
.def("maxComponent", &math::Coord::maxComponent, "other"_a)
|
||||
.def("offsetBy", nb::overload_cast<ValueType, ValueType, ValueType>(&math::Coord::offsetBy, nb::const_), "dx"_a, "dy"_a, "dz"_a)
|
||||
.def("offsetBy", nb::overload_cast<ValueType>(&math::Coord::offsetBy, nb::const_), "n"_a)
|
||||
.def_static("lessThan", &math::Coord::lessThan, "a"_a, "b"_a)
|
||||
.def_static("Floor", &math::Coord::template Floor<math::Vec3<float>>, "xyz"_a)
|
||||
.def_static("Floor", &math::Coord::template Floor<math::Vec3<double>>, "xyz"_a)
|
||||
.def("hash", &math::Coord::template hash<12>)
|
||||
.def("octant", &math::Coord::octant)
|
||||
.def("asVec3s", &math::Coord::asVec3s)
|
||||
.def("asVec3d", &math::Coord::asVec3d)
|
||||
.def("round", &math::Coord::round)
|
||||
"__iadd__", [](math::Coord& a, int b) { return a += b; }, nb::is_operator(), "n"_a,
|
||||
"In-place add the scalar n to every component.")
|
||||
.def(nb::self + nb::self, "rhs"_a,
|
||||
"Component-wise addition.")
|
||||
.def(nb::self - nb::self, "rhs"_a,
|
||||
"Component-wise subtraction.")
|
||||
.def(-nb::self,
|
||||
"Negate every component.")
|
||||
.def(nb::self += nb::self, "rhs"_a,
|
||||
"In-place component-wise addition.")
|
||||
.def(nb::self -= nb::self, "rhs"_a,
|
||||
"In-place component-wise subtraction.")
|
||||
.def("minComponent", &math::Coord::minComponent, "other"_a,
|
||||
"Component-wise minimum with other. See nanovdb::math::Coord::minComponent in NanoVDB.h.")
|
||||
.def("maxComponent", &math::Coord::maxComponent, "other"_a,
|
||||
"Component-wise maximum with other. See nanovdb::math::Coord::maxComponent in NanoVDB.h.")
|
||||
.def("offsetBy", nb::overload_cast<ValueType, ValueType, ValueType>(&math::Coord::offsetBy, nb::const_), "dx"_a, "dy"_a, "dz"_a,
|
||||
"Return a new Coord offset by (dx, dy, dz). See nanovdb::math::Coord::offsetBy in NanoVDB.h.")
|
||||
.def("offsetBy", nb::overload_cast<ValueType>(&math::Coord::offsetBy, nb::const_), "n"_a,
|
||||
"Return a new Coord offset by n in every component.")
|
||||
.def_static("lessThan", &math::Coord::lessThan, "a"_a, "b"_a,
|
||||
"Component-wise a < b returning a Coord of 0 / 1 flags.")
|
||||
.def_static("Floor", &math::Coord::template Floor<math::Vec3<float>>, "xyz"_a,
|
||||
"Floor each component of a Vec3f to produce an integer Coord.")
|
||||
.def_static("Floor", &math::Coord::template Floor<math::Vec3<double>>, "xyz"_a,
|
||||
"Floor each component of a Vec3d to produce an integer Coord.")
|
||||
.def("hash", &math::Coord::template hash<12>,
|
||||
"Spatial hash of this coordinate suited for hashed root-table lookups.")
|
||||
.def("octant", &math::Coord::octant,
|
||||
"Return the 0..7 octant index of this coordinate's sign bits.")
|
||||
.def("asVec3s", &math::Coord::asVec3s,
|
||||
"Convert to a Vec3f (float) with no scaling.")
|
||||
.def("asVec3d", &math::Coord::asVec3d,
|
||||
"Convert to a Vec3d (double) with no scaling.")
|
||||
.def("round", &math::Coord::round,
|
||||
"Component-wise round; for an integer Coord this is the identity.")
|
||||
.def("__repr__", [](const math::Coord& ijk) {
|
||||
std::stringstream ostr;
|
||||
ostr << ijk;
|
||||
@@ -101,13 +138,20 @@ void defineCoord(nb::module_& m)
|
||||
template<typename T> void defineVec3(nb::module_& m, const char* name, const char* doc)
|
||||
{
|
||||
nb::class_<math::Vec3<T>>(m, name, doc)
|
||||
.def(nb::init<>())
|
||||
.def(nb::init<T>(), "x"_a)
|
||||
.def(nb::init<T, T, T>(), "x"_a, "y"_a, "z"_a)
|
||||
.def(nb::init<math::Vec3<T>>(), "v"_a)
|
||||
.def(nb::init<math::Coord>(), "ijk"_a)
|
||||
.def(nb::self == nb::self, "rhs"_a)
|
||||
.def(nb::self != nb::self, "rhs"_a)
|
||||
.def(nb::init<>(),
|
||||
"Construct a zero-initialized vector.")
|
||||
.def(nb::init<T>(), "x"_a,
|
||||
"Construct (x, x, x).")
|
||||
.def(nb::init<T, T, T>(), "x"_a, "y"_a, "z"_a,
|
||||
"Construct (x, y, z).")
|
||||
.def(nb::init<math::Vec3<T>>(), "v"_a,
|
||||
"Copy-construct from another Vec3.")
|
||||
.def(nb::init<math::Coord>(), "ijk"_a,
|
||||
"Construct from an integer Coord, casting each component.")
|
||||
.def(nb::self == nb::self, "rhs"_a,
|
||||
"Component-wise equality.")
|
||||
.def(nb::self != nb::self, "rhs"_a,
|
||||
"Component-wise inequality.")
|
||||
.def(
|
||||
"__getitem__",
|
||||
[](const math::Vec3<T>& v, size_t i) {
|
||||
@@ -116,7 +160,8 @@ template<typename T> void defineVec3(nb::module_& m, const char* name, const cha
|
||||
}
|
||||
return v[static_cast<int>(i)];
|
||||
},
|
||||
"i"_a)
|
||||
"i"_a,
|
||||
"Read the i-th component (0=x, 1=y, 2=z).")
|
||||
.def(
|
||||
"__setitem__",
|
||||
[](math::Vec3<T>& v, size_t i, T value) {
|
||||
@@ -126,38 +171,68 @@ template<typename T> void defineVec3(nb::module_& m, const char* name, const cha
|
||||
v[static_cast<int>(i)] = value;
|
||||
},
|
||||
"i"_a,
|
||||
"value"_a)
|
||||
.def("dot", &math::Vec3<T>::template dot<math::Vec3<T>>, "v"_a)
|
||||
.def("cross", &math::Vec3<T>::template cross<math::Vec3<T>>, "v"_a)
|
||||
.def("lengthSqr", &math::Vec3<T>::lengthSqr)
|
||||
.def("length", &math::Vec3<T>::length)
|
||||
.def(-nb::self)
|
||||
.def(nb::self * nb::self, "v"_a)
|
||||
.def(nb::self / nb::self, "v"_a)
|
||||
.def(nb::self + nb::self, "v"_a)
|
||||
.def(nb::self - nb::self, "v"_a)
|
||||
.def(nb::self + math::Coord(), "ijk"_a)
|
||||
.def(nb::self - math::Coord(), "ijk"_a)
|
||||
.def(nb::self * T(), "s"_a)
|
||||
.def(nb::self / T(), "s"_a)
|
||||
.def(nb::self += nb::self, "v"_a)
|
||||
.def(nb::self += math::Coord(), "ijk"_a)
|
||||
.def(nb::self -= nb::self, "v"_a)
|
||||
.def(nb::self -= math::Coord(), "ijk"_a)
|
||||
.def(nb::self *= T(), "s"_a)
|
||||
.def(nb::self /= T(), "s"_a)
|
||||
.def("normalize", &math::Vec3<T>::normalize)
|
||||
.def("minComponent", &math::Vec3<T>::minComponent, "other"_a)
|
||||
.def("maxComponent", &math::Vec3<T>::maxComponent, "other"_a)
|
||||
.def("min", &math::Vec3<T>::min)
|
||||
.def("max", &math::Vec3<T>::max)
|
||||
.def("floor", &math::Vec3<T>::floor)
|
||||
.def("ceil", &math::Vec3<T>::ceil)
|
||||
.def("round", &math::Vec3<T>::round)
|
||||
"value"_a,
|
||||
"Write the i-th component (0=x, 1=y, 2=z).")
|
||||
.def("dot", &math::Vec3<T>::template dot<math::Vec3<T>>, "v"_a,
|
||||
"Dot product with another vector.")
|
||||
.def("cross", &math::Vec3<T>::template cross<math::Vec3<T>>, "v"_a,
|
||||
"Cross product with another vector.")
|
||||
.def("lengthSqr", &math::Vec3<T>::lengthSqr,
|
||||
"Squared Euclidean length (cheaper than length()).")
|
||||
.def("length", &math::Vec3<T>::length,
|
||||
"Euclidean length of this vector.")
|
||||
.def(-nb::self,
|
||||
"Negate every component.")
|
||||
.def(nb::self * nb::self, "v"_a,
|
||||
"Component-wise multiplication.")
|
||||
.def(nb::self / nb::self, "v"_a,
|
||||
"Component-wise division.")
|
||||
.def(nb::self + nb::self, "v"_a,
|
||||
"Component-wise addition.")
|
||||
.def(nb::self - nb::self, "v"_a,
|
||||
"Component-wise subtraction.")
|
||||
.def(nb::self + math::Coord(), "ijk"_a,
|
||||
"Add an integer Coord component-wise.")
|
||||
.def(nb::self - math::Coord(), "ijk"_a,
|
||||
"Subtract an integer Coord component-wise.")
|
||||
.def(nb::self * T(), "s"_a,
|
||||
"Multiply every component by the scalar s.")
|
||||
.def(nb::self / T(), "s"_a,
|
||||
"Divide every component by the scalar s.")
|
||||
.def(nb::self += nb::self, "v"_a,
|
||||
"In-place component-wise addition.")
|
||||
.def(nb::self += math::Coord(), "ijk"_a,
|
||||
"In-place add an integer Coord.")
|
||||
.def(nb::self -= nb::self, "v"_a,
|
||||
"In-place component-wise subtraction.")
|
||||
.def(nb::self -= math::Coord(), "ijk"_a,
|
||||
"In-place subtract an integer Coord.")
|
||||
.def(nb::self *= T(), "s"_a,
|
||||
"In-place scalar multiply.")
|
||||
.def(nb::self /= T(), "s"_a,
|
||||
"In-place scalar divide.")
|
||||
.def("normalize", &math::Vec3<T>::normalize,
|
||||
"Scale this vector to unit length in place.")
|
||||
.def("minComponent", &math::Vec3<T>::minComponent, "other"_a,
|
||||
"Component-wise minimum with other.")
|
||||
.def("maxComponent", &math::Vec3<T>::maxComponent, "other"_a,
|
||||
"Component-wise maximum with other.")
|
||||
.def("min", &math::Vec3<T>::min,
|
||||
"Smallest single component of this vector.")
|
||||
.def("max", &math::Vec3<T>::max,
|
||||
"Largest single component of this vector.")
|
||||
.def("floor", &math::Vec3<T>::floor,
|
||||
"Component-wise floor.")
|
||||
.def("ceil", &math::Vec3<T>::ceil,
|
||||
"Component-wise ceiling.")
|
||||
.def("round", &math::Vec3<T>::round,
|
||||
"Component-wise round.")
|
||||
.def(
|
||||
"__mul__", [](const T& a, math::Vec3<T> b) { return a * b; }, nb::is_operator(), "b"_a)
|
||||
"__mul__", [](const T& a, math::Vec3<T> b) { return a * b; }, nb::is_operator(), "b"_a,
|
||||
"Right-multiply: scalar * Vec3.")
|
||||
.def(
|
||||
"__truediv__", [](const T& a, math::Vec3<T> b) { return a / b; }, nb::is_operator(), "b"_a)
|
||||
"__truediv__", [](const T& a, math::Vec3<T> b) { return a / b; }, nb::is_operator(), "b"_a,
|
||||
"Right-divide: scalar / Vec3, component-wise.")
|
||||
.def("__repr__", [](const math::Vec3<T>& v) {
|
||||
std::stringstream ostr;
|
||||
ostr << v;
|
||||
@@ -168,12 +243,18 @@ template<typename T> void defineVec3(nb::module_& m, const char* name, const cha
|
||||
template<typename T> void defineVec4(nb::module_& m, const char* name, const char* doc)
|
||||
{
|
||||
nb::class_<math::Vec4<T>>(m, name, doc)
|
||||
.def(nb::init<>())
|
||||
.def(nb::init<T>(), "x"_a)
|
||||
.def(nb::init<T, T, T, T>(), "x"_a, "y"_a, "z"_a, "w"_a)
|
||||
.def(nb::init<math::Vec4<T>>(), "v"_a)
|
||||
.def(nb::self == nb::self, "rhs"_a)
|
||||
.def(nb::self != nb::self, "rhs"_a)
|
||||
.def(nb::init<>(),
|
||||
"Construct a zero-initialized vector.")
|
||||
.def(nb::init<T>(), "x"_a,
|
||||
"Construct (x, x, x, x).")
|
||||
.def(nb::init<T, T, T, T>(), "x"_a, "y"_a, "z"_a, "w"_a,
|
||||
"Construct (x, y, z, w).")
|
||||
.def(nb::init<math::Vec4<T>>(), "v"_a,
|
||||
"Copy-construct from another Vec4.")
|
||||
.def(nb::self == nb::self, "rhs"_a,
|
||||
"Component-wise equality.")
|
||||
.def(nb::self != nb::self, "rhs"_a,
|
||||
"Component-wise inequality.")
|
||||
.def(
|
||||
"__getitem__",
|
||||
[](const math::Vec4<T>& v, size_t i) {
|
||||
@@ -182,7 +263,8 @@ template<typename T> void defineVec4(nb::module_& m, const char* name, const cha
|
||||
}
|
||||
return v[static_cast<int>(i)];
|
||||
},
|
||||
"i"_a)
|
||||
"i"_a,
|
||||
"Read the i-th component (0=x, 1=y, 2=z, 3=w).")
|
||||
.def(
|
||||
"__setitem__",
|
||||
[](math::Vec4<T>& v, size_t i, T value) {
|
||||
@@ -192,28 +274,48 @@ template<typename T> void defineVec4(nb::module_& m, const char* name, const cha
|
||||
v[static_cast<int>(i)] = value;
|
||||
},
|
||||
"i"_a,
|
||||
"value"_a)
|
||||
.def("dot", &math::Vec4<T>::template dot<math::Vec4<T>>, "v"_a)
|
||||
.def("lengthSqr", &math::Vec4<T>::lengthSqr)
|
||||
.def("length", &math::Vec4<T>::length)
|
||||
.def(-nb::self)
|
||||
.def(nb::self * nb::self, "v"_a)
|
||||
.def(nb::self / nb::self, "v"_a)
|
||||
.def(nb::self + nb::self, "v"_a)
|
||||
.def(nb::self - nb::self, "v"_a)
|
||||
.def(nb::self * T(), "s"_a)
|
||||
.def(nb::self / T(), "s"_a)
|
||||
.def(nb::self += nb::self, "v"_a)
|
||||
.def(nb::self -= nb::self, "v"_a)
|
||||
.def(nb::self *= T(), "s"_a)
|
||||
.def(nb::self /= T(), "s"_a)
|
||||
.def("normalize", &math::Vec4<T>::normalize)
|
||||
.def("minComponent", &math::Vec4<T>::minComponent, "other"_a)
|
||||
.def("maxComponent", &math::Vec4<T>::maxComponent, "other"_a)
|
||||
"value"_a,
|
||||
"Write the i-th component (0=x, 1=y, 2=z, 3=w).")
|
||||
.def("dot", &math::Vec4<T>::template dot<math::Vec4<T>>, "v"_a,
|
||||
"Dot product with another vector.")
|
||||
.def("lengthSqr", &math::Vec4<T>::lengthSqr,
|
||||
"Squared Euclidean length (cheaper than length()).")
|
||||
.def("length", &math::Vec4<T>::length,
|
||||
"Euclidean length of this vector.")
|
||||
.def(-nb::self,
|
||||
"Negate every component.")
|
||||
.def(nb::self * nb::self, "v"_a,
|
||||
"Component-wise multiplication.")
|
||||
.def(nb::self / nb::self, "v"_a,
|
||||
"Component-wise division.")
|
||||
.def(nb::self + nb::self, "v"_a,
|
||||
"Component-wise addition.")
|
||||
.def(nb::self - nb::self, "v"_a,
|
||||
"Component-wise subtraction.")
|
||||
.def(nb::self * T(), "s"_a,
|
||||
"Multiply every component by the scalar s.")
|
||||
.def(nb::self / T(), "s"_a,
|
||||
"Divide every component by the scalar s.")
|
||||
.def(nb::self += nb::self, "v"_a,
|
||||
"In-place component-wise addition.")
|
||||
.def(nb::self -= nb::self, "v"_a,
|
||||
"In-place component-wise subtraction.")
|
||||
.def(nb::self *= T(), "s"_a,
|
||||
"In-place scalar multiply.")
|
||||
.def(nb::self /= T(), "s"_a,
|
||||
"In-place scalar divide.")
|
||||
.def("normalize", &math::Vec4<T>::normalize,
|
||||
"Scale this vector to unit length in place.")
|
||||
.def("minComponent", &math::Vec4<T>::minComponent, "other"_a,
|
||||
"Component-wise minimum with other.")
|
||||
.def("maxComponent", &math::Vec4<T>::maxComponent, "other"_a,
|
||||
"Component-wise maximum with other.")
|
||||
.def(
|
||||
"__mul__", [](const T& a, math::Vec4<T> b) { return a * b; }, nb::is_operator(), "b"_a)
|
||||
"__mul__", [](const T& a, math::Vec4<T> b) { return a * b; }, nb::is_operator(), "b"_a,
|
||||
"Right-multiply: scalar * Vec4.")
|
||||
.def(
|
||||
"__truediv__", [](const T& a, math::Vec4<T> b) { return a / b; }, nb::is_operator(), "b"_a)
|
||||
"__truediv__", [](const T& a, math::Vec4<T> b) { return a / b; }, nb::is_operator(), "b"_a,
|
||||
"Right-divide: scalar / Vec4, component-wise.")
|
||||
.def("__repr__", [](const math::Vec4<T>& v) {
|
||||
std::stringstream ostr;
|
||||
ostr << v;
|
||||
@@ -226,18 +328,30 @@ void defineRgba8(nb::module_& m)
|
||||
using ValueType = math::Rgba8::ValueType;
|
||||
|
||||
nb::class_<math::Rgba8>(m, "Rgba8", "8-bit red, green, blue, alpha packed into 32 bit unsigned int")
|
||||
.def(nb::init<>())
|
||||
.def(nb::init<const math::Rgba8&>(), "other"_a)
|
||||
.def(nb::init<uint8_t, uint8_t, uint8_t, uint8_t>(), "r"_a, "g"_a, "b"_a, "a"_a = 255)
|
||||
.def(nb::init<uint8_t>(), "v"_a)
|
||||
.def(nb::init<float, float, float, float>(), "r"_a, "g"_a, "b"_a, "a"_a = 1.0)
|
||||
.def(nb::init<Vec3f>(), "rgb"_a)
|
||||
.def(nb::init<Vec4f>(), "rgba"_a)
|
||||
.def(nb::self < nb::self, "rhs"_a)
|
||||
.def(nb::self == nb::self, "rhs"_a)
|
||||
.def("lengthSqr", &math::Rgba8::lengthSqr)
|
||||
.def("length", &math::Rgba8::length)
|
||||
.def("asFloat", &math::Rgba8::asFloat, "n"_a)
|
||||
.def(nb::init<>(),
|
||||
"Construct a fully transparent black Rgba8 (all components 0).")
|
||||
.def(nb::init<const math::Rgba8&>(), "other"_a,
|
||||
"Copy-construct from another Rgba8.")
|
||||
.def(nb::init<uint8_t, uint8_t, uint8_t, uint8_t>(), "r"_a, "g"_a, "b"_a, "a"_a = 255,
|
||||
"Construct from four 0..255 uint8 channels; a defaults to fully opaque.")
|
||||
.def(nb::init<uint8_t>(), "v"_a,
|
||||
"Construct a gray Rgba8 with every channel set to v.")
|
||||
.def(nb::init<float, float, float, float>(), "r"_a, "g"_a, "b"_a, "a"_a = 1.0,
|
||||
"Construct from four 0..1 floats, clamped and quantized to uint8.")
|
||||
.def(nb::init<Vec3f>(), "rgb"_a,
|
||||
"Construct from an RGB float triple; alpha defaults to opaque.")
|
||||
.def(nb::init<Vec4f>(), "rgba"_a,
|
||||
"Construct from an RGBA float quadruple.")
|
||||
.def(nb::self < nb::self, "rhs"_a,
|
||||
"Less-than comparison on the packed uint32 representation.")
|
||||
.def(nb::self == nb::self, "rhs"_a,
|
||||
"Equality on the packed uint32 representation.")
|
||||
.def("lengthSqr", &math::Rgba8::lengthSqr,
|
||||
"Squared length over (r, g, b, a) as integers.")
|
||||
.def("length", &math::Rgba8::length,
|
||||
"Euclidean length over (r, g, b, a) as floats.")
|
||||
.def("asFloat", &math::Rgba8::asFloat, "n"_a,
|
||||
"Return the n-th channel as a 0..1 float.")
|
||||
.def(
|
||||
"__getitem__",
|
||||
[](const math::Rgba8& rgba, size_t i) {
|
||||
@@ -246,7 +360,8 @@ void defineRgba8(nb::module_& m)
|
||||
}
|
||||
return rgba[static_cast<int>(i)];
|
||||
},
|
||||
"i"_a)
|
||||
"i"_a,
|
||||
"Read the i-th channel as a uint8 (0=r, 1=g, 2=b, 3=a).")
|
||||
.def(
|
||||
"__setitem__",
|
||||
[](math::Rgba8& rgba, size_t i, ValueType value) {
|
||||
@@ -256,26 +371,38 @@ void defineRgba8(nb::module_& m)
|
||||
rgba[static_cast<int>(i)] = value;
|
||||
},
|
||||
"i"_a,
|
||||
"value"_a)
|
||||
"value"_a,
|
||||
"Write the i-th channel as a uint8 (0=r, 1=g, 2=b, 3=a).")
|
||||
.def_prop_rw(
|
||||
"packed", [](const math::Rgba8& rgba) { return rgba.packed(); }, [](math::Rgba8& rgba, uint32_t packed) { rgba.packed() = packed; })
|
||||
"packed", [](const math::Rgba8& rgba) { return rgba.packed(); }, [](math::Rgba8& rgba, uint32_t packed) { rgba.packed() = packed; },
|
||||
"The raw 32-bit packed RGBA representation.")
|
||||
.def_prop_rw(
|
||||
"r", [](const math::Rgba8& rgba) { return rgba.r(); }, [](math::Rgba8& rgba, uint8_t r) { rgba.r() = r; })
|
||||
"r", [](const math::Rgba8& rgba) { return rgba.r(); }, [](math::Rgba8& rgba, uint8_t r) { rgba.r() = r; },
|
||||
"Red channel as a uint8 0..255.")
|
||||
.def_prop_rw(
|
||||
"g", [](const math::Rgba8& rgba) { return rgba.g(); }, [](math::Rgba8& rgba, uint8_t g) { rgba.g() = g; })
|
||||
"g", [](const math::Rgba8& rgba) { return rgba.g(); }, [](math::Rgba8& rgba, uint8_t g) { rgba.g() = g; },
|
||||
"Green channel as a uint8 0..255.")
|
||||
.def_prop_rw(
|
||||
"b", [](const math::Rgba8& rgba) { return rgba.b(); }, [](math::Rgba8& rgba, uint8_t b) { rgba.b() = b; })
|
||||
"b", [](const math::Rgba8& rgba) { return rgba.b(); }, [](math::Rgba8& rgba, uint8_t b) { rgba.b() = b; },
|
||||
"Blue channel as a uint8 0..255.")
|
||||
.def_prop_rw(
|
||||
"a", [](const math::Rgba8& rgba) { return rgba.a(); }, [](math::Rgba8& rgba, uint8_t a) { rgba.a() = a; })
|
||||
.def("asVec3f", [](const math::Rgba8& rgba) { return Vec3f(rgba); })
|
||||
.def("asVec4f", [](const math::Rgba8& rgba) { return Vec4f(rgba); });
|
||||
"a", [](const math::Rgba8& rgba) { return rgba.a(); }, [](math::Rgba8& rgba, uint8_t a) { rgba.a() = a; },
|
||||
"Alpha channel as a uint8 0..255.")
|
||||
.def("asVec3f", [](const math::Rgba8& rgba) { return Vec3f(rgba); },
|
||||
"Convert RGB channels to a Vec3f of 0..1 floats (alpha dropped).")
|
||||
.def("asVec4f", [](const math::Rgba8& rgba) { return Vec4f(rgba); },
|
||||
"Convert RGBA channels to a Vec4f of 0..1 floats.");
|
||||
}
|
||||
|
||||
template<typename Vec3T> void defineBaseBBox(nb::module_& m, const char* name)
|
||||
{
|
||||
nb::class_<math::BaseBBox<Vec3T>>(m, name)
|
||||
.def(nb::self == nb::self, "rhs"_a)
|
||||
.def(nb::self != nb::self, "rhs"_a)
|
||||
nb::class_<math::BaseBBox<Vec3T>>(m, name,
|
||||
"Axis-aligned bounding-box base class. Stores a min / max corner; "
|
||||
"concrete subclasses add the open-interval vs closed-interval semantics.")
|
||||
.def(nb::self == nb::self, "rhs"_a,
|
||||
"Equality of both min and max corners.")
|
||||
.def(nb::self != nb::self, "rhs"_a,
|
||||
"Inequality of either min or max corner.")
|
||||
.def(
|
||||
"__getitem__",
|
||||
[](const math::BaseBBox<Vec3T>& bbox, size_t i) {
|
||||
@@ -284,7 +411,8 @@ template<typename Vec3T> void defineBaseBBox(nb::module_& m, const char* name)
|
||||
}
|
||||
return bbox[static_cast<int>(i)];
|
||||
},
|
||||
"i"_a)
|
||||
"i"_a,
|
||||
"Read corner 0 (min) or corner 1 (max).")
|
||||
.def(
|
||||
"__setitem__",
|
||||
[](math::BaseBBox<Vec3T>& bbox, size_t i, const Vec3T& value) {
|
||||
@@ -294,29 +422,45 @@ template<typename Vec3T> void defineBaseBBox(nb::module_& m, const char* name)
|
||||
bbox[static_cast<int>(i)] = value;
|
||||
},
|
||||
"i"_a,
|
||||
"value"_a)
|
||||
"value"_a,
|
||||
"Write corner 0 (min) or corner 1 (max).")
|
||||
.def_prop_rw(
|
||||
"min", [](const math::BaseBBox<Vec3T>& bbox) { return bbox.min(); }, [](math::BaseBBox<Vec3T>& bbox, const Vec3T& min) { bbox.min() = min; })
|
||||
"min", [](const math::BaseBBox<Vec3T>& bbox) { return bbox.min(); }, [](math::BaseBBox<Vec3T>& bbox, const Vec3T& min) { bbox.min() = min; },
|
||||
"Minimum corner of the bounding box.")
|
||||
.def_prop_rw(
|
||||
"max", [](const math::BaseBBox<Vec3T>& bbox) { return bbox.max(); }, [](math::BaseBBox<Vec3T>& bbox, const Vec3T& max) { bbox.max() = max; })
|
||||
.def("translate", &math::BaseBBox<Vec3T>::translate, "xyz"_a)
|
||||
.def("expand", nb::overload_cast<const Vec3T&>(&math::BaseBBox<Vec3T>::expand), "xyz"_a)
|
||||
.def("expand", nb::overload_cast<const math::BaseBBox<Vec3T>&>(&math::BaseBBox<Vec3T>::expand), "bbox"_a)
|
||||
.def("intersect", &math::BaseBBox<Vec3T>::intersect, "bbox"_a)
|
||||
.def("isInside", &math::BaseBBox<Vec3T>::isInside, "xyz"_a);
|
||||
"max", [](const math::BaseBBox<Vec3T>& bbox) { return bbox.max(); }, [](math::BaseBBox<Vec3T>& bbox, const Vec3T& max) { bbox.max() = max; },
|
||||
"Maximum corner of the bounding box.")
|
||||
.def("translate", &math::BaseBBox<Vec3T>::translate, "xyz"_a,
|
||||
"Translate this bounding box by xyz in place.")
|
||||
.def("expand", nb::overload_cast<const Vec3T&>(&math::BaseBBox<Vec3T>::expand), "xyz"_a,
|
||||
"Grow this bounding box to include the point xyz.")
|
||||
.def("expand", nb::overload_cast<const math::BaseBBox<Vec3T>&>(&math::BaseBBox<Vec3T>::expand), "bbox"_a,
|
||||
"Grow this bounding box to include another bbox in its entirety.")
|
||||
.def("intersect", &math::BaseBBox<Vec3T>::intersect, "bbox"_a,
|
||||
"Shrink this bounding box to the intersection with bbox.")
|
||||
.def("isInside", &math::BaseBBox<Vec3T>::isInside, "xyz"_a,
|
||||
"True iff xyz lies inside this bounding box.");
|
||||
}
|
||||
|
||||
template<typename Vec3T> void defineBBoxFloatingPoint(nb::module_& m, const char* name, const char* doc)
|
||||
{
|
||||
nb::class_<math::BBox<Vec3T, true>, math::BaseBBox<Vec3T>>(m, name, doc)
|
||||
.def(nb::init<>())
|
||||
.def(nb::init<const Vec3T&, const Vec3T&>(), "min"_a, "max"_a)
|
||||
.def(nb::init<const math::Coord&, const math::Coord&>(), "min"_a, "max"_a)
|
||||
.def_static("createCube", &math::BBox<Vec3T>::createCube, "min"_a, "dim"_a)
|
||||
.def(nb::init<const math::BaseBBox<math::Coord>&>(), "bbox"_a)
|
||||
.def("empty", &math::BBox<Vec3T>::empty)
|
||||
.def("dim", &math::BBox<Vec3T>::dim)
|
||||
.def("isInside", &math::BBox<Vec3T>::isInside, "p"_a)
|
||||
.def(nb::init<>(),
|
||||
"Construct an empty bounding box (min > max sentinel).")
|
||||
.def(nb::init<const Vec3T&, const Vec3T&>(), "min"_a, "max"_a,
|
||||
"Construct from explicit min and max corners.")
|
||||
.def(nb::init<const math::Coord&, const math::Coord&>(), "min"_a, "max"_a,
|
||||
"Construct from integer Coord corners, cast to floating-point.")
|
||||
.def_static("createCube", &math::BBox<Vec3T>::createCube, "min"_a, "dim"_a,
|
||||
"Construct an axis-aligned cube of side dim anchored at min.")
|
||||
.def(nb::init<const math::BaseBBox<math::Coord>&>(), "bbox"_a,
|
||||
"Construct from an integer CoordBBox, cast to floating-point.")
|
||||
.def("empty", &math::BBox<Vec3T>::empty,
|
||||
"True iff this bbox is empty (any min component > the matching max).")
|
||||
.def("dim", &math::BBox<Vec3T>::dim,
|
||||
"Return max - min as a Vec3 of side lengths.")
|
||||
.def("isInside", &math::BBox<Vec3T>::isInside, "p"_a,
|
||||
"True iff p lies inside this bounding box (half-open interval).")
|
||||
.def("__repr__", [](const math::BBox<Vec3T>& b) {
|
||||
std::stringstream ostr;
|
||||
ostr << b;
|
||||
@@ -329,24 +473,39 @@ template<typename CoordT> void defineBBoxInteger(nb::module_& m, const char* nam
|
||||
using ValueType = typename CoordT::ValueType;
|
||||
|
||||
nb::class_<math::BBox<CoordT, false>, math::BaseBBox<CoordT>>(m, name, doc)
|
||||
.def(nb::init<>())
|
||||
.def(nb::init<const CoordT&, const CoordT&>(), "min"_a, "max"_a)
|
||||
.def(nb::init<>(),
|
||||
"Construct an empty CoordBBox (min > max sentinel).")
|
||||
.def(nb::init<const CoordT&, const CoordT&>(), "min"_a, "max"_a,
|
||||
"Construct from explicit min and max Coord corners (inclusive).")
|
||||
.def(
|
||||
"__iter__",
|
||||
[](const math::BBox<CoordT>& b) { return nb::make_iterator(nb::type<math::BBox<CoordT>>(), "CoordBBoxIterator", b.begin(), b.end()); },
|
||||
nb::keep_alive<0, 1>())
|
||||
.def_static("createCube", nb::overload_cast<const CoordT&, ValueType>(&math::BBox<CoordT>::createCube), "min"_a, "dim"_a)
|
||||
.def_static("createCube", nb::overload_cast<ValueType, ValueType>(&math::BBox<CoordT>::createCube), "min"_a, "max"_a)
|
||||
.def("is_divisible", &math::BBox<CoordT>::is_divisible)
|
||||
.def("empty", &math::BBox<CoordT>::empty)
|
||||
.def("dim", &math::BBox<CoordT>::dim)
|
||||
.def("volume", &math::BBox<CoordT>::volume)
|
||||
.def("isInside", nb::overload_cast<const CoordT&>(&math::BBox<CoordT>::isInside, nb::const_), "p"_a)
|
||||
.def("isInside", nb::overload_cast<const math::BBox<CoordT>&>(&math::BBox<CoordT>::isInside, nb::const_), "b"_a)
|
||||
.def("asFloat", &math::BBox<CoordT>::template asReal<float>)
|
||||
.def("asDouble", &math::BBox<CoordT>::template asReal<double>)
|
||||
.def("hasOverlap", &math::BBox<CoordT>::hasOverlap, "b"_a)
|
||||
.def("expandBy", &math::BBox<CoordT>::expandBy, "padding"_a)
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Iterate over every Coord in this CoordBBox in row-major order.")
|
||||
.def_static("createCube", nb::overload_cast<const CoordT&, ValueType>(&math::BBox<CoordT>::createCube), "min"_a, "dim"_a,
|
||||
"Construct a cube of side dim voxels anchored at the min Coord.")
|
||||
.def_static("createCube", nb::overload_cast<ValueType, ValueType>(&math::BBox<CoordT>::createCube), "min"_a, "max"_a,
|
||||
"Construct a cube spanning [min, max] in every axis.")
|
||||
.def("is_divisible", &math::BBox<CoordT>::is_divisible,
|
||||
"True iff this CoordBBox has more than one voxel in every axis.")
|
||||
.def("empty", &math::BBox<CoordT>::empty,
|
||||
"True iff this CoordBBox is empty (any min component > the matching max).")
|
||||
.def("dim", &math::BBox<CoordT>::dim,
|
||||
"Return max - min + 1 as a Coord of side lengths (inclusive).")
|
||||
.def("volume", &math::BBox<CoordT>::volume,
|
||||
"Total number of voxels enclosed by this CoordBBox.")
|
||||
.def("isInside", nb::overload_cast<const CoordT&>(&math::BBox<CoordT>::isInside, nb::const_), "p"_a,
|
||||
"True iff the integer point p lies inside this CoordBBox.")
|
||||
.def("isInside", nb::overload_cast<const math::BBox<CoordT>&>(&math::BBox<CoordT>::isInside, nb::const_), "b"_a,
|
||||
"True iff b lies entirely inside this CoordBBox.")
|
||||
.def("asFloat", &math::BBox<CoordT>::template asReal<float>,
|
||||
"Convert this CoordBBox to a floating-point BBox of Vec3f.")
|
||||
.def("asDouble", &math::BBox<CoordT>::template asReal<double>,
|
||||
"Convert this CoordBBox to a floating-point BBox of Vec3d.")
|
||||
.def("hasOverlap", &math::BBox<CoordT>::hasOverlap, "b"_a,
|
||||
"True iff this CoordBBox shares any voxel with b.")
|
||||
.def("expandBy", &math::BBox<CoordT>::expandBy, "padding"_a,
|
||||
"Grow this CoordBBox by the given padding in every direction.")
|
||||
.def("__repr__", [](const CoordBBox& b) {
|
||||
std::stringstream ostr;
|
||||
ostr << b;
|
||||
@@ -377,33 +536,12 @@ void defineMathModule(nb::module_& m)
|
||||
defineBaseBBox<math::Coord>(m, "CoordBaseBBox");
|
||||
defineBBoxInteger<math::Coord>(m, "CoordBBox", "Bounding box for Coord minimum and maximum");
|
||||
|
||||
defineNearestNeighborSampler<float>(m, "FloatNearestNeighborSampler");
|
||||
defineTrilinearSampler<float>(m, "FloatTrilinearSampler");
|
||||
defineTriquadraticSampler<float>(m, "FloatTriquadraticSampler");
|
||||
defineTricubicSampler<float>(m, "FloatTricubicSampler");
|
||||
|
||||
defineNearestNeighborSampler<double>(m, "DoubleNearestNeighborSampler");
|
||||
defineTrilinearSampler<double>(m, "DoubleTrilinearSampler");
|
||||
defineTriquadraticSampler<double>(m, "DoubleTriquadraticSampler");
|
||||
defineTricubicSampler<double>(m, "DoubleTricubicSampler");
|
||||
|
||||
defineNearestNeighborSampler<int32_t>(m, "Int32NearestNeighborSampler");
|
||||
defineTrilinearSampler<int32_t>(m, "Int32TrilinearSampler");
|
||||
defineTriquadraticSampler<int32_t>(m, "Int32TriquadraticSampler");
|
||||
defineTricubicSampler<int32_t>(m, "Int32TricubicSampler");
|
||||
|
||||
defineNearestNeighborSampler<Vec3f>(m, "Vec3fNearestNeighborSampler");
|
||||
defineTrilinearSampler<Vec3f>(m, "Vec3fTrilinearSampler");
|
||||
defineTriquadraticSampler<Vec3f>(m, "Vec3fTriquadraticSampler");
|
||||
defineTricubicSampler<Vec3f>(m, "Vec3fTricubicSampler");
|
||||
|
||||
#ifdef NANOVDB_USE_CUDA
|
||||
nb::module_ cudaModule = m.def_submodule("cuda");
|
||||
cudaModule.doc() = "A submodule that implements CUDA-accelerated math functions";
|
||||
|
||||
defineSampleFromVoxels<float>(cudaModule, "sampleFromVoxels");
|
||||
defineSampleFromVoxels<double>(cudaModule, "sampleFromVoxels");
|
||||
#endif
|
||||
#define NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(T, Suffix) \
|
||||
defineNearestNeighborSampler<T>(m, #Suffix "NearestNeighborSampler"); \
|
||||
defineTrilinearSampler<T>(m, #Suffix "TrilinearSampler"); \
|
||||
defineTriquadraticSampler<T>(m, #Suffix "TriquadraticSampler"); \
|
||||
defineTricubicSampler<T>(m, #Suffix "TricubicSampler");
|
||||
#include "BuildTypes.def"
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
@@ -32,11 +32,9 @@ GridHandle<BufferT> createLevelSetSphere(GridType gridType,
|
||||
switch (gridType) {
|
||||
case GridType::Float: return createLevelSetSphere<float, BufferT>(radius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
case GridType::Double: return createLevelSetSphere<double, BufferT>(radius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
default: {
|
||||
std::stringstream ss;
|
||||
// ss << "Cannot createLevelSetSphere for grid of type \"" << toStr(gridType);
|
||||
throw std::runtime_error(ss.str() + "\"");
|
||||
}
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"createLevelSetSphere: only float and double grid types are supported");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,11 +56,9 @@ GridHandle<BufferT> createLevelSetTorus(GridType gridType,
|
||||
return createLevelSetTorus<float, BufferT>(majorRadius, minorRadius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
case GridType::Double:
|
||||
return createLevelSetTorus<double, BufferT>(majorRadius, minorRadius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
default: {
|
||||
std::stringstream ss;
|
||||
// ss << "Cannot createLevelSetTorus for grid of type \"" << toStr(gridType);
|
||||
throw std::runtime_error(ss.str() + "\"");
|
||||
}
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"createLevelSetTorus: only float and double grid types are supported");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,11 +77,9 @@ GridHandle<BufferT> createFogVolumeSphere(GridType gridType,
|
||||
switch (gridType) {
|
||||
case GridType::Float: return createFogVolumeSphere<float, BufferT>(radius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
case GridType::Double: return createFogVolumeSphere<double, BufferT>(radius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
default: {
|
||||
std::stringstream ss;
|
||||
// ss << "Cannot createFogVolumeSphere for grid of type \"" << toStr(gridType);
|
||||
throw std::runtime_error(ss.str() + "\"");
|
||||
}
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"createFogVolumeSphere: only float and double grid types are supported");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,14 +101,218 @@ GridHandle<BufferT> createFogVolumeTorus(GridType gridType,
|
||||
return createFogVolumeTorus<float, BufferT>(majorRadius, minorRadius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
case GridType::Double:
|
||||
return createFogVolumeTorus<double, BufferT>(majorRadius, minorRadius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
default: {
|
||||
std::stringstream ss;
|
||||
// ss << "Cannot createFogVolumeTorus for grid of type \"" << toStr(gridType);
|
||||
throw std::runtime_error(ss.str() + "\"");
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"createFogVolumeTorus: only float and double grid types are supported");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- New primitives ----------
|
||||
// Same float/double switch pattern as the four existing primitives above.
|
||||
// The C++ templates also accept Fp4/Fp8/Fp16/FpN; those flavors live on
|
||||
// the createNanoGrid path with explicit oracle and dither parameters
|
||||
// rather than being expressed as primitive overloads here.
|
||||
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT> createLevelSetBox(GridType gridType,
|
||||
double width,
|
||||
double height,
|
||||
double depth,
|
||||
const Vec3d& center,
|
||||
double voxelSize,
|
||||
double halfWidth,
|
||||
const Vec3d& origin,
|
||||
const std::string& name,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
const BufferT& buffer)
|
||||
{
|
||||
switch (gridType) {
|
||||
case GridType::Float:
|
||||
return tools::createLevelSetBox<float, BufferT>(
|
||||
width, height, depth, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
case GridType::Double:
|
||||
return tools::createLevelSetBox<double, BufferT>(
|
||||
width, height, depth, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"createLevelSetBox: only float and double grid types are supported");
|
||||
}
|
||||
}
|
||||
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT> createLevelSetBBox(GridType gridType,
|
||||
double width,
|
||||
double height,
|
||||
double depth,
|
||||
double thickness,
|
||||
const Vec3d& center,
|
||||
double voxelSize,
|
||||
double halfWidth,
|
||||
const Vec3d& origin,
|
||||
const std::string& name,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
const BufferT& buffer)
|
||||
{
|
||||
switch (gridType) {
|
||||
case GridType::Float:
|
||||
return tools::createLevelSetBBox<float, BufferT>(
|
||||
width, height, depth, thickness, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
case GridType::Double:
|
||||
return tools::createLevelSetBBox<double, BufferT>(
|
||||
width, height, depth, thickness, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"createLevelSetBBox: only float and double grid types are supported");
|
||||
}
|
||||
}
|
||||
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT> createLevelSetOctahedron(GridType gridType,
|
||||
double scale,
|
||||
const Vec3d& center,
|
||||
double voxelSize,
|
||||
double halfWidth,
|
||||
const Vec3d& origin,
|
||||
const std::string& name,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
const BufferT& buffer)
|
||||
{
|
||||
switch (gridType) {
|
||||
case GridType::Float:
|
||||
return tools::createLevelSetOctahedron<float, BufferT>(
|
||||
scale, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
case GridType::Double:
|
||||
return tools::createLevelSetOctahedron<double, BufferT>(
|
||||
scale, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"createLevelSetOctahedron: only float and double grid types are supported");
|
||||
}
|
||||
}
|
||||
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT> createFogVolumeBox(GridType gridType,
|
||||
double width,
|
||||
double height,
|
||||
double depth,
|
||||
const Vec3d& center,
|
||||
double voxelSize,
|
||||
double halfWidth,
|
||||
const Vec3d& origin,
|
||||
const std::string& name,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
const BufferT& buffer)
|
||||
{
|
||||
switch (gridType) {
|
||||
case GridType::Float:
|
||||
return tools::createFogVolumeBox<float, BufferT>(
|
||||
width, height, depth, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
case GridType::Double:
|
||||
return tools::createFogVolumeBox<double, BufferT>(
|
||||
width, height, depth, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"createFogVolumeBox: only float and double grid types are supported");
|
||||
}
|
||||
}
|
||||
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT> createFogVolumeOctahedron(GridType gridType,
|
||||
double scale,
|
||||
const Vec3d& center,
|
||||
double voxelSize,
|
||||
double halfWidth,
|
||||
const Vec3d& origin,
|
||||
const std::string& name,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
const BufferT& buffer)
|
||||
{
|
||||
switch (gridType) {
|
||||
case GridType::Float:
|
||||
return tools::createFogVolumeOctahedron<float, BufferT>(
|
||||
scale, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
case GridType::Double:
|
||||
return tools::createFogVolumeOctahedron<double, BufferT>(
|
||||
scale, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"createFogVolumeOctahedron: only float and double grid types are supported");
|
||||
}
|
||||
}
|
||||
|
||||
// Point primitives. The result is always a PointDataGrid (uint32 storage),
|
||||
// so unlike the level-set / fog-volume primitives there's no value-type
|
||||
// dispatch worth exposing — the intermediate level-set's precision is
|
||||
// not user-controllable in this binding. The C++ template also accepts
|
||||
// BuildT=double, but that path segfaults during scatter at least with the
|
||||
// current C++ implementation, so the binding stays on the float-only
|
||||
// instantiation that's exercised by the C++ unit tests.
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT> createPointSphere(int pointsPerVoxel,
|
||||
double radius,
|
||||
const Vec3d& center,
|
||||
double voxelSize,
|
||||
const Vec3d& origin,
|
||||
const std::string& name,
|
||||
CheckMode mode,
|
||||
const BufferT& buffer)
|
||||
{
|
||||
return tools::createPointSphere<float, BufferT>(
|
||||
pointsPerVoxel, radius, center, voxelSize, origin, name, mode, buffer);
|
||||
}
|
||||
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT> createPointTorus(int pointsPerVoxel,
|
||||
double majorRadius,
|
||||
double minorRadius,
|
||||
const Vec3d& center,
|
||||
double voxelSize,
|
||||
const Vec3d& origin,
|
||||
const std::string& name,
|
||||
CheckMode cMode,
|
||||
const BufferT& buffer)
|
||||
{
|
||||
return tools::createPointTorus<float, BufferT>(
|
||||
pointsPerVoxel, majorRadius, minorRadius, center, voxelSize, origin, name, cMode, buffer);
|
||||
}
|
||||
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT> createPointBox(int pointsPerVoxel,
|
||||
double width,
|
||||
double height,
|
||||
double depth,
|
||||
const Vec3d& center,
|
||||
double voxelSize,
|
||||
const Vec3d& origin,
|
||||
const std::string& name,
|
||||
CheckMode mode,
|
||||
const BufferT& buffer)
|
||||
{
|
||||
return tools::createPointBox<float, BufferT>(
|
||||
pointsPerVoxel, width, height, depth, center, voxelSize, origin, name, mode, buffer);
|
||||
}
|
||||
|
||||
// createPointScatter takes an existing level set as its source. We bind
|
||||
// the float source variant — the C++ template also accepts double, but
|
||||
// the existing primitives and tests use float, and the source grid is
|
||||
// the runtime-typed nanovdb::NanoGrid<float>, so a single overload keeps
|
||||
// the Python surface simple.
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT> createPointScatter(const NanoGrid<float>& srcGrid,
|
||||
int pointsPerVoxel,
|
||||
const std::string& name,
|
||||
CheckMode mode,
|
||||
const BufferT& buffer)
|
||||
{
|
||||
return tools::createPointScatter<float, BufferT>(
|
||||
srcGrid, pointsPerVoxel, name, mode, buffer);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
template<typename BufferT> void definePrimitives(nb::module_& m)
|
||||
@@ -131,7 +329,8 @@ template<typename BufferT> void definePrimitives(nb::module_& m)
|
||||
"name"_a = "sphere_ls",
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT());
|
||||
"buffer"_a = BufferT(),
|
||||
"Narrow-band level set of a sphere of the given radius and center.");
|
||||
|
||||
m.def("createLevelSetTorus",
|
||||
nb::overload_cast<GridType,
|
||||
@@ -155,7 +354,8 @@ template<typename BufferT> void definePrimitives(nb::module_& m)
|
||||
"name"_a = "torus_ls",
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT());
|
||||
"buffer"_a = BufferT(),
|
||||
"Narrow-band level set of a torus with the given major and minor radii.");
|
||||
|
||||
m.def("createFogVolumeSphere",
|
||||
nb::overload_cast<GridType, double, const Vec3d&, double, double, const Vec3d&, const std::string&, tools::StatsMode, CheckMode, const BufferT&>(
|
||||
@@ -169,7 +369,8 @@ template<typename BufferT> void definePrimitives(nb::module_& m)
|
||||
"name"_a = "sphere_fog",
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT());
|
||||
"buffer"_a = BufferT(),
|
||||
"Sparse fog volume of a sphere of the given radius and center.");
|
||||
|
||||
m.def("createFogVolumeTorus",
|
||||
nb::overload_cast<GridType,
|
||||
@@ -193,7 +394,139 @@ template<typename BufferT> void definePrimitives(nb::module_& m)
|
||||
"name"_a = "torus_fog",
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT());
|
||||
"buffer"_a = BufferT(),
|
||||
"Sparse fog volume of a torus with the given major and minor radii.");
|
||||
|
||||
// ---------- Level-set / fog-volume box / bbox / octahedron primitives ----
|
||||
m.def("createLevelSetBox", &createLevelSetBox<BufferT>,
|
||||
"gridType"_a = GridType::Float,
|
||||
"width"_a = 40.0,
|
||||
"height"_a = 60.0,
|
||||
"depth"_a = 100.0,
|
||||
"center"_a = Vec3d(0.0),
|
||||
"voxelSize"_a = 1.0,
|
||||
"halfWidth"_a = 3.0,
|
||||
"origin"_a = Vec3d(0.0),
|
||||
"name"_a = "box_ls",
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT(),
|
||||
"Narrow-band level set of an axis-aligned box.");
|
||||
|
||||
m.def("createLevelSetBBox", &createLevelSetBBox<BufferT>,
|
||||
"gridType"_a = GridType::Float,
|
||||
"width"_a = 40.0,
|
||||
"height"_a = 60.0,
|
||||
"depth"_a = 100.0,
|
||||
"thickness"_a = 10.0,
|
||||
"center"_a = Vec3d(0.0),
|
||||
"voxelSize"_a = 1.0,
|
||||
"halfWidth"_a = 3.0,
|
||||
"origin"_a = Vec3d(0.0),
|
||||
"name"_a = "bbox_ls",
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT(),
|
||||
"Narrow-band level set of a hollow box wireframe (BBox = bounding "
|
||||
"box edges with the given thickness).");
|
||||
|
||||
m.def("createLevelSetOctahedron", &createLevelSetOctahedron<BufferT>,
|
||||
"gridType"_a = GridType::Float,
|
||||
"scale"_a = 100.0,
|
||||
"center"_a = Vec3d(0.0),
|
||||
"voxelSize"_a = 1.0,
|
||||
"halfWidth"_a = 3.0,
|
||||
"origin"_a = Vec3d(0.0),
|
||||
// Default name spells the shape correctly even though the
|
||||
// upstream C++ default still carries the historical
|
||||
// "octadedron_ls" typo. Callers can override either way.
|
||||
"name"_a = "octahedron_ls",
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT(),
|
||||
"Narrow-band level set of an octahedron.");
|
||||
|
||||
m.def("createFogVolumeBox", &createFogVolumeBox<BufferT>,
|
||||
"gridType"_a = GridType::Float,
|
||||
"width"_a = 40.0,
|
||||
"height"_a = 60.0,
|
||||
"depth"_a = 100.0,
|
||||
"center"_a = Vec3d(0.0),
|
||||
"voxelSize"_a = 1.0,
|
||||
"halfWidth"_a = 3.0,
|
||||
"origin"_a = Vec3d(0.0),
|
||||
"name"_a = "box_fog",
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT(),
|
||||
"Sparse fog volume of a box (exterior 0/inactive, interior active "
|
||||
"with values smoothly varying from 0 at the surface to 1 inside).");
|
||||
|
||||
m.def("createFogVolumeOctahedron", &createFogVolumeOctahedron<BufferT>,
|
||||
"gridType"_a = GridType::Float,
|
||||
"scale"_a = 100.0,
|
||||
"center"_a = Vec3d(0.0),
|
||||
"voxelSize"_a = 1.0,
|
||||
"halfWidth"_a = 3.0,
|
||||
"origin"_a = Vec3d(0.0),
|
||||
"name"_a = "octahedron_fog",
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT(),
|
||||
"Sparse fog volume of an octahedron.");
|
||||
|
||||
// ---------- Point primitives ----------
|
||||
m.def("createPointSphere", &createPointSphere<BufferT>,
|
||||
"pointsPerVoxel"_a = 1,
|
||||
"radius"_a = 100.0,
|
||||
"center"_a = Vec3d(0.0),
|
||||
"voxelSize"_a = 1.0,
|
||||
"origin"_a = Vec3d(0.0),
|
||||
"name"_a = "sphere_points",
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT(),
|
||||
"PointDataGrid of points scattered on the surface of a sphere. "
|
||||
"The output grid is always a UInt32 PointDataGrid; the "
|
||||
"intermediate level-set's value type is hard-coded to float.");
|
||||
|
||||
m.def("createPointTorus", &createPointTorus<BufferT>,
|
||||
"pointsPerVoxel"_a = 1,
|
||||
"majorRadius"_a = 100.0,
|
||||
"minorRadius"_a = 50.0,
|
||||
"center"_a = Vec3d(0.0),
|
||||
"voxelSize"_a = 1.0,
|
||||
"origin"_a = Vec3d(0.0),
|
||||
"name"_a = "torus_points",
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT(),
|
||||
"PointDataGrid of points scattered on the surface of a torus. "
|
||||
"Always returns a UInt32 PointDataGrid.");
|
||||
|
||||
m.def("createPointBox", &createPointBox<BufferT>,
|
||||
"pointsPerVoxel"_a = 1,
|
||||
"width"_a = 40.0,
|
||||
"height"_a = 60.0,
|
||||
"depth"_a = 100.0,
|
||||
"center"_a = Vec3d(0.0),
|
||||
"voxelSize"_a = 1.0,
|
||||
"origin"_a = Vec3d(0.0),
|
||||
"name"_a = "box_points",
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT(),
|
||||
"PointDataGrid of points scattered on the surface of a box. "
|
||||
"Always returns a UInt32 PointDataGrid.");
|
||||
|
||||
m.def("createPointScatter", &createPointScatter<BufferT>,
|
||||
"srcGrid"_a,
|
||||
"pointsPerVoxel"_a = 1,
|
||||
"name"_a = "point_scatter",
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT(),
|
||||
"Scatter a PointDataGrid into the active voxels of a "
|
||||
"NanoGrid<float> level set. The source grid must satisfy "
|
||||
"srcGrid.isLevelSet() and have an active bounding box; "
|
||||
"non-level-set sources (e.g. fog volumes) raise RuntimeError. "
|
||||
"Point coordinates are stored as blind data in world space.");
|
||||
}
|
||||
|
||||
template void definePrimitives<HostBuffer>(nb::module_&);
|
||||
|
||||
@@ -15,20 +15,53 @@ namespace {
|
||||
|
||||
template<typename TreeT, int Order> void defineSampleFromVoxels(nb::module_& m, const char* name)
|
||||
{
|
||||
using CoordT = typename TreeT::CoordType;
|
||||
nb::class_<math::SampleFromVoxels<TreeT, Order, false>>(m, name)
|
||||
using CoordT = typename TreeT::CoordType;
|
||||
using ValueT = typename TreeT::ValueType;
|
||||
using SamplerT = math::SampleFromVoxels<TreeT, Order, false>;
|
||||
auto cls = nb::class_<SamplerT>(m, name,
|
||||
"Callable sampler that reconstructs a grid value at an arbitrary "
|
||||
"index-space position. Build via the matching create*Sampler() factory.")
|
||||
.def(
|
||||
"__call__", [](const math::SampleFromVoxels<TreeT, Order, false>& sampler, const CoordT& ijk) { return sampler(ijk); }, nb::is_operator(), "ijk"_a)
|
||||
"__call__", [](const SamplerT& sampler, const CoordT& ijk) { return sampler(ijk); }, nb::is_operator(), "ijk"_a,
|
||||
"Sample the grid at integer voxel coordinate ijk.")
|
||||
.def(
|
||||
"__call__", [](const math::SampleFromVoxels<TreeT, Order, false>& sampler, const Vec3f& xyz) { return sampler(xyz); }, nb::is_operator(), "xyz"_a)
|
||||
"__call__", [](const SamplerT& sampler, const Vec3f& xyz) { return sampler(xyz); }, nb::is_operator(), "xyz"_a,
|
||||
"Sample the grid at fractional index-space position xyz.")
|
||||
.def(
|
||||
"__call__", [](const math::SampleFromVoxels<TreeT, Order, false>& sampler, const Vec3d& xyz) { return sampler(xyz); }, nb::is_operator(), "xyz"_a);
|
||||
"__call__", [](const SamplerT& sampler, const Vec3d& xyz) { return sampler(xyz); }, nb::is_operator(), "xyz"_a,
|
||||
"Sample the grid at fractional index-space position xyz (double).");
|
||||
// gradient() exists on the trilinear sampler only, zeroCrossing() on the
|
||||
// trilinear and triquadratic samplers, and both static_assert a
|
||||
// floating-point ValueT in C++ — mirror that gating here.
|
||||
if constexpr (Order == 1 && util::is_floating_point<ValueT>::value) {
|
||||
cls.def(
|
||||
"gradient", [](const SamplerT& sampler, const Vec3f& xyz) { return sampler.gradient(xyz); }, "xyz"_a,
|
||||
"Return the index-space gradient of the trilinear reconstruction "
|
||||
"at fractional index-space position xyz. Use "
|
||||
"grid.indexToWorldGrad() to move it to world space.")
|
||||
.def(
|
||||
"gradient", [](const SamplerT& sampler, const Vec3d& xyz) { return sampler.gradient(xyz); }, "xyz"_a,
|
||||
"Return the index-space gradient at fractional index-space "
|
||||
"position xyz (double).");
|
||||
}
|
||||
if constexpr ((Order == 1 || Order == 2) && util::is_floating_point<ValueT>::value) {
|
||||
cls.def(
|
||||
"zeroCrossing", [](const SamplerT& sampler, const Vec3f& xyz) { return sampler.zeroCrossing(xyz); }, "xyz"_a,
|
||||
"True iff the reconstruction stencil at fractional index-space "
|
||||
"position xyz straddles the zero iso-surface.")
|
||||
.def(
|
||||
"zeroCrossing", [](const SamplerT& sampler, const Vec3d& xyz) { return sampler.zeroCrossing(xyz); }, "xyz"_a,
|
||||
"True iff the reconstruction stencil at fractional index-space "
|
||||
"position xyz (double) straddles the zero iso-surface.");
|
||||
}
|
||||
}
|
||||
|
||||
template<typename TreeT, int Order> void defineCreateSampler(nb::module_& m, const char* name)
|
||||
{
|
||||
m.def(
|
||||
name, [](const Grid<TreeT>& grid) { return math::createSampler<Order, TreeT, false>(grid.tree()); }, "grid"_a);
|
||||
name, [](const Grid<TreeT>& grid) { return math::createSampler<Order, TreeT, false>(grid.tree()); }, "grid"_a,
|
||||
"Build a sampler of the matching order that reads values from the "
|
||||
"given grid's tree.");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -65,24 +98,11 @@ template<typename BuildT> void defineTricubicSampler(nb::module_& m, const char*
|
||||
defineCreateSampler<TreeT, 3>(m, "createTricubicSampler");
|
||||
}
|
||||
|
||||
template void defineNearestNeighborSampler<float>(nb::module_&, const char*);
|
||||
template void defineTrilinearSampler<float>(nb::module_&, const char*);
|
||||
template void defineTriquadraticSampler<float>(nb::module_&, const char*);
|
||||
template void defineTricubicSampler<float>(nb::module_&, const char*);
|
||||
|
||||
template void defineNearestNeighborSampler<double>(nb::module_&, const char*);
|
||||
template void defineTrilinearSampler<double>(nb::module_&, const char*);
|
||||
template void defineTriquadraticSampler<double>(nb::module_&, const char*);
|
||||
template void defineTricubicSampler<double>(nb::module_&, const char*);
|
||||
|
||||
template void defineNearestNeighborSampler<int32_t>(nb::module_&, const char*);
|
||||
template void defineTrilinearSampler<int32_t>(nb::module_&, const char*);
|
||||
template void defineTriquadraticSampler<int32_t>(nb::module_&, const char*);
|
||||
template void defineTricubicSampler<int32_t>(nb::module_&, const char*);
|
||||
|
||||
template void defineNearestNeighborSampler<Vec3f>(nb::module_&, const char*);
|
||||
template void defineTrilinearSampler<Vec3f>(nb::module_&, const char*);
|
||||
template void defineTriquadraticSampler<Vec3f>(nb::module_&, const char*);
|
||||
template void defineTricubicSampler<Vec3f>(nb::module_&, const char*);
|
||||
#define NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(T, Suffix) \
|
||||
template void defineNearestNeighborSampler<T>(nb::module_&, const char*); \
|
||||
template void defineTrilinearSampler<T>(nb::module_&, const char*); \
|
||||
template void defineTriquadraticSampler<T>(nb::module_&, const char*); \
|
||||
template void defineTricubicSampler<T>(nb::module_&, const char*);
|
||||
#include "BuildTypes.def"
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "PyNanoToOpenVDB.h"
|
||||
#ifdef NANOVDB_USE_CUDA
|
||||
#include "cuda/PyPointsToGrid.h"
|
||||
#include "cuda/PySampleFromVoxels.h"
|
||||
#include "cuda/PySignedFloodFill.h"
|
||||
#endif
|
||||
|
||||
@@ -31,12 +32,17 @@ void defineToolsModule(nb::module_& m)
|
||||
|
||||
defineStatsMode(m);
|
||||
|
||||
defineGridStatsModule(m);
|
||||
defineGridValidatorModule(m);
|
||||
defineEvalChecksumModule(m);
|
||||
|
||||
defineCreateNanoGridConversions(m);
|
||||
|
||||
definePrimitives<HostBuffer>(m);
|
||||
|
||||
defineCreateNanoGrid<float>(m, "createFloatGrid");
|
||||
defineCreateNanoGrid<double>(m, "createDoubleGrid");
|
||||
defineCreateNanoGrid<int32_t>(m, "createInt32Grid");
|
||||
defineCreateNanoGrid<Vec3f>(m, "createVec3fGrid");
|
||||
#define NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(T, Suffix) \
|
||||
defineCreateNanoGrid<T>(m, "create" #Suffix "Grid");
|
||||
#include "BuildTypes.def"
|
||||
|
||||
#ifdef NANOVDB_USE_OPENVDB
|
||||
defineOpenToNanoVDB<HostBuffer>(m);
|
||||
@@ -55,6 +61,9 @@ void defineToolsModule(nb::module_& m)
|
||||
defineSignedFloodFill<double>(cudaModule, "signedFloodFill");
|
||||
|
||||
definePointsToGrid<math::Rgba8>(cudaModule, "pointsToRGBA8Grid");
|
||||
|
||||
defineSampleFromVoxels<float>(cudaModule, "sampleFromVoxels");
|
||||
defineSampleFromVoxels<double>(cudaModule, "sampleFromVoxels");
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "PyTree.h"
|
||||
|
||||
#include <nanobind/stl/string.h>
|
||||
|
||||
namespace nb = nanobind;
|
||||
using namespace nb::literals;
|
||||
using namespace nanovdb;
|
||||
|
||||
namespace pynanovdb {
|
||||
|
||||
// Polymorphic mgr() that returns the right typed NodeManager based on the
|
||||
// handle's stored gridType. Dispatch follows the same X-macro pattern as
|
||||
// pyHostGrid / pyDeviceGrid; unbound BuildTs return None rather than the
|
||||
// generic getMgr<BuildT>() ptr that would be reinterpreted.
|
||||
template<typename BufferT>
|
||||
static nb::object pyNodeMgr(nb::handle py_self)
|
||||
{
|
||||
using HandleT = NodeManagerHandle<BufferT>;
|
||||
auto& handle = nb::cast<HandleT&>(py_self);
|
||||
if (!handle.data()) return nb::none();
|
||||
// We need to read the stored gridType, but it's private. The public
|
||||
// mgr<BuildT>() returns NULL for type mismatch, so iterate by BuildT.
|
||||
// The X-macro produces one case per bound BuildT; first non-null wins.
|
||||
#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
if (auto* m = handle.template mgr<T>()) { \
|
||||
return nb::cast(m, nb::rv_policy::reference, py_self); \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \
|
||||
if (auto* m = handle.template mgr<T>()) { \
|
||||
return nb::cast(m, nb::rv_policy::reference, py_self); \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_POINT_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
if (auto* m = handle.template mgr<T>()) { \
|
||||
return nb::cast(m, nb::rv_policy::reference, py_self); \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_READONLY_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
if (auto* m = handle.template mgr<T>()) { \
|
||||
return nb::cast(m, nb::rv_policy::reference, py_self); \
|
||||
}
|
||||
#include "BuildTypes.def"
|
||||
return nb::none();
|
||||
}
|
||||
|
||||
void defineNodeManagerHandle(nb::module_& m)
|
||||
{
|
||||
using HandleT = NodeManagerHandle<HostBuffer>;
|
||||
nb::class_<HandleT>(m, "NodeManagerHandle",
|
||||
"Owns the memory backing a NodeManager. Move-only. "
|
||||
"Obtain via nanovdb.createNodeManager(grid).")
|
||||
.def("size",
|
||||
[](const HandleT& h) { return h.size(); },
|
||||
"Byte size of the buffer backing this NodeManagerHandle.")
|
||||
.def(
|
||||
"__bool__",
|
||||
[](const HandleT& h) { return h.data() != nullptr; },
|
||||
nb::is_operator(),
|
||||
"True iff this handle owns a non-empty buffer.")
|
||||
.def("mgr", &pyNodeMgr<HostBuffer>,
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Return the typed NodeManager for the grid this handle was "
|
||||
"built from, or None if the BuildT is not Python-visible. The "
|
||||
"returned NodeManager keeps this handle alive.");
|
||||
}
|
||||
|
||||
// createNodeManager has one template instantiation per BuildT. We expose a
|
||||
// single polymorphic `createNodeManager(grid)` that picks the right one
|
||||
// based on the runtime type of `grid` (any nb::class_-bound NanoGrid<T>).
|
||||
// nb::isinstance is a fast type check that avoids the exception-on-mismatch
|
||||
// overhead that would come from trying nb::cast and catching cast_error for
|
||||
// every non-matching BuildT.
|
||||
template<typename BuildT>
|
||||
static nb::object tryCreateNodeManager(nb::handle py_grid)
|
||||
{
|
||||
using GridT = NanoGrid<BuildT>;
|
||||
if (!nb::isinstance<GridT>(py_grid)) {
|
||||
return nb::object(); // sentinel: "not this BuildT, try next"
|
||||
}
|
||||
auto& grid = nb::cast<GridT&>(py_grid);
|
||||
return nb::cast(createNodeManager<BuildT, HostBuffer>(grid));
|
||||
}
|
||||
|
||||
void defineCreateNodeManager(nb::module_& m)
|
||||
{
|
||||
m.def("createNodeManager",
|
||||
[](nb::handle py_grid) -> nb::object {
|
||||
// Try every bound BuildT; first successful cast wins.
|
||||
#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
if (auto obj = tryCreateNodeManager<T>(py_grid); obj.is_valid()) { \
|
||||
return obj; \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \
|
||||
if (auto obj = tryCreateNodeManager<T>(py_grid); obj.is_valid()) { \
|
||||
return obj; \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_POINT_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
if (auto obj = tryCreateNodeManager<T>(py_grid); obj.is_valid()) { \
|
||||
return obj; \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_READONLY_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
if (auto obj = tryCreateNodeManager<T>(py_grid); obj.is_valid()) { \
|
||||
return obj; \
|
||||
}
|
||||
#include "BuildTypes.def"
|
||||
throw nb::type_error(
|
||||
"createNodeManager: argument is not a NanoVDB grid of any "
|
||||
"bound BuildT");
|
||||
},
|
||||
"grid"_a,
|
||||
// The constructed NodeManager stores a raw pointer back to the
|
||||
// grid; the handle must therefore keep the grid (and transitively
|
||||
// the GridHandle that owns the grid's buffer) alive.
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Build a NodeManager for the given grid, returning a "
|
||||
"NodeManagerHandle that owns the underlying buffer. The handle's "
|
||||
"mgr() method returns the typed NodeManager. The handle keeps the "
|
||||
"source grid alive for as long as it lives.");
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
@@ -0,0 +1,486 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#ifndef NANOVDB_PYTREE_HAS_BEEN_INCLUDED
|
||||
#define NANOVDB_PYTREE_HAS_BEEN_INCLUDED
|
||||
|
||||
#include <nanobind/nanobind.h>
|
||||
#include <nanobind/ndarray.h>
|
||||
#include <nanobind/stl/tuple.h>
|
||||
|
||||
#include <nanovdb/NanoVDB.h>
|
||||
#include <nanovdb/NodeManager.h>
|
||||
#include <nanovdb/HostBuffer.h>
|
||||
|
||||
#include <type_traits> // std::is_arithmetic_v, std::enable_if
|
||||
|
||||
namespace nb = nanobind;
|
||||
|
||||
namespace pynanovdb {
|
||||
|
||||
// -------------------- NanoLeaf<BuildT> --------------------
|
||||
//
|
||||
// Binds the 8^3 leaf node. Methods that don't depend on whether the leaf
|
||||
// stores a contiguous T[512] array are bound unconditionally; the
|
||||
// zero-copy 512-element NumPy values() view is only bound when the leaf
|
||||
// layout actually carries T mValues[512] (the BuildTraits<T>::is_special
|
||||
// types use packed / index / mask layouts where a 512-element T view is
|
||||
// either impossible or misleading).
|
||||
template<typename BuildT> void defineNanoLeaf(nb::module_& m, const char* name)
|
||||
{
|
||||
using LeafT = nanovdb::NanoLeaf<BuildT>;
|
||||
using ValueT = typename LeafT::ValueType;
|
||||
using CoordT = typename LeafT::CoordType;
|
||||
|
||||
auto cls = nb::class_<LeafT>(m, name,
|
||||
"Leaf node — 8x8x8 voxels. Inherits stats and bbox from the same "
|
||||
"leaf-data block bound across BuildTs.");
|
||||
|
||||
cls.def("origin", &LeafT::origin,
|
||||
"Index-space origin (minimum corner) of this leaf node.")
|
||||
.def("bbox", &LeafT::bbox,
|
||||
"Index-space bounding box of this leaf's active voxels.")
|
||||
.def("hasBBox", &LeafT::hasBBox,
|
||||
"True iff this leaf carries a cached active bounding box.")
|
||||
.def_static("dim", &LeafT::dim,
|
||||
"Side length of a leaf node in voxels (always 8).")
|
||||
.def_static("voxelCount", &LeafT::voxelCount,
|
||||
"Total voxel count in a leaf (always 512).")
|
||||
.def("memUsage", &LeafT::memUsage,
|
||||
"Byte size of this leaf node.")
|
||||
.def("isActive",
|
||||
nb::overload_cast<const CoordT&>(&LeafT::isActive, nb::const_),
|
||||
nb::arg("ijk"),
|
||||
"True iff the voxel at index-space ijk is active.")
|
||||
.def("isActive",
|
||||
[](const LeafT& leaf, uint32_t n) {
|
||||
// Underlying mValueMask.isOn(n) is unchecked; release builds
|
||||
// skip the C++ NANOVDB_ASSERT and would silently read OOB.
|
||||
if (n >= LeafT::voxelCount()) {
|
||||
throw nb::index_error(
|
||||
"Leaf.isActive(n): n out of range [0, voxelCount)");
|
||||
}
|
||||
return leaf.isActive(n);
|
||||
},
|
||||
nb::arg("n"),
|
||||
"True iff the n-th voxel (linear index into the 512-element leaf) is active.")
|
||||
.def("getValue",
|
||||
[](const LeafT& leaf, uint32_t offset) {
|
||||
// mValues[offset] is unchecked in C++; guard the Python side.
|
||||
if (offset >= LeafT::voxelCount()) {
|
||||
throw nb::index_error(
|
||||
"Leaf.getValue(offset): offset out of range [0, voxelCount)");
|
||||
}
|
||||
return leaf.getValue(offset);
|
||||
},
|
||||
nb::arg("offset"),
|
||||
"Return the value at linear offset (0..511) within this leaf.")
|
||||
.def("getValue",
|
||||
nb::overload_cast<const CoordT&>(&LeafT::getValue, nb::const_),
|
||||
nb::arg("ijk"),
|
||||
"Return the value at index-space ijk; ijk must lie inside this leaf.")
|
||||
.def("getFirstValue", &LeafT::getFirstValue,
|
||||
"Value at the leaf's first voxel (linear offset 0).")
|
||||
.def("getLastValue", &LeafT::getLastValue,
|
||||
"Value at the leaf's last voxel (linear offset 511).")
|
||||
.def("minimum", &LeafT::minimum,
|
||||
"Minimum active value within this leaf.")
|
||||
.def("maximum", &LeafT::maximum,
|
||||
"Maximum active value within this leaf.")
|
||||
.def("average", &LeafT::average,
|
||||
"Average of active values within this leaf.")
|
||||
.def("stdDeviation", &LeafT::stdDeviation,
|
||||
"Standard deviation of active values within this leaf.")
|
||||
// NOTE: variance() omitted — NanoVDB.h line 4388 uses unqualified
|
||||
// Pow2() which fails ADL for non-float ValueTs (ValueIndex /
|
||||
// ValueMask / etc.). Users can compute it as stdDeviation() ** 2.
|
||||
.def("flags", &LeafT::flags,
|
||||
"Raw leaf flag bits.")
|
||||
.def("valueMask", &LeafT::valueMask, nb::rv_policy::reference_internal,
|
||||
"Reference to the leaf's 512-bit active-value mask.")
|
||||
.def("probeValue",
|
||||
[](const LeafT& leaf, const CoordT& ijk) {
|
||||
ValueT v;
|
||||
bool on = leaf.probeValue(ijk, v);
|
||||
return std::make_tuple(v, on);
|
||||
},
|
||||
nb::arg("ijk"),
|
||||
"Return (value, isActive) for the voxel at index-space ijk.");
|
||||
|
||||
// Zero-copy 512-element NumPy view of mValues. Only enabled for
|
||||
// BuildTs whose ValueType is a primitive arithmetic type (float,
|
||||
// double, int*). For Fp* / Index / Mask / bool / Point the leaf uses
|
||||
// packed / mask / void layouts. For Vec3f / Vec3d / Vec4f / Vec4d /
|
||||
// Vec3u8 / Vec3u16 / Rgba8 the leaf carries a struct array that
|
||||
// nanobind's ndarray can't represent directly — those would need a
|
||||
// flattened (count, dim) component-typed view which a follow-up can
|
||||
// add. Users can still walk every leaf via the bound getValue().
|
||||
if constexpr (std::is_arithmetic_v<ValueT>
|
||||
&& !nanovdb::BuildTraits<BuildT>::is_special) {
|
||||
cls.def("values",
|
||||
[](nb::handle py_self) {
|
||||
auto& leaf = nb::cast<LeafT&>(py_self);
|
||||
size_t shape[1] = {LeafT::voxelCount()};
|
||||
return nb::cast(
|
||||
nb::ndarray<nb::numpy, ValueT, nb::ndim<1>, nb::c_contig, nb::device::cpu>(
|
||||
static_cast<void*>(leaf.data()->mValues),
|
||||
size_t(1), shape, py_self),
|
||||
nb::rv_policy::reference);
|
||||
},
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Return a zero-copy NumPy view of the 512 leaf values. The view "
|
||||
"keeps the leaf (and transitively the GridHandle that owns the "
|
||||
"underlying buffer) alive.");
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- NanoUpper / NanoLower<BuildT> --------------------
|
||||
//
|
||||
// Both internal node levels share the same C++ API surface (just different
|
||||
// LOG2DIM). One helper templated on the concrete InternalNode type.
|
||||
template<typename InternalT>
|
||||
void defineInternalNodeBase(nb::class_<InternalT>& cls)
|
||||
{
|
||||
using ValueT = typename InternalT::ValueType;
|
||||
using CoordT = typename InternalT::CoordType;
|
||||
cls.def("origin", &InternalT::origin,
|
||||
"Index-space origin (minimum corner) of this internal node.")
|
||||
.def("bbox", &InternalT::bbox,
|
||||
"Index-space bounding box of this internal node's active voxels.")
|
||||
.def_static("dim", &InternalT::dim,
|
||||
"Side length in voxels covered by this internal node.")
|
||||
.def_static("memUsage", []() { return InternalT::memUsage(); },
|
||||
"Byte size of an internal node.")
|
||||
.def("minimum", &InternalT::minimum,
|
||||
"Minimum active value within this node's subtree.")
|
||||
.def("maximum", &InternalT::maximum,
|
||||
"Maximum active value within this node's subtree.")
|
||||
.def("average", &InternalT::average,
|
||||
"Average of active values within this node's subtree.")
|
||||
.def("stdDeviation", &InternalT::stdDeviation,
|
||||
"Standard deviation of active values within this node's subtree.")
|
||||
// variance() omitted for parity with the leaf binding; compute as
|
||||
// stdDeviation() ** 2 in Python.
|
||||
.def("valueMask", &InternalT::valueMask, nb::rv_policy::reference_internal,
|
||||
"Reference to the node's active-tile mask.")
|
||||
.def("childMask", &InternalT::childMask, nb::rv_policy::reference_internal,
|
||||
"Reference to the node's child-pointer mask (1 where a child node exists).")
|
||||
.def("getValue",
|
||||
nb::overload_cast<const CoordT&>(&InternalT::getValue, nb::const_),
|
||||
nb::arg("ijk"),
|
||||
"Return the value at index-space ijk by descending into the subtree.")
|
||||
.def("getFirstValue", &InternalT::getFirstValue,
|
||||
"Value at the first (lowest-indexed) tile in this node.")
|
||||
.def("getLastValue", &InternalT::getLastValue,
|
||||
"Value at the last (highest-indexed) tile in this node.")
|
||||
.def("isActive",
|
||||
nb::overload_cast<const CoordT&>(&InternalT::isActive, nb::const_),
|
||||
nb::arg("ijk"),
|
||||
"True iff the voxel at index-space ijk is active.")
|
||||
.def("probeValue",
|
||||
[](const InternalT& node, const CoordT& ijk) {
|
||||
ValueT v;
|
||||
bool on = node.probeValue(ijk, v);
|
||||
return std::make_tuple(v, on);
|
||||
},
|
||||
nb::arg("ijk"),
|
||||
"Return (value, isActive) for the voxel at index-space ijk.");
|
||||
}
|
||||
|
||||
template<typename BuildT> void defineNanoUpper(nb::module_& m, const char* name)
|
||||
{
|
||||
using UpperT = nanovdb::NanoUpper<BuildT>;
|
||||
nb::class_<UpperT> cls(m, name,
|
||||
"Upper internal node — 32x32x32 (covers a 4096^3 region in index space).");
|
||||
defineInternalNodeBase(cls);
|
||||
}
|
||||
|
||||
template<typename BuildT> void defineNanoLower(nb::module_& m, const char* name)
|
||||
{
|
||||
using LowerT = nanovdb::NanoLower<BuildT>;
|
||||
nb::class_<LowerT> cls(m, name,
|
||||
"Lower internal node — 16x16x16 (covers a 128^3 region in index space).");
|
||||
defineInternalNodeBase(cls);
|
||||
}
|
||||
|
||||
// -------------------- NanoRoot<BuildT> --------------------
|
||||
template<typename BuildT> void defineNanoRoot(nb::module_& m, const char* name)
|
||||
{
|
||||
using RootT = nanovdb::NanoRoot<BuildT>;
|
||||
using ValueT = typename RootT::ValueType;
|
||||
using CoordT = typename RootT::CoordType;
|
||||
|
||||
nb::class_<RootT>(m, name, "Root node — top of the tree, holds the tile table.")
|
||||
.def("background", &RootT::background, nb::rv_policy::reference_internal,
|
||||
"Background value returned for inactive voxels.")
|
||||
.def("tileCount", &RootT::tileCount,
|
||||
"Number of active tiles in the root tile table.")
|
||||
.def("getTableSize", &RootT::getTableSize,
|
||||
"Total number of entries in the root tile table.")
|
||||
.def("isEmpty", &RootT::isEmpty,
|
||||
"True iff the root has no active tiles.")
|
||||
.def("bbox", &RootT::bbox, nb::rv_policy::reference_internal,
|
||||
"Index-space bounding box of every active value in this tree.")
|
||||
.def("minimum", &RootT::minimum, nb::rv_policy::reference_internal,
|
||||
"Minimum active value in the tree.")
|
||||
.def("maximum", &RootT::maximum, nb::rv_policy::reference_internal,
|
||||
"Maximum active value in the tree.")
|
||||
.def("average", &RootT::average, nb::rv_policy::reference_internal,
|
||||
"Average of active values in the tree.")
|
||||
.def("stdDeviation", &RootT::stdDeviation, nb::rv_policy::reference_internal,
|
||||
"Standard deviation of active values in the tree.")
|
||||
.def("memUsage",
|
||||
nb::overload_cast<>(&RootT::memUsage, nb::const_),
|
||||
"Byte size of the root node and its tile table.")
|
||||
.def("getValue",
|
||||
nb::overload_cast<const CoordT&>(&RootT::getValue, nb::const_),
|
||||
nb::arg("ijk"),
|
||||
"Return the value at index-space ijk by walking from the root.")
|
||||
.def("isActive",
|
||||
nb::overload_cast<const CoordT&>(&RootT::isActive, nb::const_),
|
||||
nb::arg("ijk"),
|
||||
"True iff the voxel at index-space ijk is active.")
|
||||
.def("probeValue",
|
||||
[](const RootT& root, const CoordT& ijk) {
|
||||
ValueT v;
|
||||
bool on = root.probeValue(ijk, v);
|
||||
return std::make_tuple(v, on);
|
||||
},
|
||||
nb::arg("ijk"),
|
||||
"Return (value, isActive) for the voxel at index-space ijk.");
|
||||
}
|
||||
|
||||
// -------------------- NanoTree<BuildT> --------------------
|
||||
template<typename BuildT> void defineNanoTree(nb::module_& m, const char* name)
|
||||
{
|
||||
using TreeT = nanovdb::NanoTree<BuildT>;
|
||||
using ValueT = typename TreeT::ValueType;
|
||||
using CoordT = typename TreeT::CoordType;
|
||||
using RootT = typename TreeT::RootType;
|
||||
using UpperT = typename TreeT::UpperNodeType;
|
||||
using LowerT = typename TreeT::LowerNodeType;
|
||||
using LeafT = typename TreeT::LeafNodeType;
|
||||
|
||||
nb::class_<TreeT>(m, name,
|
||||
"Tree — owns the root and provides bulk metadata queries "
|
||||
"(node counts, active voxel count, extrema).")
|
||||
.def("root",
|
||||
nb::overload_cast<>(&TreeT::root, nb::const_),
|
||||
nb::rv_policy::reference_internal,
|
||||
"Root node of this tree. Lifetime is anchored to the tree.")
|
||||
.def("background", &TreeT::background, nb::rv_policy::reference_internal,
|
||||
"Background value returned for inactive voxels.")
|
||||
.def("activeVoxelCount", &TreeT::activeVoxelCount,
|
||||
"Total number of active voxels in this tree.")
|
||||
// activeTileCount(level): valid range is 1..3 (lower / upper / root
|
||||
// tile counts). C++ uses NANOVDB_ASSERT(level > 0 && level <= 3)
|
||||
// which is a no-op in release builds — so guard explicitly.
|
||||
.def("activeTileCount",
|
||||
[](const TreeT& tree, uint32_t level) -> uint32_t {
|
||||
if (level < 1 || level > 3) {
|
||||
throw nb::value_error(
|
||||
"Tree.activeTileCount(level): level must be 1, 2, or 3");
|
||||
}
|
||||
return tree.activeTileCount(level);
|
||||
},
|
||||
nb::arg("level"),
|
||||
"Number of active tiles at the given tree level (1=lower, 2=upper, 3=root).")
|
||||
// nodeCount(level): valid range is 0..2 (leaf / lower / upper).
|
||||
// C++ uses NANOVDB_ASSERT(level < 3), again no-op in release.
|
||||
// The lambda's `int level` argument disambiguates the call against
|
||||
// Tree's templated nodeCount<NodeT>() overload at the C++ level,
|
||||
// so we don't need an overload_cast / static_cast wrapper here.
|
||||
.def("nodeCount",
|
||||
[](const TreeT& tree, int level) -> uint32_t {
|
||||
if (level < 0 || level >= 3) {
|
||||
throw nb::value_error(
|
||||
"Tree.nodeCount(level): level must be 0, 1, or 2");
|
||||
}
|
||||
return tree.nodeCount(level);
|
||||
},
|
||||
nb::arg("level"),
|
||||
"Number of nodes at the given tree level (0=leaf, 1=lower, 2=upper).")
|
||||
.def("totalNodeCount", &TreeT::totalNodeCount,
|
||||
"Sum of node counts across every tree level.")
|
||||
.def_static("memUsage", &TreeT::memUsage,
|
||||
"Byte size of a single tree structure (header only).")
|
||||
.def("getValue",
|
||||
nb::overload_cast<const CoordT&>(&TreeT::getValue, nb::const_),
|
||||
nb::arg("ijk"),
|
||||
"Return the value at index-space ijk by descending from the root.")
|
||||
.def("isActive", &TreeT::isActive, nb::arg("ijk"),
|
||||
"True iff the voxel at index-space ijk is active.")
|
||||
.def("probeValue",
|
||||
[](const TreeT& tree, const CoordT& ijk) {
|
||||
ValueT v;
|
||||
bool on = tree.probeValue(ijk, v);
|
||||
return std::make_tuple(v, on);
|
||||
},
|
||||
nb::arg("ijk"),
|
||||
"Return (value, isActive) for the voxel at index-space ijk.")
|
||||
.def("extrema",
|
||||
[](const TreeT& tree) {
|
||||
ValueT mn, mx;
|
||||
tree.extrema(mn, mx);
|
||||
return std::make_tuple(mn, mx);
|
||||
},
|
||||
"Return (min, max) of the active values over the whole tree.")
|
||||
.def("getFirstLeaf",
|
||||
nb::overload_cast<>(&TreeT::getFirstLeaf, nb::const_),
|
||||
nb::rv_policy::reference_internal,
|
||||
"First leaf node in breadth-first order, or None if the tree is empty.")
|
||||
.def("getFirstLower",
|
||||
nb::overload_cast<>(&TreeT::getFirstLower, nb::const_),
|
||||
nb::rv_policy::reference_internal,
|
||||
"First lower internal node in breadth-first order, or None if none exists.")
|
||||
.def("getFirstUpper",
|
||||
nb::overload_cast<>(&TreeT::getFirstUpper, nb::const_),
|
||||
nb::rv_policy::reference_internal,
|
||||
"First upper internal node in breadth-first order, or None if none exists.");
|
||||
}
|
||||
|
||||
// -------------------- NodeManager<BuildT> --------------------
|
||||
//
|
||||
// NodeManager is heap-managed by a NodeManagerHandle (move-only, owns the
|
||||
// underlying memory). We bind one NodeManager class per BuildT and one
|
||||
// host-side NodeManagerHandle class. Users get a handle from
|
||||
// nanovdb.createNodeManager(grid); they then call handle.mgr() to obtain a
|
||||
// borrowed pointer to the typed NodeManager — its lifetime is anchored to
|
||||
// the handle via reference_internal.
|
||||
template<typename BuildT> void defineNodeManager(nb::module_& m, const char* name)
|
||||
{
|
||||
using NMT = nanovdb::NodeManager<BuildT>;
|
||||
nb::class_<NMT>(m, name,
|
||||
"Sequential breadth-first accessor for the leaf / lower / upper "
|
||||
"internal nodes of a NanoGrid. Construct via "
|
||||
"nanovdb.createNodeManager(grid).")
|
||||
.def("isLinear",
|
||||
nb::overload_cast<>(&NMT::isLinear, nb::const_),
|
||||
"True iff this NodeManager is laid out as a linear offset table over a breadth-first grid.")
|
||||
.def("memUsage",
|
||||
nb::overload_cast<>(&NMT::memUsage, nb::const_),
|
||||
"Byte size of this NodeManager.")
|
||||
.def("nodeCount",
|
||||
[](const NMT& nm, int level) -> uint64_t {
|
||||
// Mirror Tree.nodeCount bounds (NodeManager forwards to Tree).
|
||||
if (level < 0 || level >= 3) {
|
||||
throw nb::value_error(
|
||||
"NodeManager.nodeCount(level): level must be 0, 1, or 2");
|
||||
}
|
||||
return nm.nodeCount(level);
|
||||
},
|
||||
nb::arg("level"),
|
||||
"Number of nodes at the given tree level (0=leaf, 1=lower, 2=upper).")
|
||||
.def("leafCount", &NMT::leafCount,
|
||||
"Number of leaf nodes managed by this NodeManager.")
|
||||
.def("lowerCount", &NMT::lowerCount,
|
||||
"Number of lower internal nodes managed by this NodeManager.")
|
||||
.def("upperCount", &NMT::upperCount,
|
||||
"Number of upper internal nodes managed by this NodeManager.")
|
||||
// leaf / lower / upper: NANOVDB_ASSERT(i < nodeCount(LEVEL)) in C++ is
|
||||
// no-op in release, so guard explicitly to convert OOB access into a
|
||||
// Python IndexError instead of memory corruption.
|
||||
.def("leaf",
|
||||
[](const NMT& nm, uint32_t i) -> const nanovdb::NanoLeaf<BuildT>& {
|
||||
if (i >= nm.leafCount()) {
|
||||
throw nb::index_error(
|
||||
"NodeManager.leaf(i): i out of range [0, leafCount)");
|
||||
}
|
||||
return nm.leaf(i);
|
||||
},
|
||||
nb::rv_policy::reference_internal, nb::arg("i"),
|
||||
"Return the i-th leaf node in breadth-first order.")
|
||||
.def("lower",
|
||||
[](const NMT& nm, uint32_t i) -> const nanovdb::NanoLower<BuildT>& {
|
||||
if (i >= nm.lowerCount()) {
|
||||
throw nb::index_error(
|
||||
"NodeManager.lower(i): i out of range [0, lowerCount)");
|
||||
}
|
||||
return nm.lower(i);
|
||||
},
|
||||
nb::rv_policy::reference_internal, nb::arg("i"),
|
||||
"Return the i-th lower internal node in breadth-first order.")
|
||||
.def("upper",
|
||||
[](const NMT& nm, uint32_t i) -> const nanovdb::NanoUpper<BuildT>& {
|
||||
if (i >= nm.upperCount()) {
|
||||
throw nb::index_error(
|
||||
"NodeManager.upper(i): i out of range [0, upperCount)");
|
||||
}
|
||||
return nm.upper(i);
|
||||
},
|
||||
nb::rv_policy::reference_internal, nb::arg("i"),
|
||||
"Return the i-th upper internal node in breadth-first order.");
|
||||
}
|
||||
|
||||
void defineNodeManagerHandle(nb::module_& m);
|
||||
void defineCreateNodeManager(nb::module_& m);
|
||||
|
||||
// -------------------- grid.leaf_values() bulk extractor --------------------
|
||||
//
|
||||
// For non-special BuildTs with breadth-first, fixed-size leaves, the leaf
|
||||
// values can be reached as a contiguous (N_leaves, 512) array — every leaf
|
||||
// occupies sizeof(NanoLeaf<T>) bytes and mValues starts at a known offset
|
||||
// inside each leaf. We bind this on NanoGrid<T> as leaf_values() for
|
||||
// efficient bulk analytics from Python.
|
||||
template<typename BuildT, typename = void>
|
||||
struct PyLeafValuesBinder
|
||||
{
|
||||
template<typename ClsT> static void apply(ClsT&) {}
|
||||
};
|
||||
|
||||
template<typename BuildT>
|
||||
struct PyLeafValuesBinder<BuildT,
|
||||
typename std::enable_if<
|
||||
std::is_arithmetic_v<typename nanovdb::NanoLeaf<BuildT>::ValueType>
|
||||
&& !nanovdb::BuildTraits<BuildT>::is_special>::type>
|
||||
{
|
||||
template<typename ClsT>
|
||||
static void apply(ClsT& cls)
|
||||
{
|
||||
using GridT = nanovdb::NanoGrid<BuildT>;
|
||||
using LeafT = nanovdb::NanoLeaf<BuildT>;
|
||||
using ValueT = typename LeafT::ValueType;
|
||||
cls.def("leaf_values",
|
||||
[](nb::handle py_self) -> nb::object {
|
||||
auto& grid = nb::cast<GridT&>(py_self);
|
||||
const auto& tree = grid.tree();
|
||||
const uint32_t nLeaves = tree.template nodeCount<LeafT>();
|
||||
if (!grid.isBreadthFirst()) {
|
||||
throw nb::value_error(
|
||||
"leaf_values() requires a breadth-first grid "
|
||||
"layout; rebuild via "
|
||||
"nanovdb.tools.createNanoGrid(...).");
|
||||
}
|
||||
// For an empty grid (no leaves) we still return an ndarray
|
||||
// — shape (0, 512) — so callers can iterate / np.asarray()
|
||||
// / shape-test without branching on a None sentinel.
|
||||
LeafT* first = const_cast<LeafT*>(tree.getFirstLeaf());
|
||||
size_t shape[2] = {nLeaves, LeafT::voxelCount()};
|
||||
int64_t strides[2] = {
|
||||
static_cast<int64_t>(sizeof(LeafT) / sizeof(ValueT)),
|
||||
1
|
||||
};
|
||||
// first is non-null whenever nLeaves > 0; when nLeaves == 0
|
||||
// we pass a dummy non-null aligned pointer (the grid itself)
|
||||
// so nanobind has something to base the empty array on.
|
||||
// Nothing will be read since the leading shape is 0.
|
||||
void* data = (first != nullptr)
|
||||
? static_cast<void*>(first->data()->mValues)
|
||||
: static_cast<void*>(&grid);
|
||||
return nb::cast(
|
||||
nb::ndarray<nb::numpy, ValueT, nb::ndim<2>, nb::device::cpu>(
|
||||
data, size_t(2), shape, py_self, strides),
|
||||
nb::rv_policy::reference);
|
||||
},
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Return a zero-copy (N_leaves, 512) NumPy view of every leaf's "
|
||||
"values, in breadth-first leaf order. Available only for "
|
||||
"BuildTs whose leaf layout carries T mValues[512] (i.e. not "
|
||||
"Fp*, Index, Mask, bool, or Point) and only on breadth-first "
|
||||
"grids. Returns an empty (0, 512) array for grids with no "
|
||||
"leaves. The view keeps the grid alive.");
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,547 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "PyVoxelBlockManager.h"
|
||||
|
||||
#include <nanobind/ndarray.h>
|
||||
#include <nanobind/stl/tuple.h>
|
||||
|
||||
#include <nanovdb/NanoVDB.h>
|
||||
#include <nanovdb/HostBuffer.h>
|
||||
#include <nanovdb/tools/CreateNanoGrid.h>
|
||||
#include <nanovdb/tools/VoxelBlockManager.h>
|
||||
#include <nanovdb/util/Util.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
namespace nb = nanobind;
|
||||
using namespace nb::literals;
|
||||
using namespace nanovdb;
|
||||
using nanovdb::tools::VoxelBlockManager;
|
||||
using nanovdb::tools::VoxelBlockManagerBase;
|
||||
using nanovdb::tools::VoxelBlockManagerHandle;
|
||||
using nanovdb::tools::buildVoxelBlockManager;
|
||||
|
||||
namespace pynanovdb {
|
||||
|
||||
// ----------------------- Log2BlockWidth dispatch --------------------------
|
||||
//
|
||||
// Log2BlockWidth is a compile-time template parameter on every VBM helper.
|
||||
// We expose it to Python as a runtime int and dispatch via a switch that
|
||||
// instantiates the four useful widths (BlockWidth = 64, 128, 256, 512).
|
||||
// Larger widths are not bound by default; callers who need them can add a
|
||||
// new case below.
|
||||
|
||||
template<typename F>
|
||||
static auto dispatchLog2BlockWidth(int log2BlockWidth, F&& fn)
|
||||
{
|
||||
switch (log2BlockWidth) {
|
||||
case 6: return fn(std::integral_constant<int, 6>{});
|
||||
case 7: return fn(std::integral_constant<int, 7>{});
|
||||
case 8: return fn(std::integral_constant<int, 8>{});
|
||||
case 9: return fn(std::integral_constant<int, 9>{});
|
||||
default:
|
||||
throw nb::value_error(
|
||||
"VoxelBlockManager: log2_block_width must be 6, 7, 8, or 9 "
|
||||
"(BlockWidth = 64, 128, 256, or 512). Larger widths are not "
|
||||
"bound in Python by default.");
|
||||
}
|
||||
}
|
||||
|
||||
// PyVBMHandle wraps the C++ VoxelBlockManagerHandle and carries the
|
||||
// log2_block_width the handle was built with. The C++ handle does NOT store
|
||||
// log2_block_width itself, so without this wrapper the Python binding would
|
||||
// have to ask the caller every time — which the user can lie about and
|
||||
// trigger out-of-bounds reads of the metadata buffers. Storing it once at
|
||||
// build time and consulting it in every accessor closes that hole.
|
||||
struct PyVBMHandle
|
||||
{
|
||||
VoxelBlockManagerHandle<HostBuffer> handle;
|
||||
int log2BlockWidth = 6;
|
||||
|
||||
PyVBMHandle() = default;
|
||||
PyVBMHandle(VoxelBlockManagerHandle<HostBuffer>&& h, int lbw) noexcept
|
||||
: handle(std::move(h)), log2BlockWidth(lbw) {}
|
||||
|
||||
PyVBMHandle(const PyVBMHandle&) = delete;
|
||||
PyVBMHandle& operator=(const PyVBMHandle&) = delete;
|
||||
PyVBMHandle(PyVBMHandle&&) = default;
|
||||
PyVBMHandle& operator=(PyVBMHandle&&) = default;
|
||||
|
||||
uint64_t blockCount() const { return handle.blockCount(); }
|
||||
uint64_t firstOffset() const { return handle.firstOffset(); }
|
||||
uint64_t lastOffset() const { return handle.lastOffset(); }
|
||||
void reset() { handle.reset(); }
|
||||
int blockWidth() const { return 1 << log2BlockWidth; }
|
||||
int jumpMapLength() const { return 1 << (log2BlockWidth - 6); }
|
||||
};
|
||||
|
||||
// ----------------------- decodeInverseMaps helper -------------------------
|
||||
//
|
||||
// Common implementation used by both the free function and the
|
||||
// handle.decodeBlock(i) method. Allocates fresh leafIndex (uint32) and
|
||||
// voxelOffset (uint16) NumPy arrays of length BlockWidth and fills them.
|
||||
template<int Log2BlockWidth>
|
||||
static nb::object pyDecodeInverseMapsImpl(const NanoGrid<ValueOnIndex>& grid,
|
||||
uint32_t firstLeafID,
|
||||
const uint64_t* jumpMap,
|
||||
uint64_t blockFirstOffset)
|
||||
{
|
||||
constexpr int BlockWidth = 1 << Log2BlockWidth;
|
||||
constexpr int JumpMapLength =
|
||||
VoxelBlockManagerBase<Log2BlockWidth>::JumpMapLength;
|
||||
|
||||
// The C++ decodeInverseMaps iterates leafID = firstLeafID ..
|
||||
// firstLeafID + nExtraLeaves, where nExtraLeaves is the popcount of
|
||||
// this block's jumpMap (each set bit marks an additional leaf
|
||||
// boundary crossed within the block). If the jumpMap is corrupt or
|
||||
// was built against a different grid, the loop could read past
|
||||
// tree.getFirstNode<0>(). Pre-compute the upper bound and validate
|
||||
// it against grid.tree().nodeCount(0) before any allocation.
|
||||
uint32_t nExtraLeaves = 0;
|
||||
for (int i = 0; i < JumpMapLength; ++i)
|
||||
nExtraLeaves += util::countOn(jumpMap[i]);
|
||||
const uint32_t nLeaves = grid.tree().nodeCount(0);
|
||||
if (uint64_t(firstLeafID) + uint64_t(nExtraLeaves) >= nLeaves) {
|
||||
throw nb::value_error(
|
||||
"decodeInverseMaps: firstLeafID + popcount(jumpMap) would "
|
||||
"index past grid.tree().nodeCount(0) — the jumpMap is "
|
||||
"either corrupt or was paired with a different grid.");
|
||||
}
|
||||
|
||||
// Each call allocates fresh BlockWidth-sized output arrays for the
|
||||
// leaf-index and voxel-offset results. We use plain new[] (rather than
|
||||
// a numpy-allocated buffer) because the produced ndarrays are returned
|
||||
// by reference and Python owns them via the capsule deleters below —
|
||||
// when the ndarray is destroyed, the capsule's deleter runs delete[].
|
||||
//
|
||||
// The raw pointers live in std::unique_ptr until the matching capsule
|
||||
// has been constructed; that way if the second allocation, the
|
||||
// decodeInverseMaps call, or either capsule construction throws, the
|
||||
// unique_ptr unwinds the half-built state cleanly. After a capsule
|
||||
// takes ownership we release() so the unique_ptr no longer double-frees.
|
||||
std::unique_ptr<uint32_t[]> leafIndex(new uint32_t[BlockWidth]);
|
||||
std::unique_ptr<uint16_t[]> voxelOffset(new uint16_t[BlockWidth]);
|
||||
|
||||
using VBM = VoxelBlockManager<Log2BlockWidth>;
|
||||
{
|
||||
// Release the GIL around the pure-C++ decode kernel — the heavy part,
|
||||
// and the only part of this helper that touches no Python objects. The
|
||||
// GIL is re-acquired on scope exit (including during exception unwind)
|
||||
// before the capsules / ndarrays below are constructed.
|
||||
nb::gil_scoped_release release;
|
||||
VBM::template decodeInverseMaps<ValueOnIndex>(
|
||||
&grid, firstLeafID, jumpMap, blockFirstOffset,
|
||||
leafIndex.get(), voxelOffset.get());
|
||||
}
|
||||
|
||||
// nb::capsule wraps the raw pointer + matching delete[] so it can serve
|
||||
// as the ndarray's owner — the capsule lives as long as the ndarray and
|
||||
// its destruction runs the deleter.
|
||||
nb::capsule leafOwner(leafIndex.get(),
|
||||
[](void* p) noexcept { delete[] static_cast<uint32_t*>(p); });
|
||||
auto* leafRaw = leafIndex.release();
|
||||
nb::capsule offsetOwner(voxelOffset.get(),
|
||||
[](void* p) noexcept { delete[] static_cast<uint16_t*>(p); });
|
||||
auto* offsetRaw = voxelOffset.release();
|
||||
|
||||
size_t shape[1] = {static_cast<size_t>(BlockWidth)};
|
||||
nb::ndarray<nb::numpy, uint32_t, nb::ndim<1>, nb::c_contig, nb::device::cpu>
|
||||
leafArr(leafRaw, size_t(1), shape, leafOwner);
|
||||
nb::ndarray<nb::numpy, uint16_t, nb::ndim<1>, nb::c_contig, nb::device::cpu>
|
||||
offsetArr(offsetRaw, size_t(1), shape, offsetOwner);
|
||||
return nb::make_tuple(
|
||||
nb::cast(leafArr, nb::rv_policy::reference),
|
||||
nb::cast(offsetArr, nb::rv_policy::reference));
|
||||
}
|
||||
|
||||
// ------------------- VoxelBlockManagerHandle binding ----------------------
|
||||
|
||||
static const NanoGrid<ValueOnIndex>* castOnIndexGrid(nb::handle py_grid,
|
||||
const char* fn_name)
|
||||
{
|
||||
if (!nb::isinstance<NanoGrid<ValueOnIndex>>(py_grid)) {
|
||||
std::string msg(fn_name);
|
||||
msg += ": grid must be a NanoVDB grid of build type ValueOnIndex (OnIndexGrid)";
|
||||
throw nb::type_error(msg.c_str());
|
||||
}
|
||||
return &nb::cast<const NanoGrid<ValueOnIndex>&>(py_grid);
|
||||
}
|
||||
|
||||
static void defineHandle(nb::module_& toolsModule)
|
||||
{
|
||||
nb::class_<PyVBMHandle>(toolsModule, "VoxelBlockManagerHandle",
|
||||
"Owns the firstLeafID / jumpMap metadata buffers backing a "
|
||||
"VoxelBlockManager. Constructed by nanovdb.tools.buildVoxelBlockManager.")
|
||||
.def(nb::init<>(),
|
||||
"Construct an empty VoxelBlockManagerHandle with no buffers.")
|
||||
.def("blockCount", &PyVBMHandle::blockCount,
|
||||
"Number of voxel blocks managed by this handle.")
|
||||
.def("firstOffset", &PyVBMHandle::firstOffset,
|
||||
"Sequential voxel index of the first active voxel covered "
|
||||
"by this handle (1 by default when the handle covers the "
|
||||
"full grid).")
|
||||
.def("lastOffset", &PyVBMHandle::lastOffset,
|
||||
"Sequential voxel index of the last active voxel covered "
|
||||
"by this handle.")
|
||||
.def("reset", &PyVBMHandle::reset,
|
||||
"Release this handle's buffers and reset it to the empty state.")
|
||||
.def_prop_ro("log2_block_width", [](const PyVBMHandle& h) { return h.log2BlockWidth; },
|
||||
"The log2_block_width this handle was built with. The jumpMap "
|
||||
"and decodeBlock outputs derive their shapes from this value.")
|
||||
.def_prop_ro("block_width", &PyVBMHandle::blockWidth,
|
||||
"BlockWidth = 1 << log2_block_width (64, 128, 256, or 512).")
|
||||
.def_prop_ro("jump_map_length", &PyVBMHandle::jumpMapLength,
|
||||
"JumpMapLength = BlockWidth / 64 (1, 2, 4, or 8).")
|
||||
.def(
|
||||
"__bool__",
|
||||
[](const PyVBMHandle& h) { return h.blockCount() > 0; },
|
||||
nb::is_operator())
|
||||
// Zero-copy view of the (blockCount,) firstLeafID array.
|
||||
.def("firstLeafID",
|
||||
[](nb::handle py_self) -> nb::object {
|
||||
auto& h = nb::cast<PyVBMHandle&>(py_self);
|
||||
size_t shape[1] = {static_cast<size_t>(h.blockCount())};
|
||||
// A default-constructed or reset() handle has a null
|
||||
// hostFirstLeafID(); we still return an empty (0,) ndarray
|
||||
// so callers don't have to branch on a None sentinel. The
|
||||
// dummy non-null pointer (the handle itself) keeps nanobind
|
||||
// happy; nothing is read since the leading shape is 0.
|
||||
uint32_t* raw = h.handle.hostFirstLeafID();
|
||||
void* data = (raw != nullptr) ? static_cast<void*>(raw)
|
||||
: static_cast<void*>(&h);
|
||||
return nb::cast(
|
||||
nb::ndarray<nb::numpy, uint32_t, nb::ndim<1>,
|
||||
nb::c_contig, nb::device::cpu>(
|
||||
data, size_t(1), shape, py_self),
|
||||
nb::rv_policy::reference);
|
||||
},
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Return a zero-copy (blockCount,) uint32 NumPy view of the "
|
||||
"firstLeafID array. Returns an empty (0,) array on a "
|
||||
"default-constructed or reset() handle. The view keeps this "
|
||||
"handle alive.")
|
||||
// jumpMap is uint64_t[blockCount * JumpMapLength]. JumpMapLength is
|
||||
// determined by the log2_block_width recorded on the handle, not by
|
||||
// the caller — that way the returned view always covers exactly the
|
||||
// allocated buffer, with no risk of OOB reads.
|
||||
.def("jumpMap",
|
||||
[](nb::handle py_self) -> nb::object {
|
||||
auto& h = nb::cast<PyVBMHandle&>(py_self);
|
||||
size_t shape[2] = {static_cast<size_t>(h.blockCount()),
|
||||
static_cast<size_t>(h.jumpMapLength())};
|
||||
// Same null-buffer guard as firstLeafID(): a
|
||||
// default-constructed / reset() handle has a null
|
||||
// hostJumpMap(); return an empty (0, jump_map_length)
|
||||
// ndarray rather than passing nullptr to nanobind.
|
||||
uint64_t* raw = h.handle.hostJumpMap();
|
||||
void* data = (raw != nullptr) ? static_cast<void*>(raw)
|
||||
: static_cast<void*>(&h);
|
||||
return nb::cast(
|
||||
nb::ndarray<nb::numpy, uint64_t, nb::ndim<2>,
|
||||
nb::c_contig, nb::device::cpu>(
|
||||
data, size_t(2), shape, py_self),
|
||||
nb::rv_policy::reference);
|
||||
},
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Return a zero-copy (blockCount, jump_map_length) uint64 NumPy "
|
||||
"view of the jumpMap. The shape is determined by the "
|
||||
"log2_block_width the handle was built with. Returns an empty "
|
||||
"(0, jump_map_length) array on a default-constructed or reset() "
|
||||
"handle. The view keeps this handle alive.")
|
||||
// Decode the inverse maps for a single block of this VBM. The
|
||||
// log2_block_width is taken from the handle, so the caller cannot
|
||||
// request a width that doesn't match what was built.
|
||||
.def("decodeBlock",
|
||||
[](PyVBMHandle& self,
|
||||
nb::handle py_grid,
|
||||
uint64_t block_index) -> nb::object {
|
||||
const auto* grid = castOnIndexGrid(py_grid,
|
||||
"VoxelBlockManagerHandle.decodeBlock");
|
||||
if (block_index >= self.blockCount()) {
|
||||
throw nb::index_error(
|
||||
"VoxelBlockManagerHandle.decodeBlock(block_index): "
|
||||
"block_index out of range [0, blockCount).");
|
||||
}
|
||||
// Defensive: NanoVDB's buildVoxelBlockManager doesn't always
|
||||
// initialize firstLeafID for blocks where no leaf starts at
|
||||
// a block boundary AND no leaf's iteration sweep reaches
|
||||
// them (e.g. when the source grid is tile-compressed, so
|
||||
// some sequential offsets correspond to tile values rather
|
||||
// than leaf voxels). The slot is then uninitialized memory;
|
||||
// passing it into decodeInverseMaps would lead to an OOB
|
||||
// read of tree.getFirstNode<0>()[garbage]. Catch the case
|
||||
// and raise rather than segfault.
|
||||
const uint32_t firstLeafID =
|
||||
self.handle.hostFirstLeafID()[block_index];
|
||||
const uint32_t nLeaves = grid->tree().nodeCount(0);
|
||||
if (firstLeafID >= nLeaves) {
|
||||
throw nb::value_error(
|
||||
"VoxelBlockManagerHandle.decodeBlock: the VBM's "
|
||||
"firstLeafID for this block was not initialized by "
|
||||
"buildVoxelBlockManager (the underlying algorithm "
|
||||
"doesn't cover blocks that no leaf reaches via its "
|
||||
"iteration). This typically happens on OnIndex "
|
||||
"grids built from tile-compressed source grids; "
|
||||
"until the issue is fixed upstream the workaround "
|
||||
"is to build the source grid voxel-by-voxel with "
|
||||
"build::Grid so it stays uncompressed.");
|
||||
}
|
||||
return dispatchLog2BlockWidth(self.log2BlockWidth, [&](auto W) {
|
||||
constexpr int LBW = decltype(W)::value;
|
||||
constexpr int BlockWidth = 1 << LBW;
|
||||
constexpr int JumpMapLength =
|
||||
VoxelBlockManagerBase<LBW>::JumpMapLength;
|
||||
const uint64_t blockFirstOffset =
|
||||
self.firstOffset() + block_index * BlockWidth;
|
||||
return pyDecodeInverseMapsImpl<LBW>(
|
||||
*grid,
|
||||
firstLeafID,
|
||||
self.handle.hostJumpMap() + block_index * JumpMapLength,
|
||||
blockFirstOffset);
|
||||
});
|
||||
},
|
||||
"grid"_a, "block_index"_a,
|
||||
"Decode the inverse maps for the block_index-th block of this "
|
||||
"VBM. Returns (leaf_index, voxel_offset) uint32 / uint16 NumPy "
|
||||
"arrays of length BlockWidth = 1<<log2_block_width, using the "
|
||||
"log2_block_width the handle was built with.");
|
||||
}
|
||||
|
||||
// ------------------- buildVoxelBlockManager binding -----------------------
|
||||
|
||||
static void defineBuild(nb::module_& toolsModule)
|
||||
{
|
||||
toolsModule.def("buildVoxelBlockManager",
|
||||
[](nb::handle py_grid,
|
||||
int log2_block_width,
|
||||
uint64_t first_offset,
|
||||
uint64_t last_offset,
|
||||
uint64_t n_blocks) -> PyVBMHandle {
|
||||
const auto* grid = castOnIndexGrid(py_grid, "buildVoxelBlockManager");
|
||||
// The C++ implementation only NANOVDB_ASSERTs these preconditions,
|
||||
// which makes them no-ops in release builds. Validate them here
|
||||
// so Python callers get a clear error instead of UB / abort.
|
||||
if (!grid->isSequential()) {
|
||||
throw nb::value_error(
|
||||
"buildVoxelBlockManager: grid must satisfy "
|
||||
"grid.isSequential() (fixed-size, breadth-first node "
|
||||
"layout). NanoVDB grids constructed via "
|
||||
"tools.createOnIndexGrid satisfy this by default.");
|
||||
}
|
||||
return dispatchLog2BlockWidth(log2_block_width, [&](auto W) {
|
||||
constexpr int LBW = decltype(W)::value;
|
||||
using Base = VoxelBlockManagerBase<LBW>;
|
||||
constexpr uint64_t BlockWidth = Base::BlockWidth;
|
||||
constexpr uint64_t JumpMapLength = Base::JumpMapLength;
|
||||
// first_offset must be 1 (mod BlockWidth). The single-arg
|
||||
// C++ helper would normalize a zero input to 1; we do the
|
||||
// same here so the in-place builder below sees a valid
|
||||
// value. Validate the nonzero case ourselves.
|
||||
if (first_offset != 0 &&
|
||||
((first_offset - 1) & (BlockWidth - 1)) != 0) {
|
||||
throw nb::value_error(
|
||||
"buildVoxelBlockManager: first_offset must satisfy "
|
||||
"first_offset == 1 (mod BlockWidth). Pass 0 (the "
|
||||
"default) to let the implementation use 1.");
|
||||
}
|
||||
if (first_offset == 0) first_offset = 1;
|
||||
if (last_offset == 0) last_offset = grid->activeVoxelCount();
|
||||
if (last_offset < first_offset) return PyVBMHandle();
|
||||
// Capacity must hold at least ceil((last - first + 1) /
|
||||
// BlockWidth) blocks; otherwise the handle's lastOffset
|
||||
// would advertise more coverage than blockCount allows
|
||||
// and decodeBlock would silently truncate. The formula
|
||||
// below equals the ceil() above when BlockWidth is a
|
||||
// power of two.
|
||||
const uint64_t minBlocks =
|
||||
(last_offset - first_offset + BlockWidth) >> LBW;
|
||||
if (n_blocks != 0 && n_blocks < minBlocks) {
|
||||
std::string msg(
|
||||
"buildVoxelBlockManager: n_blocks must be at "
|
||||
"least ceil((last_offset - first_offset + 1) / "
|
||||
"BlockWidth) = ");
|
||||
msg += std::to_string(minBlocks);
|
||||
msg += ". Pass 0 (the default) to use the minimum "
|
||||
"required capacity.";
|
||||
throw nb::value_error(msg.c_str());
|
||||
}
|
||||
if (n_blocks == 0) n_blocks = minBlocks;
|
||||
// Allocate the metadata buffers ourselves so we can
|
||||
// pre-initialize firstLeafID with a sentinel value before
|
||||
// the in-place builder runs. The C++ allocating overload
|
||||
// calls HostBuffer::create() which returns uninitialized
|
||||
// memory; blocks that the algorithm doesn't touch would
|
||||
// then retain arbitrary values, and our decodeBlock guard
|
||||
// (firstLeafID >= nLeaves) might miss any garbage value
|
||||
// that happens to be < nLeaves. By prefilling with nLeaves
|
||||
// up front, every untouched slot deterministically trips
|
||||
// the guard.
|
||||
auto firstLeafIDBuf = HostBuffer::create(
|
||||
n_blocks * sizeof(uint32_t));
|
||||
auto jumpMapBuf = HostBuffer::create(
|
||||
n_blocks * JumpMapLength * sizeof(uint64_t));
|
||||
const uint32_t nLeaves = grid->tree().nodeCount(0);
|
||||
{
|
||||
uint32_t* slots = static_cast<uint32_t*>(
|
||||
firstLeafIDBuf.data());
|
||||
for (uint64_t i = 0; i < n_blocks; ++i) {
|
||||
slots[i] = nLeaves;
|
||||
}
|
||||
}
|
||||
VoxelBlockManagerHandle<HostBuffer> handle(
|
||||
std::move(firstLeafIDBuf), std::move(jumpMapBuf),
|
||||
n_blocks, first_offset, last_offset);
|
||||
// In-place builder zeros the jumpMap itself and only
|
||||
// touches firstLeafID slots it actually visits. Release the
|
||||
// GIL around it — it's pure C++ (touches no Python objects)
|
||||
// and may parallelize internally via util::forEach.
|
||||
{
|
||||
nb::gil_scoped_release release;
|
||||
buildVoxelBlockManager<LBW, HostBuffer>(grid, handle);
|
||||
}
|
||||
return PyVBMHandle(std::move(handle), LBW);
|
||||
});
|
||||
},
|
||||
"grid"_a,
|
||||
"log2_block_width"_a = 6,
|
||||
"first_offset"_a = 0,
|
||||
"last_offset"_a = 0,
|
||||
"n_blocks"_a = 0,
|
||||
"Build a host-side VoxelBlockManager from an OnIndexGrid. "
|
||||
"log2_block_width selects the per-block active-voxel count "
|
||||
"(6=64, 7=128, 8=256, 9=512). Pass 0 for first_offset / "
|
||||
"last_offset / n_blocks to use the full grid (first active "
|
||||
"voxel through grid.activeVoxelCount(), minimum block count). "
|
||||
"first_offset, if nonzero, must satisfy first_offset == 1 "
|
||||
"(mod BlockWidth).");
|
||||
}
|
||||
|
||||
// --------------------- decodeInverseMaps binding --------------------------
|
||||
|
||||
static void defineDecode(nb::module_& toolsModule)
|
||||
{
|
||||
toolsModule.def("decodeInverseMaps",
|
||||
[](nb::handle py_grid,
|
||||
uint32_t first_leaf_id,
|
||||
nb::ndarray<const uint64_t, nb::ndim<1>,
|
||||
nb::c_contig, nb::device::cpu> jump_map,
|
||||
uint64_t block_first_offset,
|
||||
int log2_block_width) -> nb::object {
|
||||
const auto* grid = castOnIndexGrid(py_grid, "decodeInverseMaps");
|
||||
// The C++ helper indexes tree.getFirstNode<0>()[first_leaf_id]
|
||||
// without a bounds check, so a stray first_leaf_id leads to an
|
||||
// OOB read. Validate up front. (We also require isSequential();
|
||||
// getFirstNode only makes sense on a sequential tree.)
|
||||
if (!grid->isSequential()) {
|
||||
throw nb::value_error(
|
||||
"decodeInverseMaps: grid must satisfy "
|
||||
"grid.isSequential().");
|
||||
}
|
||||
const uint32_t nLeaves = grid->tree().nodeCount(0);
|
||||
if (first_leaf_id >= nLeaves) {
|
||||
throw nb::index_error(
|
||||
"decodeInverseMaps: first_leaf_id out of range "
|
||||
"[0, grid.tree().nodeCount(0)).");
|
||||
}
|
||||
return dispatchLog2BlockWidth(log2_block_width, [&](auto W) {
|
||||
constexpr int LBW = decltype(W)::value;
|
||||
constexpr int JumpMapLength =
|
||||
VoxelBlockManagerBase<LBW>::JumpMapLength;
|
||||
if (jump_map.shape(0) != JumpMapLength) {
|
||||
std::string msg("decodeInverseMaps: jump_map must have "
|
||||
"length JumpMapLength = ");
|
||||
msg += std::to_string(JumpMapLength);
|
||||
msg += " for log2_block_width=";
|
||||
msg += std::to_string(LBW);
|
||||
throw nb::value_error(msg.c_str());
|
||||
}
|
||||
return pyDecodeInverseMapsImpl<LBW>(
|
||||
*grid, first_leaf_id, jump_map.data(),
|
||||
block_first_offset);
|
||||
});
|
||||
},
|
||||
"grid"_a,
|
||||
"first_leaf_id"_a,
|
||||
"jump_map"_a,
|
||||
"block_first_offset"_a,
|
||||
"log2_block_width"_a = 6,
|
||||
"Decode the inverse maps for a single voxel block of an OnIndexGrid. "
|
||||
"Returns a (leaf_index, voxel_offset) tuple of fresh NumPy arrays of "
|
||||
"length BlockWidth = 1<<log2_block_width. jump_map must have length "
|
||||
"BlockWidth/64. first_leaf_id must be in [0, grid.tree().nodeCount(0)).");
|
||||
}
|
||||
|
||||
// ----- createOnIndexGrid test-scaffold factory ----------------------------
|
||||
//
|
||||
// Narrow source-coverage factory used by the VoxelBlockManager unit tests.
|
||||
// New code should prefer tools.createNanoGridOnIndex (in PyCreateNanoGrid.cc)
|
||||
// which accepts a wider source set.
|
||||
|
||||
template<typename SrcBuildT>
|
||||
static nb::object tryCreateOnIndexGrid(nb::handle py_grid,
|
||||
uint32_t channels,
|
||||
bool include_stats,
|
||||
bool include_tiles,
|
||||
int verbose)
|
||||
{
|
||||
using SrcGridT = NanoGrid<SrcBuildT>;
|
||||
if (!nb::isinstance<SrcGridT>(py_grid)) {
|
||||
return nb::object();
|
||||
}
|
||||
const SrcGridT& src = nb::cast<const SrcGridT&>(py_grid);
|
||||
return nb::cast(
|
||||
tools::createNanoGrid<SrcGridT, ValueOnIndex, HostBuffer>(
|
||||
src, channels, include_stats, include_tiles, verbose));
|
||||
}
|
||||
|
||||
static void defineCreateOnIndexGrid(nb::module_& toolsModule)
|
||||
{
|
||||
toolsModule.def("createOnIndexGrid",
|
||||
[](nb::handle py_grid,
|
||||
uint32_t channels,
|
||||
bool include_stats,
|
||||
bool include_tiles,
|
||||
int verbose) -> nb::object {
|
||||
// Try every source BuildT we accept.
|
||||
if (auto r = tryCreateOnIndexGrid<float>(
|
||||
py_grid, channels, include_stats, include_tiles, verbose);
|
||||
r.is_valid()) return r;
|
||||
if (auto r = tryCreateOnIndexGrid<double>(
|
||||
py_grid, channels, include_stats, include_tiles, verbose);
|
||||
r.is_valid()) return r;
|
||||
if (auto r = tryCreateOnIndexGrid<int32_t>(
|
||||
py_grid, channels, include_stats, include_tiles, verbose);
|
||||
r.is_valid()) return r;
|
||||
if (auto r = tryCreateOnIndexGrid<Vec3f>(
|
||||
py_grid, channels, include_stats, include_tiles, verbose);
|
||||
r.is_valid()) return r;
|
||||
throw nb::type_error(
|
||||
"createOnIndexGrid: source grid must be a FloatGrid, "
|
||||
"DoubleGrid, Int32Grid, or Vec3fGrid (other source BuildTs "
|
||||
"are not yet bound).");
|
||||
},
|
||||
"src_grid"_a,
|
||||
"channels"_a = 0u,
|
||||
"include_stats"_a = true,
|
||||
"include_tiles"_a = true,
|
||||
"verbose"_a = 0,
|
||||
"Convert a source grid into a NanoGrid<ValueOnIndex> "
|
||||
"(OnIndexGrid). Accepts FloatGrid / DoubleGrid / Int32Grid / "
|
||||
"Vec3fGrid. This is a narrow helper kept alongside "
|
||||
"buildVoxelBlockManager; for general index conversion (broader "
|
||||
"source coverage, blind-data channels) prefer "
|
||||
"nanovdb.tools.createNanoGridOnIndex.");
|
||||
}
|
||||
|
||||
void defineVoxelBlockManagerModule(nb::module_& toolsModule)
|
||||
{
|
||||
defineHandle(toolsModule);
|
||||
defineBuild(toolsModule);
|
||||
defineDecode(toolsModule);
|
||||
defineCreateOnIndexGrid(toolsModule);
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#ifndef NANOVDB_PYVOXELBLOCKMANAGER_HAS_BEEN_INCLUDED
|
||||
#define NANOVDB_PYVOXELBLOCKMANAGER_HAS_BEEN_INCLUDED
|
||||
|
||||
#include <nanobind/nanobind.h>
|
||||
|
||||
namespace nb = nanobind;
|
||||
|
||||
namespace pynanovdb {
|
||||
|
||||
/// @brief Bind VoxelBlockManagerHandle<HostBuffer>,
|
||||
/// tools.buildVoxelBlockManager, tools.decodeInverseMaps, and the
|
||||
/// test-scaffold tools.createOnIndexGrid factory under the given
|
||||
/// Python submodule (expected to be the existing nanovdb.tools).
|
||||
void defineVoxelBlockManagerModule(nb::module_& toolsModule);
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
#endif
|
||||
@@ -1,9 +1,26 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import os
|
||||
import sys
|
||||
if sys.platform == "win32":
|
||||
import os
|
||||
openvdb_dll_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, 'openvdb', 'lib')
|
||||
os.add_dll_directory(directory)
|
||||
|
||||
from .lib.nanovdb import *
|
||||
if sys.platform == "win32":
|
||||
_openvdb_dll_directory = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), os.pardir, "openvdb", "lib"
|
||||
)
|
||||
if os.path.isdir(_openvdb_dll_directory):
|
||||
os.add_dll_directory(_openvdb_dll_directory)
|
||||
|
||||
|
||||
def get_include():
|
||||
"""Return the absolute path to the bundled NanoVDB C/C++ include directory.
|
||||
|
||||
Use this from a downstream Python extension's build system so the extension
|
||||
compiles against the same NanoVDB headers the installed wheel was built with::
|
||||
|
||||
import nanovdb
|
||||
ext_kwargs = dict(include_dirs=[nanovdb.get_include()])
|
||||
"""
|
||||
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "include")
|
||||
|
||||
|
||||
from .lib.nanovdb import * # noqa: E402,F401,F403
|
||||
|
||||
@@ -13,7 +13,10 @@ namespace pynanovdb {
|
||||
|
||||
void defineDeviceBuffer(nb::module_& m)
|
||||
{
|
||||
nb::class_<cuda::DeviceBuffer>(m, "DeviceBuffer");
|
||||
nb::class_<cuda::DeviceBuffer>(m, "DeviceBuffer",
|
||||
"CUDA device-side buffer used to back a DeviceGridHandle. Holds a "
|
||||
"host mirror and a device pointer; deviceUpload / deviceDownload on "
|
||||
"the handle move bytes between the two.");
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <nanobind/ndarray.h>
|
||||
|
||||
#include <nanovdb/cuda/DeviceBuffer.h>
|
||||
#include <nanovdb/NanoVDB.h>
|
||||
|
||||
namespace nb = nanobind;
|
||||
using namespace nb::literals;
|
||||
@@ -13,6 +14,48 @@ using namespace nanovdb;
|
||||
|
||||
namespace pynanovdb {
|
||||
|
||||
// Device-side polymorphic deviceGrid(n) — same dispatch shape as
|
||||
// pyHostGrid<BufferT> in PyGridHandle.h, but returns the device pointer.
|
||||
// gridType(n) is read from the host-side GridData header (the handle keeps
|
||||
// a host mirror), so this works whether or not the grid has been uploaded.
|
||||
// Returns None if the device-side grid is null (i.e. no deviceUpload yet)
|
||||
// or the BuildT is not Python-visible.
|
||||
static nb::object pyDeviceGrid(nb::handle py_handle, uint32_t n)
|
||||
{
|
||||
using BufferT = nanovdb::cuda::DeviceBuffer;
|
||||
auto& handle = nb::cast<GridHandle<BufferT>&>(py_handle);
|
||||
if (n >= handle.gridCount()) return nb::none();
|
||||
switch (handle.gridType(n)) {
|
||||
#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
case nanovdb::GridType::GridTypeEnum: { \
|
||||
auto* grid = handle.template deviceGrid<T>(n); \
|
||||
return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \
|
||||
: nb::none(); \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \
|
||||
case nanovdb::GridType::GridTypeEnum: { \
|
||||
auto* grid = handle.template deviceGrid<T>(n); \
|
||||
return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \
|
||||
: nb::none(); \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_POINT_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
case nanovdb::GridType::GridTypeEnum: { \
|
||||
auto* grid = handle.template deviceGrid<T>(n); \
|
||||
return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \
|
||||
: nb::none(); \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_READONLY_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
case nanovdb::GridType::GridTypeEnum: { \
|
||||
auto* grid = handle.template deviceGrid<T>(n); \
|
||||
return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \
|
||||
: nb::none(); \
|
||||
}
|
||||
#include "../BuildTypes.def"
|
||||
default:
|
||||
return nb::none();
|
||||
}
|
||||
}
|
||||
|
||||
void defineDeviceGridHandle(nb::module_& m)
|
||||
{
|
||||
using BufferT = nanovdb::cuda::DeviceBuffer;
|
||||
@@ -27,19 +70,31 @@ void defineDeviceGridHandle(nb::module_& m)
|
||||
new (&handle) GridHandle<BufferT>(std::move(buffer));
|
||||
},
|
||||
"cpu_t"_a.noconvert(),
|
||||
"cuda_t"_a.noconvert())
|
||||
.def("deviceFloatGrid", nb::overload_cast<uint32_t>(&GridHandle<BufferT>::template deviceGrid<float>), "n"_a = 0, nb::rv_policy::reference_internal)
|
||||
.def("deviceDoubleGrid", nb::overload_cast<uint32_t>(&GridHandle<BufferT>::template deviceGrid<double>), "n"_a = 0, nb::rv_policy::reference_internal)
|
||||
.def("deviceInt32Grid", nb::overload_cast<uint32_t>(&GridHandle<BufferT>::template deviceGrid<int32_t>), "n"_a = 0, nb::rv_policy::reference_internal)
|
||||
.def("deviceVec3fGrid", nb::overload_cast<uint32_t>(&GridHandle<BufferT>::template deviceGrid<Vec3f>), "n"_a = 0, nb::rv_policy::reference_internal)
|
||||
.def("deviceRGBA8Grid",
|
||||
nb::overload_cast<uint32_t>(&GridHandle<BufferT>::template deviceGrid<math::Rgba8>),
|
||||
"n"_a = 0,
|
||||
nb::rv_policy::reference_internal)
|
||||
"cuda_t"_a.noconvert(),
|
||||
"Construct a DeviceGridHandle that wraps an existing pair of "
|
||||
"host and device uint32 arrays of equal length.")
|
||||
.def("deviceGrid", &pyDeviceGrid, "n"_a = 0,
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Return the n-th device-resident grid as a typed Grid subclass "
|
||||
"selected by gridType(n), or None if the BuildT is not bound in "
|
||||
"Python or the device copy has not been uploaded yet. The "
|
||||
"returned grid keeps this handle alive.")
|
||||
.def(
|
||||
"deviceUpload", [](GridHandle<BufferT>& handle, bool sync) { handle.deviceUpload(nullptr, sync); }, "sync"_a = true)
|
||||
"deviceUpload", [](GridHandle<BufferT>& handle, bool sync) { handle.deviceUpload(nullptr, sync); }, "sync"_a = true,
|
||||
"Copy the host-side buffer to the device. If sync is True the "
|
||||
"call blocks until the transfer completes.")
|
||||
.def(
|
||||
"deviceDownload", [](GridHandle<BufferT>& handle, bool sync) { handle.deviceDownload(nullptr, sync); }, "sync"_a = true);
|
||||
"deviceDownload", [](GridHandle<BufferT>& handle, bool sync) { handle.deviceDownload(nullptr, sync); }, "sync"_a = true,
|
||||
"Copy the device-side buffer back to the host. If sync is True "
|
||||
"the call blocks until the transfer completes.");
|
||||
// NOTE: defineGridHandleUtilities<BufferT> intentionally NOT called for
|
||||
// DeviceBuffer. Registering nanovdb.splitGrids / nanovdb.mergeGrids as a
|
||||
// second overload taking a DeviceGridHandle list conflicts with the host
|
||||
// overload because both signatures take nb::list, and nanobind's
|
||||
// overload resolution can't disambiguate by element type — it picks one
|
||||
// and the inner cast fails with std::bad_cast. A properly typed device
|
||||
// variant (with its own name, or strongly-typed std::vector<HandleT>
|
||||
// args via nanobind/stl/vector.h) can land later if it's needed.
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
@@ -50,7 +50,8 @@ template<typename BuildT> void definePointsToGrid(nb::module_& m, const char* na
|
||||
auto handle = converter.getHandle(points, tensor.shape(0));
|
||||
return handle;
|
||||
},
|
||||
"tensor"_a);
|
||||
"tensor"_a,
|
||||
"Rasterize the given (N, 3) int32 device tensor of points into a fresh GridHandle.");
|
||||
}
|
||||
|
||||
template void definePointsToGrid<nanovdb::math::Rgba8>(nb::module_&, const char*);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# NanoVDB Python Examples
|
||||
|
||||
Runnable `.py` scripts demonstrating the NanoVDB Python bindings.
|
||||
Each script is self-contained, builds its own input data, and prints
|
||||
a small summary to stdout.
|
||||
|
||||
Run any example with:
|
||||
|
||||
```bash
|
||||
python load_inspect.py
|
||||
```
|
||||
|
||||
When working from the source tree, the `nanovdb` module needs to be
|
||||
on `PYTHONPATH` (e.g. via the build output directory):
|
||||
|
||||
```bash
|
||||
cd <build>/nanovdb/nanovdb/python
|
||||
PYTHONPATH=. python /path/to/<example>.py
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
| Script | What it shows |
|
||||
| --- | --- |
|
||||
| [`load_inspect.py`](load_inspect.py) | Polymorphic `handle.grid(n)` access, `GridMetaData` type-erased introspection, mixed-type handles via `mergeGrids`. |
|
||||
| [`build_grid.py`](build_grid.py) | Voxel-by-voxel construction with `nanovdb.tools.build.FloatGrid`, the cached `ValueAccessor`, the thread-safe `WriteAccessor`, and `.to_nanovdb()` to bake into a host `GridHandle`. |
|
||||
| [`bulk_leaf_numpy.py`](bulk_leaf_numpy.py) | Zero-copy `(N_leaves, 512)` NumPy view of every leaf's mValues via `grid.leaf_values()`. Includes a global-stats reduction and an in-place mutation that propagates back into the grid. Requires NumPy. |
|
||||
| [`quantize.py`](quantize.py) | Quantize a `NanoGrid<float>` through `nanovdb.tools.createNanoGridFp{4,8,16,N}`. Shows fixed-width quantization with dithering and variable-width `FpN` with both `AbsDiff` and `RelDiff` oracles. |
|
||||
| [`validate.py`](validate.py) | `nanovdb.tools.validateGrid` / `validateGrids`, `checkGrid`, `isValid`, and the `evalChecksum` / `validateChecksum` / `updateChecksum` round-trip. |
|
||||
| [`io_roundtrip.py`](io_roundtrip.py) | `nanovdb.io` write/read round-trip over five primitives: `writeGrids` with codec fallback, `readGridMetaData`, `hasGrid`, `readGrid` by name, `splitGrids`, and zero-copy point positions via `getBlindData`. Port of `ex_write_nanovdb_grids` + `ex_read_nanovdb_sphere_accessor`. |
|
||||
| [`make_funny_nanovdb.py`](make_funny_nanovdb.py) | Functor-based construction: `tools.createFloatGrid(background, name, gridClass, func, bbox)` evaluates a Python callback at every voxel. Port of `ex_make_funny_nanovdb` on a reduced domain. |
|
||||
| [`make_typed_grids.py`](make_typed_grids.py) | One solid sphere per value type via `tools.build.{Float,Double,Int16,Int32,Int64,UInt32,Vec3f}Grid`, written to a single file and re-read with polymorphic `handle.grid()` dispatch. Port of `ex_make_typed_grids`. |
|
||||
| [`raytrace_level_set.py`](raytrace_level_set.py) | CPU sphere-traced level-set render to PGM using `worldToIndex`, the trilinear sampler, `zeroCrossing()`, and normalized `gradient()` shading. Port of `ex_raytrace_level_set` (host path). |
|
||||
| [`raytrace_fog_volume.py`](raytrace_fog_volume.py) | CPU transmittance ray-march of a fog volume to PGM using a `ReadAccessor` and `Coord.Floor` — the accessor-based sampling idiom. Port of `ex_raytrace_fog_volume` (host path). |
|
||||
| [`collide_level_set.py`](collide_level_set.py) | Particles colliding with a level set: `worldToIndexF`, `tree.isActive` narrow-band test, accessor distance reads, and `sampler.gradient()` collision normals. Port of `ex_collide_level_set` (host path). |
|
||||
| [`index_grid_channels.py`](index_grid_channels.py) | `tools.createNanoGridOnIndex(src, channels=1)`, `grid.valueCount()`, coordinate reads through `createChannelAccessor`, and blind-data authoring with `tools.CreateNanoGrid.addBlindData` + the writable `getBlindData` view. Extends the host half of `ex_index_grid_cuda`. Requires NumPy for the authoring section. |
|
||||
| [`node_manager.py`](node_manager.py) | Linearized node iteration with `createNodeManager`: per-level counts, `leaf(i)` / `lower(i)` access, node origins, masks, and stats. Port of the host half of `ex_nodemanager_cuda`. |
|
||||
| [`openvdb_interop.py`](openvdb_interop.py) | `tools.openToNanoVDB` / `nanoToOpenVDB` round-trip with accessor comparison on both sides. Self-skips unless built with `NANOVDB_USE_OPENVDB` and `openvdb` is importable. Port of `ex_openvdb_to_nanovdb_accessor`. |
|
||||
|
||||
Scripts that produce files write them to a fresh temporary directory
|
||||
and print its path, so the source tree stays clean. The whole set is
|
||||
smoke-tested by [`../test/TestExamples.py`](../test/TestExamples.py)
|
||||
(ctest name `pytest_nanovdb_examples`) when the module is configured
|
||||
with `NANOVDB_BUILD_PYTHON_UNITTESTS=ON`.
|
||||
|
||||
For full API signatures and per-argument docstrings, use Python's
|
||||
`help()` on any symbol — e.g. `help(nanovdb.tools.createNanoGridFpN)`.
|
||||
The bindings ship `.pyi` type stubs for IDE / type-checker support.
|
||||
@@ -0,0 +1,80 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Construct a NanoVDB grid voxel-by-voxel in pure Python.
|
||||
|
||||
nanovdb.tools.build.<Suffix>Grid is the mutable CPU grid builder
|
||||
that mirrors nanovdb::tools::build::Grid<T>. This example shows the
|
||||
three construction loops you'll typically reach for (setValue
|
||||
directly, the cached ValueAccessor, and the thread-safe
|
||||
WriteAccessor), then bakes each build grid into a host NanoGrid via
|
||||
.to_nanovdb() and reads it back through the regular polymorphic
|
||||
handle.grid() API.
|
||||
|
||||
Run with: python build_grid.py
|
||||
"""
|
||||
import nanovdb
|
||||
|
||||
|
||||
def fill_with_setValue():
|
||||
"""Simplest path: setValue() directly on the build grid."""
|
||||
g = nanovdb.tools.build.FloatGrid(
|
||||
background=0.0, name="setValue_demo",
|
||||
gridClass=nanovdb.GridClass.FogVolume)
|
||||
# Plant five active voxels along the x axis.
|
||||
for i in range(5):
|
||||
g.setValue(nanovdb.math.Coord(i, 0, 0), float(i + 1))
|
||||
return g
|
||||
|
||||
|
||||
def fill_with_accessor():
|
||||
"""Cached path: getAccessor() avoids re-walking the tree for
|
||||
each write when consecutive coordinates share a leaf."""
|
||||
g = nanovdb.tools.build.FloatGrid(background=0.0, name="accessor_demo")
|
||||
acc = g.getAccessor()
|
||||
# The accessor caches the last leaf / lower / upper node, so a
|
||||
# burst of writes in a 16^3 neighborhood only walks the tree once.
|
||||
for x in range(8):
|
||||
for y in range(8):
|
||||
for z in range(8):
|
||||
if x + y + z == 7:
|
||||
acc.setValue(nanovdb.math.Coord(x, y, z),
|
||||
float(x * 64 + y * 8 + z))
|
||||
return g
|
||||
|
||||
|
||||
def fill_with_write_accessor():
|
||||
"""Thread-safe path: getWriteAccessor() buffers writes into a
|
||||
private root and merges them into the parent on destruction or
|
||||
on an explicit .merge() call. Useful when fanning out to multiple
|
||||
threads — one WriteAccessor per thread, no shared mutable state."""
|
||||
g = nanovdb.tools.build.FloatGrid(background=0.0, name="write_accessor_demo")
|
||||
wa = g.getWriteAccessor()
|
||||
wa.setValue(nanovdb.math.Coord(50, 50, 50), 9.0)
|
||||
# Before merge, the parent doesn't see the change yet.
|
||||
assert g.getValue(nanovdb.math.Coord(50, 50, 50)) == 0.0
|
||||
wa.merge()
|
||||
assert g.getValue(nanovdb.math.Coord(50, 50, 50)) == 9.0
|
||||
return g
|
||||
|
||||
|
||||
def main():
|
||||
for builder in (fill_with_setValue, fill_with_accessor,
|
||||
fill_with_write_accessor):
|
||||
g = builder()
|
||||
print(f"=== {g.getName()} ===")
|
||||
print(f" nodeCount (leaf, lower, upper) = {g.nodeCount()}")
|
||||
|
||||
# Bake into a host NanoGrid<float>.
|
||||
handle = g.to_nanovdb(sMode=nanovdb.tools.StatsMode.All)
|
||||
ng = handle.grid()
|
||||
print(f" baked NanoGrid: type={ng.gridType()}, "
|
||||
f"active={ng.activeVoxelCount()}, "
|
||||
f"worldBBox={ng.worldBBox()}")
|
||||
|
||||
# The build grid is left untouched — we can bake again.
|
||||
handle2 = g.to_nanovdb()
|
||||
assert handle2.grid().activeVoxelCount() == ng.activeVoxelCount()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,63 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Bulk per-leaf value access as a zero-copy NumPy array.
|
||||
|
||||
grid.leaf_values() is the highest-bandwidth path from NanoVDB into
|
||||
NumPy. It returns an (N_leaves, 512) view of every leaf's mValues
|
||||
without copying — modify it, slice it, feed it into a PyTorch tensor,
|
||||
hash it for cache lookup, whatever you need.
|
||||
|
||||
Run with: python bulk_leaf_numpy.py
|
||||
"""
|
||||
import nanovdb
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
import numpy as np
|
||||
except ImportError:
|
||||
print("This example requires numpy. Install it with: pip install numpy")
|
||||
return
|
||||
|
||||
# Build a fog volume sphere with stats so the leaves have meaningful
|
||||
# min/max attached (just for the printing below — not required by
|
||||
# leaf_values itself).
|
||||
handle = nanovdb.tools.createFogVolumeSphere(
|
||||
radius=20.0, name="bulk_demo")
|
||||
grid = handle.grid()
|
||||
print(f"Grid: {grid.gridType()}, active voxels = {grid.activeVoxelCount()}, "
|
||||
f"leaves = {grid.tree().nodeCount(0)}")
|
||||
|
||||
# leaf_values() is the zero-copy view. Modifying it modifies the grid.
|
||||
bulk = grid.leaf_values()
|
||||
# np.asarray adds a NumPy wrapper but doesn't copy.
|
||||
arr = np.asarray(bulk)
|
||||
print(f"leaf_values: shape={arr.shape}, dtype={arr.dtype}, "
|
||||
f"backed by grid memory (no copy).")
|
||||
|
||||
# Global statistics across every voxel in every leaf, computed in C.
|
||||
# 0.0 voxels (background) are excluded by using a mask.
|
||||
nonzero = arr[arr != 0.0]
|
||||
print(f" non-background voxels = {nonzero.size}")
|
||||
print(f" min = {nonzero.min()}, max = {nonzero.max()}, "
|
||||
f"mean = {nonzero.mean()}")
|
||||
|
||||
# Per-leaf reductions: each row of `arr` is one leaf's 512 voxels.
|
||||
per_leaf_max = arr.max(axis=1)
|
||||
print(f" per-leaf max (first 5): {per_leaf_max[:5]}")
|
||||
|
||||
# Zero-copy means writes propagate. Zero out the first leaf's values
|
||||
# and read one back through the regular accessor to confirm the grid
|
||||
# actually changed. (The active *mask* is unchanged — we wrote into
|
||||
# mValues only — so activeVoxelCount() stays the same.)
|
||||
arr[0] = 0.0
|
||||
leaf = grid.tree().getFirstLeaf()
|
||||
if leaf is not None:
|
||||
first_value_after = leaf.getFirstValue()
|
||||
print(f" zeroed first leaf's values in place: "
|
||||
f"leaf.getFirstValue() = {first_value_after}, "
|
||||
f"activeVoxelCount unchanged: {grid.activeVoxelCount()}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,85 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Collide falling particles against a level-set surface on the CPU.
|
||||
|
||||
Python port of the host path of ex_collide_level_set: particles fall
|
||||
under gravity; each step transforms the candidate position to index
|
||||
space with grid.worldToIndexF, tests the narrow band with
|
||||
tree.isActive, reads the signed distance through a ReadAccessor, and
|
||||
on penetration reflects the velocity about the surface normal. The C++
|
||||
original builds the normal from 6-tap finite differences of the SDF;
|
||||
here the bound sampler.gradient() provides the same quantity in one
|
||||
call (index-space units — normalized before use).
|
||||
|
||||
Run with: python collide_level_set.py
|
||||
"""
|
||||
import random
|
||||
|
||||
import nanovdb
|
||||
|
||||
NUM_PARTICLES = 500
|
||||
NUM_STEPS = 40
|
||||
DT = 0.1
|
||||
GRAVITY = -9.8
|
||||
|
||||
|
||||
def main():
|
||||
handle = nanovdb.tools.createLevelSetSphere(radius=100.0, name="sphere")
|
||||
grid = handle.grid()
|
||||
tree = grid.tree()
|
||||
acc = grid.getAccessor()
|
||||
sampler = nanovdb.math.createTrilinearSampler(grid)
|
||||
|
||||
# Seed particles above the north pole of the sphere, falling down.
|
||||
rng = random.Random(42)
|
||||
particles = []
|
||||
for _ in range(NUM_PARTICLES):
|
||||
p = [rng.uniform(-30.0, 30.0), rng.uniform(115.0, 140.0),
|
||||
rng.uniform(-30.0, 30.0)]
|
||||
particles.append((p, [0.0, -20.0, 0.0]))
|
||||
|
||||
total_collisions = 0
|
||||
for step in range(NUM_STEPS):
|
||||
collisions = 0
|
||||
for p, v in particles:
|
||||
v[1] += GRAVITY * DT
|
||||
next_p = [p[i] + v[i] * DT for i in range(3)]
|
||||
|
||||
ijk = nanovdb.math.Coord.Floor(grid.worldToIndexF(
|
||||
nanovdb.math.Vec3f(next_p[0], next_p[1], next_p[2])))
|
||||
if tree.isActive(ijk): # inside the narrow band?
|
||||
d = acc.getValue(ijk)
|
||||
if d <= 0.0: # inside the level set?
|
||||
ipos = grid.worldToIndexF(
|
||||
nanovdb.math.Vec3f(next_p[0], next_p[1], next_p[2]))
|
||||
n = sampler.gradient(ipos)
|
||||
n.normalize()
|
||||
# Project the position back to the surface and
|
||||
# reflect the velocity, as in the C++ example.
|
||||
for i in range(3):
|
||||
next_p[i] -= d * n[i]
|
||||
v_dot_n = sum(v[i] * n[i] for i in range(3))
|
||||
for i in range(3):
|
||||
v[i] -= 2.0 * v_dot_n * n[i]
|
||||
collisions += 1
|
||||
p[:] = next_p
|
||||
total_collisions += collisions
|
||||
if collisions:
|
||||
print(f"step {step:2d}: {collisions} collisions")
|
||||
|
||||
print(f"total collisions over {NUM_STEPS} steps: {total_collisions}")
|
||||
assert total_collisions > 0
|
||||
|
||||
# No particle should end up inside the surface.
|
||||
worst = 0.0
|
||||
for p, _ in particles:
|
||||
ijk = nanovdb.math.Coord.Floor(grid.worldToIndexF(
|
||||
nanovdb.math.Vec3f(p[0], p[1], p[2])))
|
||||
if tree.isActive(ijk):
|
||||
worst = min(worst, acc.getValue(ijk))
|
||||
print(f"deepest final penetration: {worst:.3f} world units")
|
||||
assert worst > -2.0 * grid.voxelSize()[0]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,86 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Index grids, value channels, and blind-data authoring.
|
||||
|
||||
Part A ports the host half of ex_index_grid_cuda: a float level set is
|
||||
re-encoded as a NanoGrid<ValueOnIndex> whose voxels store sequential
|
||||
uint64 indices, with the original float values copied into a blind-data
|
||||
channel — then read back by coordinate through a ChannelAccessor,
|
||||
which the C++ example only does in a CUDA kernel. Part B authors a
|
||||
brand-new blind-data channel on a grid with tools.CreateNanoGrid and
|
||||
fills it in place through the writable getBlindData() NumPy view.
|
||||
|
||||
Run with: python index_grid_channels.py
|
||||
"""
|
||||
import nanovdb
|
||||
|
||||
|
||||
def index_grid_with_channel():
|
||||
src = nanovdb.tools.createLevelSetSphere(radius=50.0, name="sphere")
|
||||
src_grid = src.grid()
|
||||
|
||||
# channels=1 copies the source values into blind-data channel 0,
|
||||
# indexed by the per-voxel uint64 indices the OnIndex grid stores.
|
||||
handle = nanovdb.tools.createNanoGridOnIndex(src_grid, channels=1)
|
||||
grid = handle.grid()
|
||||
print(f"OnIndex grid: valueCount={grid.valueCount()}, "
|
||||
f"source activeVoxelCount={src_grid.activeVoxelCount()}")
|
||||
assert grid.valueCount() >= src_grid.activeVoxelCount()
|
||||
|
||||
# createChannelAccessor inspects the channel's recorded dataType and
|
||||
# returns the matching typed accessor (here: OnIndexFloat...).
|
||||
channel = nanovdb.createChannelAccessor(grid, 0)
|
||||
print(f"channel accessor: {type(channel).__name__}, "
|
||||
f"valueCount={channel.valueCount()}")
|
||||
|
||||
src_acc = src_grid.getAccessor()
|
||||
for ijk in (nanovdb.math.Coord(48, 0, 0), nanovdb.math.Coord(0, 50, 0),
|
||||
nanovdb.math.Coord(0, 0, 52)):
|
||||
via_channel = channel.getValue(ijk)
|
||||
via_source = src_acc.getValue(ijk)
|
||||
print(f" {ijk}: channel={via_channel:.3f} source={via_source:.3f} "
|
||||
f"(linear offset {channel.getIndex(ijk)})")
|
||||
assert via_channel == via_source
|
||||
|
||||
|
||||
def author_blind_data():
|
||||
try:
|
||||
import numpy as np
|
||||
except ImportError:
|
||||
print("NumPy not found. Skipping the blind-data authoring section.")
|
||||
return
|
||||
src = nanovdb.tools.build.FloatGrid(0.0, "authored",
|
||||
nanovdb.GridClass.Unknown)
|
||||
for i in range(8):
|
||||
src.setValue(nanovdb.math.Coord(i, 0, 0), float(i))
|
||||
|
||||
# Declare a channel at conversion time; it is allocated zero-filled
|
||||
# in the baked grid and filled afterwards through the writable view.
|
||||
conv = nanovdb.tools.CreateNanoGrid(src)
|
||||
ch = conv.addBlindData("temperature", count=64,
|
||||
dataType=nanovdb.GridType.Float)
|
||||
handle = conv.getHandle()
|
||||
grid = handle.grid()
|
||||
print(f"authored grid has {grid.blindDataCount()} blind-data channel(s)")
|
||||
|
||||
view = grid.getBlindData(ch)
|
||||
view[:] = np.linspace(273.0, 373.0, num=64, dtype=np.float32)
|
||||
|
||||
# Re-resolve the channel by name and confirm the writes persisted.
|
||||
n = grid.findBlindData("temperature")
|
||||
meta = grid.blindMetaData(n)
|
||||
stored = grid.getBlindData(n)
|
||||
print(f" {meta.name()!r}: {meta.valueCount} x {meta.dataType}, "
|
||||
f"range [{stored.min():.1f}, {stored.max():.1f}]")
|
||||
assert n == ch
|
||||
assert stored[0] == np.float32(273.0)
|
||||
|
||||
|
||||
def main():
|
||||
index_grid_with_channel()
|
||||
print()
|
||||
author_blind_data()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,103 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Write a bundle of primitive grids to a .nvdb file and read it back.
|
||||
|
||||
Python port of ex_write_nanovdb_grids, ex_read_nanovdb_sphere, and
|
||||
ex_read_nanovdb_sphere_accessor, consolidated into one self-contained
|
||||
round trip: bake five primitives, write them to a single file (with
|
||||
codec fallback), inspect the file metadata without loading the grids,
|
||||
re-read one grid by name and all grids at once, and split a merged
|
||||
handle back apart. The point-sphere section reads the world-space
|
||||
point positions through the zero-copy getBlindData() NumPy view.
|
||||
|
||||
Run with: python io_roundtrip.py
|
||||
"""
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import nanovdb
|
||||
|
||||
|
||||
def write_primitives(path):
|
||||
handles = [
|
||||
nanovdb.tools.createLevelSetSphere(radius=50.0, name="sphere_ls"),
|
||||
nanovdb.tools.createLevelSetTorus(majorRadius=50.0, minorRadius=20.0,
|
||||
name="torus_ls"),
|
||||
nanovdb.tools.createLevelSetBox(width=40.0, height=60.0, depth=80.0,
|
||||
name="box_ls"),
|
||||
nanovdb.tools.createLevelSetBBox(width=40.0, height=60.0, depth=80.0,
|
||||
thickness=10.0, name="bbox_ls"),
|
||||
nanovdb.tools.createPointSphere(pointsPerVoxel=2, radius=50.0,
|
||||
name="sphere_points"),
|
||||
]
|
||||
# BLOSC gives the best compression but is a build-time option; fall
|
||||
# back to an uncompressed file when this module was built without it.
|
||||
try:
|
||||
nanovdb.io.writeGrids(path, handles, codec=nanovdb.io.Codec.BLOSC)
|
||||
codec = "BLOSC"
|
||||
except RuntimeError:
|
||||
nanovdb.io.writeGrids(path, handles, codec=nanovdb.io.Codec.NONE)
|
||||
codec = "NONE"
|
||||
print(f"Wrote {len(handles)} grids to {path} (codec={codec})")
|
||||
|
||||
|
||||
def inspect_file(path):
|
||||
# readGridMetaData parses the per-grid file headers without loading
|
||||
# any voxel data — the cheap way to answer "what's in this file?".
|
||||
for meta in nanovdb.io.readGridMetaData(path):
|
||||
print(f" {meta.gridName!r}: type={meta.gridType}, "
|
||||
f"class={meta.gridClass}, voxels={meta.voxelCount}")
|
||||
assert nanovdb.io.hasGrid(path, "sphere_ls")
|
||||
assert not nanovdb.io.hasGrid(path, "no_such_grid")
|
||||
|
||||
|
||||
def read_back(path):
|
||||
# Read a single grid by name, then print the recognizable
|
||||
# ex_read_nanovdb_sphere_accessor cross-section along the x-axis.
|
||||
handle = nanovdb.io.readGrid(path, "sphere_ls")
|
||||
acc = handle.grid().getAccessor()
|
||||
for i in range(47, 54):
|
||||
ijk = nanovdb.math.Coord(i, 0, 0)
|
||||
print(f" sphere_ls({i},0,0) = {acc.getValue(ijk):.2f}")
|
||||
|
||||
# Read every grid, merge them into one multi-grid handle, and split
|
||||
# that handle back into one handle per grid.
|
||||
handles = nanovdb.io.readGrids(path)
|
||||
merged = nanovdb.mergeGrids(handles)
|
||||
print(f" merged handle holds {merged.gridCount()} grids")
|
||||
parts = nanovdb.splitGrids(merged)
|
||||
assert len(parts) == len(handles)
|
||||
print(f" splitGrids -> {len(parts)} single-grid handles")
|
||||
return handles
|
||||
|
||||
|
||||
def point_positions(handles):
|
||||
try:
|
||||
import numpy as np
|
||||
except ImportError:
|
||||
print("NumPy not found. Skipping the point-positions section.")
|
||||
return
|
||||
# The point sphere stores its world-space positions as a blind-data
|
||||
# channel; getBlindData() exposes it as a zero-copy (N, 3) view.
|
||||
points = next(h.grid() for h in handles
|
||||
if h.grid().gridName() == "sphere_points")
|
||||
positions = points.getBlindData(0)
|
||||
radii = np.linalg.norm(positions, axis=1)
|
||||
print(f" {positions.shape[0]} points, "
|
||||
f"|p| in [{radii.min():.2f}, {radii.max():.2f}]")
|
||||
# Points are jittered within their voxel, so allow ~1.5 voxels.
|
||||
assert np.all(np.abs(radii - 50.0) < 1.5)
|
||||
|
||||
|
||||
def main():
|
||||
out_dir = tempfile.mkdtemp(prefix="nanovdb_")
|
||||
path = os.path.join(out_dir, "primitives.nvdb")
|
||||
write_primitives(path)
|
||||
inspect_file(path)
|
||||
handles = read_back(path)
|
||||
point_positions(handles)
|
||||
print(f"Output left in {out_dir}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,67 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Load a NanoVDB grid and inspect it polymorphically.
|
||||
|
||||
handle.grid(i) returns the correct typed Python subclass for whatever
|
||||
GridType the grid carries, so a single call site can handle a mixed
|
||||
bundle of grid types. This example builds two grids of different
|
||||
value types into one handle, then walks the handle inspecting each
|
||||
grid via the polymorphic accessor plus GridMetaData (the type-erased
|
||||
introspector that answers "what's in this buffer?" without knowing
|
||||
BuildT at compile time).
|
||||
|
||||
Run with: python load_inspect.py
|
||||
"""
|
||||
import nanovdb
|
||||
|
||||
|
||||
def describe_handle(handle):
|
||||
print(f"Handle contains {handle.gridCount()} grid(s).")
|
||||
for i in range(handle.gridCount()):
|
||||
# GridType / gridSize are cheap to query on the handle itself.
|
||||
gtype = handle.gridType(i)
|
||||
gsize = handle.gridSize(i)
|
||||
print(f" [{i}] type={gtype}, size={gsize} bytes")
|
||||
|
||||
# handle.grid(i) returns the matching <Suffix>Grid subclass
|
||||
# at runtime — no isinstance dispatch needed at the call site.
|
||||
# The grid name lives on the grid itself, not the handle.
|
||||
grid = handle.grid(i)
|
||||
print(f" name={grid.gridName()!r}, "
|
||||
f"gridClass={grid.gridClass()}")
|
||||
print(f" activeVoxelCount={grid.activeVoxelCount()}")
|
||||
print(f" worldBBox={grid.worldBBox()}")
|
||||
|
||||
# GridMetaData is the type-erased introspector — answers
|
||||
# "what's in this buffer?" without knowing BuildT.
|
||||
meta = nanovdb.GridMetaData(grid)
|
||||
print(f" gridSize={meta.gridSize()}, "
|
||||
f"isLevelSet={meta.isLevelSet()}, "
|
||||
f"isFogVolume={meta.isFogVolume()}")
|
||||
|
||||
|
||||
def main():
|
||||
# Build two grids of different types into one handle so we can
|
||||
# exercise the polymorphic accessor.
|
||||
h_float = nanovdb.tools.createLevelSetSphere(
|
||||
radius=10.0, name="sphere_float")
|
||||
h_double = nanovdb.tools.createLevelSetSphere(
|
||||
gridType=nanovdb.GridType.Double, radius=10.0, name="sphere_double")
|
||||
handle = nanovdb.mergeGrids([h_float, h_double])
|
||||
|
||||
describe_handle(handle)
|
||||
|
||||
print()
|
||||
print("Polymorphic dispatch from a runtime GridType:")
|
||||
for i in range(handle.gridCount()):
|
||||
grid = handle.grid(i)
|
||||
# Each typed grid carries a getAccessor() that returns the
|
||||
# appropriate <Suffix>ReadAccessor — float for FloatGrid,
|
||||
# double for DoubleGrid, etc.
|
||||
acc = grid.getAccessor()
|
||||
v = acc.getValue(nanovdb.math.Coord(0, 0, 0))
|
||||
print(f" grid[{i}] accessor.getValue(0,0,0) = {v}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,61 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Build a level set from a Python function evaluated at every voxel.
|
||||
|
||||
Python port of ex_make_funny_nanovdb: a trigonometric interference
|
||||
pattern is CSG-intersected with a sphere and clamped to a narrow band,
|
||||
using the functor-based nanovdb.tools.createFloatGrid factory. The
|
||||
callback is one Python call per voxel in the bbox, so this port uses a
|
||||
65^3 domain (~275k calls, a few seconds) where the C++ original fills
|
||||
[-500,500]^3 — scale `SIZE` up only if you are willing to wait.
|
||||
|
||||
Run with: python make_funny_nanovdb.py
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import nanovdb
|
||||
|
||||
SIZE = 32 # half-width of the cubic domain, in voxels
|
||||
BACKGROUND = 5.0 # narrow-band half-width, in world units
|
||||
FREQ = 0.8 # rescaled from the C++ 0.1 to fit the smaller domain
|
||||
|
||||
|
||||
def funny(ijk):
|
||||
v = 4.0 + 5.0 * (math.cos(ijk.x * FREQ) * math.sin(ijk.y * FREQ) +
|
||||
math.cos(ijk.y * FREQ) * math.sin(ijk.z * FREQ) +
|
||||
math.cos(ijk.z * FREQ) * math.sin(ijk.x * FREQ))
|
||||
# CSG intersection with a sphere of radius SIZE.
|
||||
r = math.sqrt(ijk.x ** 2 + ijk.y ** 2 + ijk.z ** 2)
|
||||
v = max(v, r - SIZE)
|
||||
# Clamp to the narrow band.
|
||||
return max(-BACKGROUND, min(BACKGROUND, v))
|
||||
|
||||
|
||||
def main():
|
||||
bbox = nanovdb.math.CoordBBox(nanovdb.math.Coord(-SIZE),
|
||||
nanovdb.math.Coord(SIZE))
|
||||
print(f"Evaluating funny() over {bbox} ...")
|
||||
handle = nanovdb.tools.createFloatGrid(
|
||||
BACKGROUND, "funny", nanovdb.GridClass.LevelSet, funny, bbox)
|
||||
|
||||
grid = handle.grid()
|
||||
print(f"activeVoxelCount = {grid.activeVoxelCount()}")
|
||||
acc = grid.getAccessor()
|
||||
probe = nanovdb.math.Coord(0, 0, 0)
|
||||
print(f"value at {probe} = {acc.getValue(probe):.3f}")
|
||||
assert grid.isLevelSet()
|
||||
assert grid.activeVoxelCount() > 0
|
||||
|
||||
out_dir = tempfile.mkdtemp(prefix="nanovdb_")
|
||||
path = os.path.join(out_dir, "funny.nvdb")
|
||||
try:
|
||||
nanovdb.io.writeGrid(path, handle, codec=nanovdb.io.Codec.BLOSC)
|
||||
except RuntimeError:
|
||||
nanovdb.io.writeGrid(path, handle, codec=nanovdb.io.Codec.NONE)
|
||||
print(f"Wrote {path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,72 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Build grids of many value types and read them back polymorphically.
|
||||
|
||||
Python port of ex_make_typed_grids: a small solid sphere is baked once
|
||||
per value type through the matching nanovdb.tools.build.<Suffix>Grid
|
||||
mutable builder, all handles are written to a single .nvdb file, and
|
||||
the file is re-read with handle.grid() returning the correct typed
|
||||
subclass for each grid at runtime.
|
||||
|
||||
Run with: python make_typed_grids.py
|
||||
"""
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import nanovdb
|
||||
|
||||
RADIUS = 8 # voxels; ~2.1k active voxels per grid keeps this quick
|
||||
|
||||
|
||||
def solid_sphere_coords():
|
||||
r2 = RADIUS * RADIUS
|
||||
for i in range(-RADIUS, RADIUS + 1):
|
||||
for j in range(-RADIUS, RADIUS + 1):
|
||||
for k in range(-RADIUS, RADIUS + 1):
|
||||
if i * i + j * j + k * k < r2:
|
||||
yield nanovdb.math.Coord(i, j, k)
|
||||
|
||||
|
||||
def build_typed_grids():
|
||||
# (builder class, grid name, background, voxel value)
|
||||
specs = [
|
||||
(nanovdb.tools.build.FloatGrid, "float_grid", 0.0, 1.0),
|
||||
(nanovdb.tools.build.DoubleGrid, "double_grid", 0.0, 1.0),
|
||||
(nanovdb.tools.build.Int16Grid, "int16_grid", 0, 1),
|
||||
(nanovdb.tools.build.Int32Grid, "int32_grid", 0, 1),
|
||||
(nanovdb.tools.build.Int64Grid, "int64_grid", 0, 1),
|
||||
(nanovdb.tools.build.UInt32Grid, "uint32_grid", 0, 1),
|
||||
(nanovdb.tools.build.Vec3fGrid, "vec3f_grid",
|
||||
nanovdb.math.Vec3f(0.0), nanovdb.math.Vec3f(1.0, 0.0, 0.0)),
|
||||
]
|
||||
handles = []
|
||||
for cls, name, background, value in specs:
|
||||
grid = cls(background, name, nanovdb.GridClass.Unknown)
|
||||
for ijk in solid_sphere_coords():
|
||||
grid.setValue(ijk, value)
|
||||
handles.append(grid.to_nanovdb())
|
||||
print(f"built {name} ({cls.__name__})")
|
||||
return handles
|
||||
|
||||
|
||||
def main():
|
||||
handles = build_typed_grids()
|
||||
|
||||
out_dir = tempfile.mkdtemp(prefix="nanovdb_")
|
||||
path = os.path.join(out_dir, "custom_types.nvdb")
|
||||
nanovdb.io.writeGrids(path, handles)
|
||||
print(f"Wrote {len(handles)} grids to {path}")
|
||||
|
||||
# Re-read and dispatch: handle.grid() returns FloatGrid, Int16Grid,
|
||||
# Vec3fGrid, ... according to the GridType each grid carries.
|
||||
probe = nanovdb.math.Coord(0, 0, 0)
|
||||
for handle in nanovdb.io.readGrids(path):
|
||||
grid = handle.grid()
|
||||
value = grid.getAccessor().getValue(probe)
|
||||
print(f" {grid.gridName():<12} -> {type(grid).__name__:<12} "
|
||||
f"active={grid.activeVoxelCount()} value(0,0,0)={value}")
|
||||
assert grid.activeVoxelCount() > 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,50 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Iterate a grid's nodes linearly through a NodeManager.
|
||||
|
||||
Python port of the host half of ex_nodemanager_cuda: createNodeManager
|
||||
builds linearized arrays of the tree's leaf, lower, and upper nodes so
|
||||
they can be visited by index instead of by tree traversal. Each node
|
||||
exposes its origin, per-node stats, and (on leaves) the raw 512-value
|
||||
buffer. For bulk NumPy analytics over every leaf at once, see
|
||||
bulk_leaf_numpy.py's grid.leaf_values() instead.
|
||||
|
||||
Run with: python node_manager.py
|
||||
"""
|
||||
import nanovdb
|
||||
|
||||
|
||||
def main():
|
||||
handle = nanovdb.tools.createLevelSetSphere(radius=50.0, name="sphere")
|
||||
grid = handle.grid()
|
||||
tree = grid.tree()
|
||||
|
||||
nmh = nanovdb.createNodeManager(grid)
|
||||
nm = nmh.mgr()
|
||||
print(f"NodeManager over {grid.gridName()!r} (linear={nm.isLinear()}):")
|
||||
print(f" leaves={nm.leafCount()}, lower={nm.lowerCount()}, "
|
||||
f"upper={nm.upperCount()}")
|
||||
|
||||
# The counts mirror the tree's per-level node counts.
|
||||
assert nm.leafCount() == tree.nodeCount(0)
|
||||
assert nm.lowerCount() == tree.nodeCount(1)
|
||||
assert nm.upperCount() == tree.nodeCount(2)
|
||||
|
||||
# Linear access agrees with tree traversal.
|
||||
assert nm.leaf(0).origin() == tree.getFirstLeaf().origin()
|
||||
|
||||
# Visit a few leaves by index: origin, activity, and value range.
|
||||
for i in range(min(5, nm.leafCount())):
|
||||
leaf = nm.leaf(i)
|
||||
print(f" leaf[{i}] origin={leaf.origin()} "
|
||||
f"on={leaf.valueMask().countOn()} "
|
||||
f"min={leaf.minimum():.3f} max={leaf.maximum():.3f}")
|
||||
|
||||
# Internal nodes are reachable the same way.
|
||||
lower = nm.lower(0)
|
||||
print(f" lower[0] origin={lower.origin()} "
|
||||
f"children={lower.childMask().countOn()}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,53 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Round-trip a grid between OpenVDB and NanoVDB.
|
||||
|
||||
Python port of ex_openvdb_to_nanovdb and ex_openvdb_to_nanovdb_accessor:
|
||||
an OpenVDB level-set sphere is converted to NanoVDB with openToNanoVDB,
|
||||
values are compared through both libraries' accessors, and the NanoVDB
|
||||
grid is converted back with nanoToOpenVDB. Requires a build with
|
||||
NANOVDB_USE_OPENVDB and an importable `openvdb` module; the script
|
||||
skips (exit 0) when either is missing.
|
||||
|
||||
Run with: python openvdb_interop.py
|
||||
"""
|
||||
import nanovdb
|
||||
|
||||
|
||||
def main():
|
||||
if not hasattr(nanovdb.tools, "openToNanoVDB"):
|
||||
print("nanovdb was built without NANOVDB_USE_OPENVDB. Skipping...")
|
||||
return
|
||||
try:
|
||||
import openvdb
|
||||
except ImportError:
|
||||
print("openvdb not found. Skipping...")
|
||||
return
|
||||
|
||||
sphere = openvdb.createLevelSetSphere(100.0)
|
||||
sphere.name = "sphere"
|
||||
handle = nanovdb.tools.openToNanoVDB(sphere)
|
||||
grid = handle.grid()
|
||||
print(f"openToNanoVDB: {grid.gridName()!r}, class={grid.gridClass()}, "
|
||||
f"active={grid.activeVoxelCount()}")
|
||||
assert grid.gridClass() == nanovdb.GridClass.LevelSet
|
||||
|
||||
# Compare a cross-section through both accessors, as the C++
|
||||
# accessor example does.
|
||||
open_acc = sphere.getAccessor()
|
||||
nano_acc = grid.getAccessor()
|
||||
for i in range(97, 104):
|
||||
open_v = open_acc.getValue((i, 0, 0))
|
||||
nano_v = nano_acc.getValue(nanovdb.math.Coord(i, 0, 0))
|
||||
print(f" ({i},0,0): openvdb={open_v:.3f} nanovdb={nano_v:.3f}")
|
||||
assert abs(open_v - nano_v) < 1e-5
|
||||
|
||||
# And back: NanoVDB -> OpenVDB.
|
||||
back = nanovdb.tools.nanoToOpenVDB(handle)
|
||||
print(f"nanoToOpenVDB: {back.name!r}, empty={back.empty()}")
|
||||
assert back.name == "sphere"
|
||||
assert not back.empty()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,59 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Quantize a NanoGrid<float> down to a quantized BuildT.
|
||||
|
||||
nanovdb.tools.createNanoGridFp4 / Fp8 / Fp16 / FpN are the quantized
|
||||
counterparts of the regular grid types. Fp4 / Fp8 / Fp16 use a fixed
|
||||
bit-width per voxel. FpN picks the bit-width per leaf so each leaf
|
||||
hits a user-supplied tolerance (the "oracle"). Smaller bit widths
|
||||
give smaller files at the cost of approximation error.
|
||||
|
||||
Run with: python quantize.py
|
||||
"""
|
||||
import nanovdb
|
||||
|
||||
|
||||
def gridSize_in_kb(handle):
|
||||
return handle.gridSize(0) / 1024
|
||||
|
||||
|
||||
def main():
|
||||
# Source: a 50-radius sphere fog volume at full float precision.
|
||||
src_handle = nanovdb.tools.createFogVolumeSphere(radius=50.0)
|
||||
src_grid = src_handle.grid()
|
||||
print(f"Source FloatGrid: {gridSize_in_kb(src_handle):.1f} KB, "
|
||||
f"active voxels = {src_grid.activeVoxelCount()}")
|
||||
|
||||
# Fixed-width quantization. Each subsequent format roughly halves
|
||||
# the per-voxel storage cost; dithering optional.
|
||||
for fn_name, label in [
|
||||
("createNanoGridFp16", "Fp16 (16-bit fixed)"),
|
||||
("createNanoGridFp8", "Fp8 (8-bit fixed)"),
|
||||
("createNanoGridFp4", "Fp4 (4-bit fixed)"),
|
||||
]:
|
||||
h = getattr(nanovdb.tools, fn_name)(src_grid, ditherOn=True)
|
||||
print(f" {label}: {gridSize_in_kb(h):.1f} KB, "
|
||||
f"active voxels = {h.grid().activeVoxelCount()}")
|
||||
|
||||
# Variable-width FpN. The oracle picks the per-leaf bit-width to
|
||||
# meet a tolerance — AbsDiff for absolute error, RelDiff for
|
||||
# relative error. -1 tolerance means "uninitialized" so we pass
|
||||
# an explicit value.
|
||||
abs_oracle = nanovdb.tools.AbsDiff(0.05) # ±0.05 per voxel
|
||||
h_fpn_abs = nanovdb.tools.createNanoGridFpN(src_grid, abs_oracle)
|
||||
print(f" FpN (AbsDiff 0.05): {gridSize_in_kb(h_fpn_abs):.1f} KB")
|
||||
|
||||
rel_oracle = nanovdb.tools.RelDiff(0.1) # 10% relative error
|
||||
h_fpn_rel = nanovdb.tools.createNanoGridFpN(src_grid, rel_oracle)
|
||||
print(f" FpN (RelDiff 0.10): {gridSize_in_kb(h_fpn_rel):.1f} KB")
|
||||
|
||||
# The output is a regular NanoGrid<FpN> — read-only, but exposes
|
||||
# the standard surface (gridType, activeVoxelCount, accessor.getValue
|
||||
# returning a decoded float, etc).
|
||||
fpn = h_fpn_abs.grid()
|
||||
print(f"\nFpN grid type: {fpn.gridType()}, "
|
||||
f"voxel at (0,0,0) = {fpn.getAccessor().getValue(nanovdb.math.Coord(0, 0, 0))}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,95 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Ray-march a fog volume on the CPU and write a PGM image.
|
||||
|
||||
Python port of the host path of ex_raytrace_fog_volume: each ray is
|
||||
clipped to the grid's index bounding box, then integrated with fixed
|
||||
steps, accumulating transmittance from the density sampled through a
|
||||
ReadAccessor at Coord.Floor of the march position — the accessor-based
|
||||
idiom, in contrast to the sampler-based raytrace_level_set.py. Every
|
||||
step is one Python-to-C++ call, so the default RES is modest; raise it
|
||||
for a nicer image.
|
||||
|
||||
Run with: python raytrace_fog_volume.py
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import nanovdb
|
||||
|
||||
RES = 64 # image is RES x RES pixels
|
||||
FOV_DEG = 45.0
|
||||
DT = 1.0 # march step, in voxels
|
||||
SIGMA = 0.2 # extinction scale applied to the sampled density
|
||||
|
||||
|
||||
def clip_to_bbox(eye, direction, bbox):
|
||||
"""Slab-clip a ray against a CoordBBox; returns (t0, t1) or None."""
|
||||
t0, t1 = 0.0, math.inf
|
||||
for axis in range(3):
|
||||
lo = bbox.min[axis] - eye[axis]
|
||||
hi = bbox.max[axis] + 1.0 - eye[axis]
|
||||
d = direction[axis]
|
||||
if abs(d) < 1e-12:
|
||||
if lo > 0.0 or hi < 0.0:
|
||||
return None
|
||||
continue
|
||||
near, far = lo / d, hi / d
|
||||
if near > far:
|
||||
near, far = far, near
|
||||
t0, t1 = max(t0, near), min(t1, far)
|
||||
if t0 > t1:
|
||||
return None
|
||||
return t0, t1
|
||||
|
||||
|
||||
def main():
|
||||
handle = nanovdb.tools.createFogVolumeSphere(radius=50.0, name="fog")
|
||||
grid = handle.grid()
|
||||
bbox = grid.indexBBox()
|
||||
acc = grid.getAccessor()
|
||||
|
||||
# Perspective camera looking down -z, as in the C++ RayGenOp,
|
||||
# working directly in index space (the grid transform is uniform).
|
||||
dim = [bbox.max[i] + 1 - bbox.min[i] for i in range(3)]
|
||||
center = [bbox.min[i] + 0.5 * dim[i] for i in range(3)]
|
||||
eye = (center[0], center[1], center[2] + 2.0 * dim[2])
|
||||
tan_fov = math.tan(math.radians(FOV_DEG) * 0.5)
|
||||
|
||||
pixels = bytearray(RES * RES)
|
||||
for y in range(RES):
|
||||
for x in range(RES):
|
||||
px = (2.0 * (x + 0.5) / RES - 1.0) * tan_fov
|
||||
py = (2.0 * (y + 0.5) / RES - 1.0) * tan_fov
|
||||
norm = math.sqrt(px * px + py * py + 1.0)
|
||||
direction = (px / norm, py / norm, -1.0 / norm)
|
||||
|
||||
span = clip_to_bbox(eye, direction, bbox)
|
||||
if span is None:
|
||||
continue
|
||||
transmittance = 1.0
|
||||
t = span[0]
|
||||
while t < span[1]:
|
||||
pos = nanovdb.math.Vec3d(eye[0] + t * direction[0],
|
||||
eye[1] + t * direction[1],
|
||||
eye[2] + t * direction[2])
|
||||
density = acc.getValue(nanovdb.math.Coord.Floor(pos))
|
||||
transmittance *= 1.0 - density * SIGMA * DT
|
||||
if transmittance < 0.005:
|
||||
break
|
||||
t += DT
|
||||
pixels[y * RES + x] = int(255 * (1.0 - transmittance))
|
||||
|
||||
out_dir = tempfile.mkdtemp(prefix="nanovdb_")
|
||||
path = os.path.join(out_dir, "raytrace_fog_volume.pgm")
|
||||
with open(path, "wb") as f:
|
||||
f.write(f"P5\n{RES} {RES}\n255\n".encode("ascii"))
|
||||
f.write(bytes(pixels))
|
||||
lit = sum(1 for p in pixels if p > 0)
|
||||
print(f"Rendered {RES}x{RES} image, {lit} foggy pixels -> {path}")
|
||||
assert lit > 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,120 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Ray-trace a narrow-band level set on the CPU and write a PGM image.
|
||||
|
||||
Python port of the host path of ex_raytrace_level_set. The C++ example
|
||||
finds surface hits with math::Ray + HDDA ZeroCrossing, which are not
|
||||
bound in Python; this port re-expresses the search as sphere tracing —
|
||||
the clamped SDF value itself bounds the safe step size — using the
|
||||
trilinear sampler, with sampler.zeroCrossing() confirming the interval
|
||||
and sampler.gradient() shading the hit. Every sample is one
|
||||
Python-to-C++ call, so the default RES is modest; raise it for a nicer
|
||||
image.
|
||||
|
||||
Run with: python raytrace_level_set.py
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import nanovdb
|
||||
|
||||
RES = 64 # image is RES x RES pixels
|
||||
FOV_DEG = 45.0
|
||||
LIGHT = (0.577, 0.577, 0.577) # unit vector toward the light
|
||||
|
||||
|
||||
def clip_to_bbox(eye, direction, bbox):
|
||||
"""Slab-clip a ray against a CoordBBox; returns (t0, t1) or None."""
|
||||
t0, t1 = 0.0, math.inf
|
||||
for axis in range(3):
|
||||
lo = bbox.min[axis] - eye[axis]
|
||||
hi = bbox.max[axis] + 1.0 - eye[axis]
|
||||
d = direction[axis]
|
||||
if abs(d) < 1e-12:
|
||||
if lo > 0.0 or hi < 0.0:
|
||||
return None
|
||||
continue
|
||||
near, far = lo / d, hi / d
|
||||
if near > far:
|
||||
near, far = far, near
|
||||
t0, t1 = max(t0, near), min(t1, far)
|
||||
if t0 > t1:
|
||||
return None
|
||||
return t0, t1
|
||||
|
||||
|
||||
def trace(sampler, eye, direction, t0, t1, voxel_size):
|
||||
"""Sphere-trace from t0 to t1; returns (hit t, crossing seen) or None."""
|
||||
t = t0
|
||||
while t < t1:
|
||||
pos = nanovdb.math.Vec3d(eye[0] + t * direction[0],
|
||||
eye[1] + t * direction[1],
|
||||
eye[2] + t * direction[2])
|
||||
d = sampler(pos) # world-unit SDF, clamped to the narrow band
|
||||
if d <= 0.0:
|
||||
# zeroCrossing() reports whether the reconstruction stencil
|
||||
# here straddles the iso-surface — the sampler-level analog
|
||||
# of the HDDA ZeroCrossing test in the C++ example.
|
||||
return t, sampler.zeroCrossing(pos)
|
||||
# The clamped SDF bounds the distance to the surface, so it is
|
||||
# a safe (index-space) step size; never step below half a voxel.
|
||||
t += max(d / voxel_size, 0.5)
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
handle = nanovdb.tools.createLevelSetSphere(radius=100.0, name="sphere")
|
||||
grid = handle.grid()
|
||||
bbox = grid.indexBBox()
|
||||
voxel_size = grid.voxelSize()[0]
|
||||
sampler = nanovdb.math.createTrilinearSampler(grid)
|
||||
|
||||
# Perspective camera looking down -z, as in the C++ RayGenOp,
|
||||
# working directly in index space (the grid transform is uniform).
|
||||
dim = [bbox.max[i] + 1 - bbox.min[i] for i in range(3)]
|
||||
center = [bbox.min[i] + 0.5 * dim[i] for i in range(3)]
|
||||
eye = nanovdb.math.Vec3d(center[0], center[1], center[2] + 2.0 * dim[2])
|
||||
tan_fov = math.tan(math.radians(FOV_DEG) * 0.5)
|
||||
|
||||
pixels = bytearray(RES * RES)
|
||||
crossings = 0
|
||||
for y in range(RES):
|
||||
for x in range(RES):
|
||||
px = (2.0 * (x + 0.5) / RES - 1.0) * tan_fov
|
||||
py = (2.0 * (y + 0.5) / RES - 1.0) * tan_fov
|
||||
norm = math.sqrt(px * px + py * py + 1.0)
|
||||
direction = (px / norm, py / norm, -1.0 / norm)
|
||||
|
||||
span = clip_to_bbox(eye, direction, bbox)
|
||||
if span is None:
|
||||
continue
|
||||
hit = trace(sampler, eye, direction, span[0], span[1],
|
||||
voxel_size)
|
||||
if hit is None:
|
||||
continue
|
||||
t_hit, crossed = hit
|
||||
crossings += crossed
|
||||
pos = nanovdb.math.Vec3d(eye[0] + t_hit * direction[0],
|
||||
eye[1] + t_hit * direction[1],
|
||||
eye[2] + t_hit * direction[2])
|
||||
# gradient() is in index-space units — normalize before use.
|
||||
n = sampler.gradient(pos)
|
||||
n.normalize()
|
||||
shade = max(0.0, n[0] * LIGHT[0] + n[1] * LIGHT[1]
|
||||
+ n[2] * LIGHT[2])
|
||||
pixels[y * RES + x] = int(255 * shade)
|
||||
|
||||
out_dir = tempfile.mkdtemp(prefix="nanovdb_")
|
||||
path = os.path.join(out_dir, "raytrace_level_set.pgm")
|
||||
with open(path, "wb") as f:
|
||||
f.write(f"P5\n{RES} {RES}\n255\n".encode("ascii"))
|
||||
f.write(bytes(pixels))
|
||||
lit = sum(1 for p in pixels if p > 0)
|
||||
print(f"Rendered {RES}x{RES} image, {lit} lit pixels "
|
||||
f"({crossings} confirmed zero-crossings) -> {path}")
|
||||
assert lit > 0 and crossings > 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,61 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Validate a NanoVDB grid and round-trip its checksum.
|
||||
|
||||
nanovdb.tools exposes the grid-quality surface: validateGrid (single
|
||||
grid), checkGrid -> (ok, error), isValid, plus the checksum helpers
|
||||
evalChecksum / validateChecksum / updateChecksum. This example walks
|
||||
through a typical "load and verify" workflow.
|
||||
|
||||
Run with: python validate.py
|
||||
"""
|
||||
import nanovdb
|
||||
|
||||
|
||||
def main():
|
||||
# A well-formed grid: createLevelSetSphere returns a NanoGrid<float>
|
||||
# with stats and checksum populated to the defaults.
|
||||
handle = nanovdb.tools.createLevelSetSphere(radius=10.0)
|
||||
grid = handle.grid()
|
||||
|
||||
# Whole-handle validation. validateGrids returns a single bool.
|
||||
# The verbose=True flag prints failure details to std::cerr
|
||||
# (visible from Python via std::cerr -> sys.stderr on most stdlibs).
|
||||
all_ok = nanovdb.tools.validateGrids(
|
||||
handle, nanovdb.CheckMode.Default, verbose=False)
|
||||
print(f"validateGrids(handle, Default) = {all_ok}")
|
||||
|
||||
# Per-grid validation, with a helpful message on failure.
|
||||
one_ok = nanovdb.tools.validateGrid(handle, 0, nanovdb.CheckMode.Full)
|
||||
print(f"validateGrid(handle, 0, Full) = {one_ok}")
|
||||
|
||||
# validateGrid returns False (no raise) on out-of-range gridID;
|
||||
# CheckMode.Disable short-circuits and always returns True.
|
||||
print(f"validateGrid(handle, 99) = "
|
||||
f"{nanovdb.tools.validateGrid(handle, 99)} (out of range)")
|
||||
print(f"validateGrid(handle, 99, Disable) = "
|
||||
f"{nanovdb.tools.validateGrid(handle, 99, nanovdb.CheckMode.Disable)}"
|
||||
f" (Disable short-circuit)")
|
||||
|
||||
# tools.checkGrid returns the structural check result plus a
|
||||
# human-readable error message (empty on success).
|
||||
ok, msg = nanovdb.tools.checkGrid(grid, nanovdb.CheckMode.Full)
|
||||
print(f"checkGrid(grid, Full) = ok={ok}, msg={msg!r}")
|
||||
|
||||
# tools.isValid is checkGrid + checksum verification, returning
|
||||
# one bool.
|
||||
print(f"isValid(grid, Default) = "
|
||||
f"{nanovdb.tools.isValid(grid, nanovdb.CheckMode.Default)}")
|
||||
|
||||
# Checksum round-trip. evalChecksum is non-mutating; updateChecksum
|
||||
# writes back into the grid header.
|
||||
cs1 = nanovdb.tools.evalChecksum(grid, nanovdb.CheckMode.Full)
|
||||
nanovdb.tools.updateChecksum(grid, nanovdb.CheckMode.Full)
|
||||
cs2 = nanovdb.tools.evalChecksum(grid, nanovdb.CheckMode.Full)
|
||||
print(f"evalChecksum equal after no-op update: {cs1 == cs2}")
|
||||
print(f"validateChecksum(grid, Full) = "
|
||||
f"{nanovdb.tools.validateChecksum(grid, nanovdb.CheckMode.Full)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Smoke test: run every example script and require a clean exit.
|
||||
|
||||
The examples are self-contained and degrade gracefully (exit 0 with a
|
||||
skip message) when an optional dependency such as NumPy or OpenVDB is
|
||||
missing, so a non-zero exit or an uncaught exception always indicates
|
||||
a real breakage — typically the bindings drifting under the examples.
|
||||
|
||||
The script list is deliberately explicit rather than a glob so that a
|
||||
missing listed example fails loudly and unrelated files placed under
|
||||
examples/ are not executed.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
EXAMPLES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
os.pardir, "examples")
|
||||
|
||||
EXAMPLE_SCRIPTS = [
|
||||
"build_grid.py",
|
||||
"bulk_leaf_numpy.py",
|
||||
"collide_level_set.py",
|
||||
"index_grid_channels.py",
|
||||
"io_roundtrip.py",
|
||||
"load_inspect.py",
|
||||
"make_funny_nanovdb.py",
|
||||
"make_typed_grids.py",
|
||||
"node_manager.py",
|
||||
"openvdb_interop.py",
|
||||
"quantize.py",
|
||||
"raytrace_fog_volume.py",
|
||||
"raytrace_level_set.py",
|
||||
"validate.py",
|
||||
]
|
||||
|
||||
|
||||
class TestExamples(unittest.TestCase):
|
||||
pass
|
||||
|
||||
|
||||
def _make_test(script_name):
|
||||
def test(self):
|
||||
script = os.path.join(EXAMPLES_DIR, script_name)
|
||||
self.assertTrue(os.path.isfile(script),
|
||||
f"example script is missing: {script}")
|
||||
command = [sys.executable, script]
|
||||
env = os.environ.copy()
|
||||
# Preserve output up to a native crash and ask Python to report the
|
||||
# active frame for fatal signals and Windows exceptions.
|
||||
env["PYTHONFAULTHANDLER"] = "1"
|
||||
env["PYTHONUNBUFFERED"] = "1"
|
||||
if hasattr(os, "add_dll_directory"):
|
||||
# Match TestNanoVDB.py's in-tree OpenVDB DLL lookup. The child
|
||||
# starts in a temporary directory, so resolve this while the
|
||||
# parent is still in the CMake configuration directory.
|
||||
config = os.path.basename(os.getcwd())
|
||||
openvdb_dll_directory = os.path.abspath(os.path.join(
|
||||
os.getcwd(), os.pardir, os.pardir, os.pardir, os.pardir,
|
||||
"openvdb", "openvdb", config))
|
||||
dll_directories = [openvdb_dll_directory]
|
||||
cuda_root = env.get("CUDA_PATH")
|
||||
if cuda_root:
|
||||
dll_directories.append(os.path.join(cuda_root, "bin"))
|
||||
env["NANOVDB_TEST_DLL_DIRECTORIES"] = os.pathsep.join(
|
||||
dll_directories)
|
||||
# Python imports sitecustomize during startup. Add this test
|
||||
# directory to the child's module path so sitecustomize.py can
|
||||
# register only the required OpenVDB and CUDA DLL directories
|
||||
# before the example imports nanovdb, while still running the
|
||||
# example as a normal script.
|
||||
test_directory = os.path.dirname(os.path.abspath(__file__))
|
||||
env["PYTHONPATH"] = os.pathsep.join(
|
||||
(test_directory, env.get("PYTHONPATH", "")))
|
||||
# A fresh cwd per run keeps any output files out of the source
|
||||
# tree (the examples themselves also write to tempdirs).
|
||||
result = subprocess.run(
|
||||
command,
|
||||
cwd=tempfile.mkdtemp(prefix="nanovdb_example_"),
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
)
|
||||
self.assertEqual(
|
||||
result.returncode, 0,
|
||||
f"{script_name} exited with {result.returncode}\n"
|
||||
f"--- stdout ---\n{result.stdout}\n"
|
||||
f"--- stderr ---\n{result.stderr}")
|
||||
|
||||
return test
|
||||
|
||||
|
||||
for _name in EXAMPLE_SCRIPTS:
|
||||
_test_name = "test_" + _name.replace(".py", "")
|
||||
setattr(TestExamples, _test_name, _make_test(_name))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Configure dependent-DLL lookup for in-tree Windows Python tests."""
|
||||
|
||||
import os
|
||||
|
||||
|
||||
# Keep these handles alive for the lifetime of the child Python process.
|
||||
_dll_directory_handles = []
|
||||
|
||||
if hasattr(os, "add_dll_directory"):
|
||||
for _path in os.environ.get(
|
||||
"NANOVDB_TEST_DLL_DIRECTORIES", "").split(os.pathsep):
|
||||
if os.path.isdir(_path):
|
||||
try:
|
||||
_dll_directory_handles.append(os.add_dll_directory(_path))
|
||||
except OSError:
|
||||
pass
|
||||
@@ -37,6 +37,21 @@ private:
|
||||
uint32_t* mNodeCounts;
|
||||
};
|
||||
|
||||
/// @brief Indicator functor that returns 1 if the input value matches the member value, 0 otherwise
|
||||
template <typename T, typename std::enable_if<std::is_integral<T>::value>::type* = nullptr>
|
||||
struct EqualityIndicator
|
||||
{
|
||||
EqualityIndicator(const T* value) : mValue(value) {}
|
||||
|
||||
__hostdev__
|
||||
T operator()(const T& x) const
|
||||
{
|
||||
return x == (*mValue);
|
||||
}
|
||||
private:
|
||||
const T* mValue;
|
||||
};
|
||||
|
||||
/// @brief Implements the merge path binary search algorithm in order to find the median across two sorted input key arrays
|
||||
template<typename KeyIteratorIn>
|
||||
__device__
|
||||
@@ -103,6 +118,34 @@ void mergePathKernel(KeyIteratorIn keys1, size_t keys1Count, KeyIteratorIn keys2
|
||||
mergePath(keys1, keys1Count, keys2, keys2Count, key1Intervals, key2Intervals, intervalIndex);
|
||||
}
|
||||
|
||||
/// @brief Extends or shortens the left end of an array interval
|
||||
template<typename DistanceIteratorIn, typename CountIteratorOut, typename OffsetIteratorOut>
|
||||
__global__
|
||||
void leftRebalanceKernel(DistanceIteratorIn leftDistance, DistanceIteratorIn rightDistance, CountIteratorOut leftCount, OffsetIteratorOut leftOffset)
|
||||
{
|
||||
if (*leftDistance < *rightDistance) {
|
||||
*leftCount -= *leftDistance;
|
||||
}
|
||||
else {
|
||||
*leftCount += *rightDistance;
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief Extends or shortens the right end of an array interval
|
||||
template<typename DistanceIteratorIn, typename CountIteratorOut, typename OffsetIteratorOut>
|
||||
__global__
|
||||
void rightRebalanceKernel(DistanceIteratorIn leftDistance, DistanceIteratorIn rightDistance, CountIteratorOut rightCount, OffsetIteratorOut rightOffset)
|
||||
{
|
||||
if (*leftDistance < *rightDistance) {
|
||||
*rightCount += *leftDistance;
|
||||
*rightOffset -= *leftDistance;
|
||||
}
|
||||
else {
|
||||
*rightCount -= *rightDistance;
|
||||
*rightOffset += *rightDistance;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace kernels
|
||||
|
||||
// Define utility macro used to call cub functions that use dynamic temporary storage
|
||||
@@ -517,6 +560,8 @@ void DistributedPointsToGrid<BuildT>::countNodes(const PtrT coords, size_t coord
|
||||
// to reduce overhead.
|
||||
std::vector<cudaEvent_t> sortEvents(mDeviceMesh.deviceCount());
|
||||
std::vector<cudaEvent_t> runLengthEncodeEvents(mDeviceMesh.deviceCount());
|
||||
std::vector<cudaEvent_t> transformReduceEvents(mDeviceMesh.deviceCount());
|
||||
std::vector<cudaEvent_t> rebalanceEvents(mDeviceMesh.deviceCount());
|
||||
std::vector<cudaEvent_t> tilePrefixSumEvents(mDeviceMesh.deviceCount());
|
||||
std::vector<cudaEvent_t> voxelCountEvents(mDeviceMesh.deviceCount());
|
||||
std::vector<cudaEvent_t> leafCountEvents(mDeviceMesh.deviceCount());
|
||||
@@ -527,6 +572,8 @@ void DistributedPointsToGrid<BuildT>::countNodes(const PtrT coords, size_t coord
|
||||
cudaCheck(cudaSetDevice(deviceId));
|
||||
cudaEventCreateWithFlags(&sortEvents[deviceId], cudaEventDisableTiming);
|
||||
cudaEventCreateWithFlags(&runLengthEncodeEvents[deviceId], cudaEventDisableTiming);
|
||||
cudaEventCreateWithFlags(&transformReduceEvents[deviceId], cudaEventDisableTiming);
|
||||
cudaEventCreateWithFlags(&rebalanceEvents[deviceId], cudaEventDisableTiming);
|
||||
cudaEventCreateWithFlags(&tilePrefixSumEvents[deviceId], cudaEventDisableTiming);
|
||||
cudaEventCreateWithFlags(&voxelCountEvents[deviceId], cudaEventDisableTiming);
|
||||
cudaEventCreateWithFlags(&leafCountEvents[deviceId], cudaEventDisableTiming);
|
||||
@@ -579,68 +626,60 @@ void DistributedPointsToGrid<BuildT>::countNodes(const PtrT coords, size_t coord
|
||||
|
||||
radixSortAsync(mDeviceMesh, mTempDevicePools, mData->d_keys, mKeys, mData->d_indx, mIndices, coordCount, mIntervals, mStripeOffsets, mStripeCounts, sortEvents.data(), sortEvents.data());
|
||||
|
||||
// Rebalance the device segments so that a device boundary always coincides
|
||||
// with a change in key value. Because TileKeyFunctor assigns identical keys
|
||||
// to every point that falls in the same upper-node "tile", this aligns the
|
||||
// device ownership boundaries with tile boundaries. Downstream construction
|
||||
// assumes each tile (and therefore each lower node, leaf node, and voxel) is
|
||||
// owned by exactly one device; if a tile straddled a boundary, multiple
|
||||
// devices would concurrently build the same leaf and race on its value mask.
|
||||
//
|
||||
// A single tile can span three or more devices (e.g. one dense leaf whose
|
||||
// points are split evenly across the mesh). Adjusting only adjacent pairs of
|
||||
// boundaries cannot consolidate such a tile because a fully-interior device
|
||||
// lies entirely within it, so we compute the boundaries globally and
|
||||
// monotonically. mKeys is globally sorted at this point, so a tile boundary
|
||||
// is simply a position where mKeys changes. Snapping is performed on the host
|
||||
// over the (small) set of device boundaries; fully-interior devices are left
|
||||
// empty, which the rest of the pipeline already handles.
|
||||
// For each segment of sorted keys on each device, we count how many of the leftmost key occur past the left boundary of the segment. The same is done for the rightmost key with the right boundary of the segment.
|
||||
auto leftIntervals = mIntervals;
|
||||
auto rightIntervals = mIntervals + mDeviceMesh.deviceCount();
|
||||
for (const auto& [deviceId, stream] : mDeviceMesh) {
|
||||
cudaCheck(cudaSetDevice(deviceId));
|
||||
cudaCheck(cudaStreamSynchronize(stream));
|
||||
}
|
||||
|
||||
{
|
||||
const int deviceCount = static_cast<int>(mDeviceMesh.deviceCount());
|
||||
const ptrdiff_t keyCount = static_cast<ptrdiff_t>(coordCount);
|
||||
ptrdiff_t previousBoundary = mStripeOffsets[0]; // device 0 always starts at 0
|
||||
for (int deviceId = 1; deviceId < deviceCount; ++deviceId) {
|
||||
ptrdiff_t boundary = mStripeOffsets[deviceId];
|
||||
if (boundary >= keyCount) {
|
||||
boundary = keyCount;
|
||||
} else if (boundary > previousBoundary && mKeys[boundary] == mKeys[boundary - 1]) {
|
||||
// The even-split boundary falls inside a tile run; find its extent
|
||||
// and snap to whichever end keeps the boundary closest to the
|
||||
// even split without crossing the previous boundary.
|
||||
ptrdiff_t runStart = boundary;
|
||||
while (runStart > previousBoundary && mKeys[runStart - 1] == mKeys[boundary]) --runStart;
|
||||
ptrdiff_t runEnd = boundary;
|
||||
while (runEnd < keyCount && mKeys[runEnd] == mKeys[boundary]) ++runEnd;
|
||||
if (runStart <= previousBoundary) {
|
||||
boundary = runEnd; // the run reaches the previous device, give the whole tile away
|
||||
} else {
|
||||
boundary = (boundary - runStart <= runEnd - boundary) ? runStart : runEnd;
|
||||
}
|
||||
}
|
||||
if (boundary < previousBoundary) boundary = previousBoundary;
|
||||
mStripeOffsets[deviceId] = boundary;
|
||||
previousBoundary = boundary;
|
||||
auto deviceStripeCount = mStripeCounts[deviceId];
|
||||
auto deviceStripeOffset = mStripeOffsets[deviceId];
|
||||
uint64_t* deviceInputKeys = mKeys + deviceStripeOffset;
|
||||
|
||||
if (deviceId > 0) {
|
||||
cudaStreamWaitEvent(stream, sortEvents[deviceId - 1]);
|
||||
EqualityIndicator<uint64_t> indicator(deviceInputKeys - 1);
|
||||
CUB_LAUNCH(DeviceReduce::TransformReduce, mTempDevicePools[deviceId], stream, deviceInputKeys, rightIntervals + deviceId, deviceStripeCount, ::cuda::std::plus(), indicator, 0);
|
||||
}
|
||||
else {
|
||||
rightIntervals[deviceId] = 0;
|
||||
}
|
||||
|
||||
// Recompute the per-device counts from the adjusted, monotonic offsets.
|
||||
for (int deviceId = 0; deviceId < deviceCount; ++deviceId) {
|
||||
const ptrdiff_t nextOffset = (deviceId + 1 < deviceCount)
|
||||
? mStripeOffsets[deviceId + 1]
|
||||
: keyCount;
|
||||
mStripeCounts[deviceId] = static_cast<size_t>(nextOffset - mStripeOffsets[deviceId]);
|
||||
if (deviceId < static_cast<int>(mDeviceMesh.deviceCount() - 1)) {
|
||||
cudaStreamWaitEvent(stream, sortEvents[deviceId + 1]);
|
||||
EqualityIndicator<uint64_t> indicator(deviceInputKeys + deviceStripeCount);
|
||||
CUB_LAUNCH(DeviceReduce::TransformReduce, mTempDevicePools[deviceId], stream, deviceInputKeys, leftIntervals + deviceId, deviceStripeCount, ::cuda::std::plus(), indicator, 0);
|
||||
}
|
||||
else {
|
||||
leftIntervals[deviceId] = 0;
|
||||
}
|
||||
cudaEventRecord(transformReduceEvents[deviceId], stream);
|
||||
}
|
||||
|
||||
// Parallel RLE in order to obtain tiles. The device boundaries were finalized
|
||||
// synchronously on the host above, so no per-device rebalance event is needed.
|
||||
// Rebalance the segments so that a device segment boundary also corresponds to a change in key value. Effectively, this aligns upper node boundaries with device ownership boundaries.
|
||||
for (const auto& [deviceId, stream] : mDeviceMesh) {
|
||||
cudaCheck(cudaSetDevice(deviceId));
|
||||
|
||||
if (deviceId > 0)
|
||||
{
|
||||
cudaStreamWaitEvent(stream, transformReduceEvents[deviceId - 1]);
|
||||
kernels::rightRebalanceKernel<<<1, 1, 0, stream>>>(leftIntervals + deviceId - 1, rightIntervals + deviceId, mStripeCounts + deviceId, mStripeOffsets + deviceId);
|
||||
}
|
||||
|
||||
if (deviceId < static_cast<int>(mDeviceMesh.deviceCount() - 1))
|
||||
{
|
||||
cudaStreamWaitEvent(stream, transformReduceEvents[deviceId + 1]);
|
||||
kernels::leftRebalanceKernel<<<1, 1, 0, stream>>>(leftIntervals + deviceId, rightIntervals + deviceId + 1, mStripeCounts + deviceId, mStripeOffsets + deviceId);
|
||||
}
|
||||
cudaEventRecord(rebalanceEvents[deviceId], stream);
|
||||
}
|
||||
|
||||
// Parallel RLE in order to obtain tiles
|
||||
for (const auto& [deviceId, stream] : mDeviceMesh) {
|
||||
cudaCheck(cudaSetDevice(deviceId));
|
||||
|
||||
cudaCheck(cudaEventSynchronize(rebalanceEvents[deviceId]));
|
||||
|
||||
auto deviceStripeCount = mStripeCounts[deviceId];
|
||||
auto deviceStripeOffset = mStripeOffsets[deviceId];
|
||||
|
||||
@@ -798,6 +837,8 @@ void DistributedPointsToGrid<BuildT>::countNodes(const PtrT coords, size_t coord
|
||||
cudaCheck(cudaSetDevice(deviceId));
|
||||
cudaEventDestroy(sortEvents[deviceId]);
|
||||
cudaEventDestroy(runLengthEncodeEvents[deviceId]);
|
||||
cudaEventDestroy(transformReduceEvents[deviceId]);
|
||||
cudaEventDestroy(rebalanceEvents[deviceId]);
|
||||
cudaEventDestroy(tilePrefixSumEvents[deviceId]);
|
||||
cudaEventDestroy(voxelCountEvents[deviceId]);
|
||||
cudaEventDestroy(leafCountEvents[deviceId]);
|
||||
@@ -874,7 +915,7 @@ inline void DistributedPointsToGrid<BuildT>::processGridTreeRoot(const PtrT poin
|
||||
} else {
|
||||
cudaCheck(cudaMemsetAsync(dst, 0, GridData::MaxNameSize, stream));
|
||||
}
|
||||
cudaCheck(cudaEventRecord(processGridTreeRootEvent, stream));
|
||||
cudaEventRecord(processGridTreeRootEvent);
|
||||
|
||||
for (const auto& [otherDeviceId, otherStream] : mDeviceMesh) {
|
||||
cudaSetDevice(otherDeviceId);
|
||||
|
||||
@@ -373,13 +373,6 @@ public:
|
||||
template<typename PtrT>
|
||||
void processPoints(const PtrT points, size_t pointCount);
|
||||
|
||||
// Only instantiated when BuildT == Point, from the corresponding branch of
|
||||
// processPoints. Holds the encode kernels because nvcc rejects an extended
|
||||
// lambda defined directly inside the block of an if constexpr statement on
|
||||
// some host compilers.
|
||||
template<typename PtrT>
|
||||
void encodePoints(const PtrT points, size_t pointCount);
|
||||
|
||||
void processBBox();
|
||||
|
||||
// the following methods are only defined when BuildT == Point
|
||||
@@ -1171,24 +1164,23 @@ inline void PointsToGrid<BuildT, ResourceT>::processLeafNodes(size_t pointCount)
|
||||
|
||||
//-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// The point-encode kernels operate on device data and are independent of the
|
||||
// resource type; only the trailing d_indx deallocation routes through ResourceT.
|
||||
// Dispatching the encode with if constexpr keeps it reachable for any ResourceT.
|
||||
template<typename BuildT, typename ResourceT>
|
||||
template<typename PtrT>
|
||||
inline void PointsToGrid<BuildT, ResourceT>::processPoints(const PtrT points, size_t pointCount)
|
||||
inline void PointsToGrid<BuildT, ResourceT>::processPoints(const PtrT, size_t pointCount)
|
||||
{
|
||||
if constexpr(util::is_same<BuildT, Point>::value) this->encodePoints(points, pointCount);
|
||||
mResource->deallocate_async(mData.d_indx, pointCount*sizeof(uint32_t), ResourceT::DEFAULT_ALIGNMENT, mStream);
|
||||
}// PointsToGrid<BuildT, ResourceT>::processPoints
|
||||
}
|
||||
|
||||
template<typename BuildT, typename ResourceT>
|
||||
//-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// Template specialization with BuildT = Point
|
||||
template<>
|
||||
template<typename PtrT>
|
||||
inline void PointsToGrid<BuildT, ResourceT>::encodePoints(const PtrT points, size_t pointCount)
|
||||
inline void PointsToGrid<Point>::processPoints(const PtrT points, size_t pointCount)
|
||||
{
|
||||
switch (mPointType){
|
||||
case PointType::Disable:
|
||||
throw std::runtime_error("PointsToGrid<Point, ResourceT>::encodePoints: mPointType == PointType::Disable\n");
|
||||
throw std::runtime_error("PointsToGrid<Point>::processPoints: mPointType == PointType::Disable\n");
|
||||
case PointType::PointID:
|
||||
util::cuda::lambdaKernel<<<numBlocks(pointCount), mNumThreads, 0, mStream>>>(pointCount, [=] __device__(size_t tid, PointsToGridData<Point> *d_data) {
|
||||
d_data->template getPoint<uint32_t>(tid) = d_data->d_indx[tid];
|
||||
@@ -1235,9 +1227,12 @@ inline void PointsToGrid<BuildT, ResourceT>::encodePoints(const PtrT points, siz
|
||||
}, mDeviceData); cudaCheckError();
|
||||
break;
|
||||
default:
|
||||
printf("Internal error in PointsToGrid<Point, ResourceT>::encodePoints\n");
|
||||
printf("Internal error in PointsToGrid<Point>::processPoints\n");
|
||||
}
|
||||
}// PointsToGrid<BuildT, ResourceT>::encodePoints
|
||||
// This is the PointsToGrid<Point> (== <Point, DeviceResource>) member specialization,
|
||||
// so ResourceT is not a name here; mResource is a DeviceResource* and the alignment is concrete.
|
||||
mResource->deallocate_async(mData.d_indx, pointCount*sizeof(uint32_t), nanovdb::cuda::DeviceResource::DEFAULT_ALIGNMENT, mStream);
|
||||
}// PointsToGrid<Point>::processPoints
|
||||
|
||||
//-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -62,23 +62,6 @@ if(NANOVDB_USE_CUDA)
|
||||
target_link_libraries(nanovdb_test_cuda_memory_resource PRIVATE nanovdb GTest::GTest GTest::Main)
|
||||
set_target_properties(nanovdb_test_cuda_memory_resource PROPERTIES CUDA_SEPARABLE_COMPILATION ON)
|
||||
add_test(nanovdb_cuda_memory_resource_unit_test nanovdb_test_cuda_memory_resource)
|
||||
|
||||
add_executable(nanovdb_test_cuda_util "TestUtilCuda.cu")
|
||||
target_link_libraries(nanovdb_test_cuda_util PRIVATE nanovdb GTest::GTest GTest::Main)
|
||||
set_target_properties(nanovdb_test_cuda_util PROPERTIES CUDA_SEPARABLE_COMPILATION ON)
|
||||
add_test(nanovdb_cuda_util_unit_test nanovdb_test_cuda_util)
|
||||
|
||||
# Same tests with every allocation forced through cudaMalloc/cudaFree --
|
||||
# the configuration a pool-less vGPU relies on.
|
||||
add_executable(nanovdb_test_cuda_util_sync "TestUtilCuda.cu")
|
||||
target_compile_definitions(nanovdb_test_cuda_util_sync PRIVATE NANOVDB_USE_SYNC_CUDA_MALLOC)
|
||||
target_link_libraries(nanovdb_test_cuda_util_sync PRIVATE nanovdb GTest::GTest GTest::Main)
|
||||
set_target_properties(nanovdb_test_cuda_util_sync PROPERTIES CUDA_SEPARABLE_COMPILATION ON)
|
||||
add_test(nanovdb_cuda_util_sync_unit_test nanovdb_test_cuda_util_sync)
|
||||
add_executable(nanovdb_test_cuda_buffer "TestBuffer.cu")
|
||||
target_link_libraries(nanovdb_test_cuda_buffer PRIVATE nanovdb GTest::GTest GTest::Main)
|
||||
set_target_properties(nanovdb_test_cuda_buffer PROPERTIES CUDA_SEPARABLE_COMPILATION ON)
|
||||
add_test(nanovdb_cuda_buffer_unit_test nanovdb_test_cuda_buffer)
|
||||
endif()
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -1,631 +0,0 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/// @file TestBuffer.cu
|
||||
///
|
||||
/// @brief Unit tests for the typed, resource-aware CUDA containers
|
||||
/// (cuda::Buffer, cuda::BufferView) and the synchronous Resource
|
||||
/// trait (cuda::is_resource).
|
||||
|
||||
#include <nanovdb/GridHandle.h>
|
||||
#include <nanovdb/cuda/Buffer.h>
|
||||
#include <nanovdb/cuda/DeviceResource.h>
|
||||
#include <nanovdb/cuda/PinnedResource.h>
|
||||
#include <nanovdb/tools/CreatePrimitives.h>
|
||||
|
||||
#include <cuda_runtime_api.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
//======================================================================
|
||||
// Shared test doubles
|
||||
//======================================================================
|
||||
|
||||
/// @brief Bookkeeping shared by all copies of a CountingResource, so counts
|
||||
/// survive the resource being held by value inside a Buffer.
|
||||
struct Counters
|
||||
{
|
||||
int allocs = 0;
|
||||
int deallocs = 0;
|
||||
size_t allocBytes = 0; // bytes of the most recent allocation
|
||||
size_t deallocBytes = 0; // bytes of the most recent deallocation
|
||||
};
|
||||
|
||||
/// @brief Resource that counts (non-null) allocations and deallocations so
|
||||
/// leaks can be asserted. Delegates the actual work to DeviceResource.
|
||||
struct CountingResource
|
||||
{
|
||||
static constexpr size_t DEFAULT_ALIGNMENT = nanovdb::cuda::DeviceResource::DEFAULT_ALIGNMENT;
|
||||
Counters* counters = nullptr;
|
||||
void* allocate_async(size_t bytes, size_t alignment, cudaStream_t stream) {
|
||||
void* p = nanovdb::cuda::DeviceResource{}.allocate_async(bytes, alignment, stream);
|
||||
if (p) { ++counters->allocs; counters->allocBytes = bytes; }
|
||||
return p;
|
||||
}
|
||||
void deallocate_async(void* p, size_t bytes, size_t alignment, cudaStream_t stream) {
|
||||
if (p) { ++counters->deallocs; counters->deallocBytes = bytes; }
|
||||
nanovdb::cuda::DeviceResource{}.deallocate_async(p, bytes, alignment, stream);
|
||||
}
|
||||
void* allocate(size_t bytes, size_t alignment) {
|
||||
void* p = nanovdb::cuda::DeviceResource{}.allocate(bytes, alignment);
|
||||
if (p) { ++counters->allocs; counters->allocBytes = bytes; }
|
||||
return p;
|
||||
}
|
||||
void deallocate(void* p, size_t bytes, size_t alignment) {
|
||||
if (p) { ++counters->deallocs; counters->deallocBytes = bytes; }
|
||||
nanovdb::cuda::DeviceResource{}.deallocate(p, bytes, alignment);
|
||||
}
|
||||
};
|
||||
|
||||
/// @brief Streams recorded by all copies of a StreamRecordingResource.
|
||||
struct StreamLog
|
||||
{
|
||||
std::vector<cudaStream_t> allocStreams;
|
||||
std::vector<cudaStream_t> deallocStreams;
|
||||
};
|
||||
|
||||
/// @brief Resource that records the stream of every allocation/deallocation,
|
||||
/// to verify stream-ordered teardown. Delegates work to DeviceResource.
|
||||
struct StreamRecordingResource
|
||||
{
|
||||
static constexpr size_t DEFAULT_ALIGNMENT = nanovdb::cuda::DeviceResource::DEFAULT_ALIGNMENT;
|
||||
StreamLog* log = nullptr;
|
||||
void* allocate_async(size_t bytes, size_t alignment, cudaStream_t stream) {
|
||||
void* p = nanovdb::cuda::DeviceResource{}.allocate_async(bytes, alignment, stream);
|
||||
if (p) log->allocStreams.push_back(stream);
|
||||
return p;
|
||||
}
|
||||
void deallocate_async(void* p, size_t bytes, size_t alignment, cudaStream_t stream) {
|
||||
if (p) log->deallocStreams.push_back(stream);
|
||||
nanovdb::cuda::DeviceResource{}.deallocate_async(p, bytes, alignment, stream);
|
||||
}
|
||||
void* allocate(size_t bytes, size_t alignment) {
|
||||
void* p = nanovdb::cuda::DeviceResource{}.allocate(bytes, alignment);
|
||||
if (p) log->allocStreams.push_back(cudaStream_t(0));
|
||||
return p;
|
||||
}
|
||||
void deallocate(void* p, size_t bytes, size_t alignment) {
|
||||
if (p) log->deallocStreams.push_back(cudaStream_t(0));
|
||||
nanovdb::cuda::DeviceResource{}.deallocate(p, bytes, alignment);
|
||||
}
|
||||
};
|
||||
|
||||
//======================================================================
|
||||
// Resource concept traits: is_resource (synchronous) / is_async_resource
|
||||
//======================================================================
|
||||
|
||||
/// @brief Local double that provides both the synchronous and the
|
||||
/// stream-ordered allocation interface, like a CCCL-style resource.
|
||||
struct DualResource
|
||||
{
|
||||
static constexpr size_t DEFAULT_ALIGNMENT = 256;
|
||||
void* allocate(size_t, size_t) { return nullptr; }
|
||||
void deallocate(void*, size_t, size_t) {}
|
||||
void* allocate_async(size_t, size_t, cudaStream_t) { return nullptr; }
|
||||
void deallocate_async(void*, size_t, size_t, cudaStream_t) {}
|
||||
};
|
||||
|
||||
// PinnedResource is synchronous-only: allocate/deallocate without a stream.
|
||||
static_assert(nanovdb::cuda::is_resource<nanovdb::cuda::PinnedResource>::value,
|
||||
"PinnedResource must satisfy the synchronous Resource concept");
|
||||
static_assert(!nanovdb::cuda::is_async_resource<nanovdb::cuda::PinnedResource>::value,
|
||||
"PinnedResource must not satisfy the AsyncResource concept");
|
||||
|
||||
// DeviceResource models the AsyncResource refinement: both the
|
||||
// stream-ordered and the synchronous interface (as in CCCL's cuda::mr).
|
||||
static_assert(nanovdb::cuda::is_async_resource<nanovdb::cuda::DeviceResource>::value,
|
||||
"DeviceResource must satisfy the AsyncResource concept");
|
||||
static_assert(nanovdb::cuda::is_resource<nanovdb::cuda::DeviceResource>::value,
|
||||
"DeviceResource must also satisfy the synchronous Resource concept");
|
||||
|
||||
/// @brief Local double with only the stream-ordered interface; under the
|
||||
/// refinement this is NOT an async resource (the sync pair is missing).
|
||||
struct AsyncOnlyResource
|
||||
{
|
||||
static constexpr size_t DEFAULT_ALIGNMENT = 256;
|
||||
void* allocate_async(size_t, size_t, cudaStream_t) { return nullptr; }
|
||||
void deallocate_async(void*, size_t, size_t, cudaStream_t) {}
|
||||
};
|
||||
static_assert(!nanovdb::cuda::is_async_resource<AsyncOnlyResource>::value,
|
||||
"the async pair alone must not satisfy the AsyncResource refinement");
|
||||
static_assert(!nanovdb::cuda::is_resource<AsyncOnlyResource>::value,
|
||||
"the async pair alone must not satisfy the Resource concept");
|
||||
|
||||
// An unrelated type satisfies neither concept.
|
||||
static_assert(!nanovdb::cuda::is_resource<int>::value,
|
||||
"int must not satisfy the Resource concept");
|
||||
static_assert(!nanovdb::cuda::is_async_resource<int>::value,
|
||||
"int must not satisfy the AsyncResource concept");
|
||||
|
||||
// A resource with both interfaces satisfies both concepts.
|
||||
static_assert(nanovdb::cuda::is_resource<DualResource>::value,
|
||||
"DualResource must satisfy the synchronous Resource concept");
|
||||
static_assert(nanovdb::cuda::is_async_resource<DualResource>::value,
|
||||
"DualResource must satisfy the AsyncResource concept");
|
||||
|
||||
TEST(TestBuffer, ResourceTraits)
|
||||
{
|
||||
// The static_asserts above are the real test; this anchors them in a
|
||||
// runnable suite entry.
|
||||
EXPECT_TRUE(nanovdb::cuda::is_resource<nanovdb::cuda::PinnedResource>::value);
|
||||
EXPECT_TRUE(nanovdb::cuda::is_async_resource<nanovdb::cuda::DeviceResource>::value);
|
||||
}
|
||||
|
||||
//======================================================================
|
||||
// Buffer<T, R> with a stream-ordered (async) resource: construction,
|
||||
// destruction, element counting, value-initialization
|
||||
//======================================================================
|
||||
|
||||
TEST(TestBuffer, AsyncCountingAllocFree)
|
||||
{
|
||||
cudaStream_t s = nullptr;
|
||||
ASSERT_EQ(cudaStreamCreate(&s), cudaSuccess);
|
||||
Counters c;
|
||||
const size_t n = 100;
|
||||
{
|
||||
nanovdb::cuda::Buffer<float, CountingResource> buf(s, CountingResource{&c}, n, nanovdb::cuda::noInit);
|
||||
EXPECT_NE(buf.data(), nullptr);
|
||||
EXPECT_EQ(buf.size(), n); // elements, not bytes
|
||||
EXPECT_EQ(buf.size_bytes(), n * sizeof(float));
|
||||
EXPECT_FALSE(buf.empty());
|
||||
EXPECT_EQ(c.allocs, 1); // exactly one allocation
|
||||
EXPECT_EQ(c.allocBytes, n * sizeof(float));
|
||||
EXPECT_EQ(c.deallocs, 0);
|
||||
}
|
||||
EXPECT_EQ(c.deallocs, 1); // exactly one deallocation
|
||||
EXPECT_EQ(c.deallocBytes, n * sizeof(float)); // with identical size
|
||||
ASSERT_EQ(cudaStreamSynchronize(s), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamDestroy(s), cudaSuccess);
|
||||
}
|
||||
|
||||
TEST(TestBuffer, EmptyBuffersAllocateNothing)
|
||||
{
|
||||
Counters c;
|
||||
{
|
||||
nanovdb::cuda::Buffer<float, CountingResource> def; // default ctor: empty
|
||||
EXPECT_EQ(def.data(), nullptr);
|
||||
EXPECT_EQ(def.size(), 0u);
|
||||
EXPECT_EQ(def.size_bytes(), 0u);
|
||||
EXPECT_TRUE(def.empty());
|
||||
|
||||
nanovdb::cuda::Buffer<float, CountingResource> zero(0, CountingResource{&c}, 0, nanovdb::cuda::noInit);
|
||||
EXPECT_EQ(zero.data(), nullptr);
|
||||
EXPECT_TRUE(zero.empty());
|
||||
}
|
||||
EXPECT_EQ(c.allocs, 0);
|
||||
EXPECT_EQ(c.deallocs, 0);
|
||||
}
|
||||
|
||||
TEST(TestBuffer, CountConstructionRequiresNoInit)
|
||||
{
|
||||
// A count without NoInit must not construct: implicit initialization of
|
||||
// freshly allocated memory is a hidden fill pass the dominant
|
||||
// allocate-then-overwrite pattern wastes (the cuda::buffer rule).
|
||||
using DevBuf = nanovdb::cuda::Buffer<int>;
|
||||
using PinBuf = nanovdb::cuda::Buffer<int, nanovdb::cuda::PinnedResource>;
|
||||
static_assert(!std::is_constructible<DevBuf, cudaStream_t, size_t>::value,
|
||||
"async count c-tor without NoInit must not exist");
|
||||
static_assert(!std::is_constructible<DevBuf, cudaStream_t, nanovdb::cuda::DeviceResource, size_t>::value,
|
||||
"async count+resource c-tor without NoInit must not exist");
|
||||
static_assert(!std::is_constructible<PinBuf, size_t>::value,
|
||||
"sync count c-tor without NoInit must not exist");
|
||||
static_assert(!std::is_constructible<PinBuf, nanovdb::cuda::PinnedResource, size_t>::value,
|
||||
"sync count+resource c-tor without NoInit must not exist");
|
||||
static_assert(std::is_constructible<DevBuf, cudaStream_t, size_t, nanovdb::cuda::NoInit>::value,
|
||||
"async count c-tor with NoInit exists");
|
||||
static_assert(std::is_constructible<PinBuf, size_t, nanovdb::cuda::NoInit>::value,
|
||||
"sync count c-tor with NoInit exists");
|
||||
SUCCEED();
|
||||
}
|
||||
|
||||
TEST(TestBuffer, OversizedCountThrows)
|
||||
{
|
||||
// An element count whose byte size overflows size_t must be rejected up
|
||||
// front rather than silently wrapping into a tiny allocation.
|
||||
const size_t oversized = std::numeric_limits<size_t>::max() / sizeof(int) + 1;
|
||||
EXPECT_THROW((nanovdb::cuda::Buffer<int>(0, oversized, nanovdb::cuda::noInit)), std::runtime_error);
|
||||
EXPECT_THROW((nanovdb::cuda::Buffer<int, nanovdb::cuda::PinnedResource>(oversized, nanovdb::cuda::noInit)), std::runtime_error);
|
||||
nanovdb::cuda::Buffer<int> buf(0, 8, nanovdb::cuda::noInit);
|
||||
cudaStream_t other = nullptr;
|
||||
ASSERT_EQ(cudaStreamCreate(&other), cudaSuccess);
|
||||
EXPECT_THROW(buf.resize(oversized, other), std::runtime_error);
|
||||
EXPECT_EQ(buf.size(), 8u); // failed resize leaves the buffer untouched --
|
||||
EXPECT_EQ(buf.stream(), cudaStream_t(0)); // including its retained stream
|
||||
ASSERT_EQ(cudaStreamDestroy(other), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamSynchronize(0), cudaSuccess);
|
||||
}
|
||||
|
||||
TEST(TestBuffer, ClearFreesAndEmpties)
|
||||
{
|
||||
Counters c;
|
||||
nanovdb::cuda::Buffer<float, CountingResource> buf(0, CountingResource{&c}, 64, nanovdb::cuda::noInit);
|
||||
ASSERT_EQ(c.allocs, 1);
|
||||
buf.clear();
|
||||
EXPECT_EQ(buf.data(), nullptr);
|
||||
EXPECT_EQ(buf.size(), 0u);
|
||||
EXPECT_TRUE(buf.empty());
|
||||
EXPECT_EQ(c.deallocs, 1);
|
||||
buf.clear(); // idempotent: no second free
|
||||
EXPECT_EQ(c.deallocs, 1);
|
||||
ASSERT_EQ(cudaStreamSynchronize(0), cudaSuccess);
|
||||
}
|
||||
|
||||
//======================================================================
|
||||
// Stream retention: the destructor and resize free on the retained stream
|
||||
// (stream of the most recent allocation, or the one supplied via setStream)
|
||||
//======================================================================
|
||||
|
||||
TEST(TestBuffer, DestructorFreesOnAllocationStream)
|
||||
{
|
||||
cudaStream_t a = nullptr;
|
||||
ASSERT_EQ(cudaStreamCreate(&a), cudaSuccess);
|
||||
StreamLog log;
|
||||
{
|
||||
nanovdb::cuda::Buffer<float, StreamRecordingResource> buf(a, StreamRecordingResource{&log}, 32, nanovdb::cuda::noInit);
|
||||
EXPECT_EQ(buf.stream(), a);
|
||||
}
|
||||
ASSERT_EQ(log.allocStreams.size(), 1u);
|
||||
EXPECT_EQ(log.allocStreams.back(), a);
|
||||
ASSERT_EQ(log.deallocStreams.size(), 1u);
|
||||
EXPECT_EQ(log.deallocStreams.back(), a);
|
||||
ASSERT_EQ(cudaStreamSynchronize(a), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamDestroy(a), cudaSuccess);
|
||||
}
|
||||
|
||||
TEST(TestBuffer, SetStreamRedirectsTheFree)
|
||||
{
|
||||
cudaStream_t a = nullptr, b = nullptr;
|
||||
ASSERT_EQ(cudaStreamCreate(&a), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamCreate(&b), cudaSuccess);
|
||||
StreamLog log;
|
||||
{
|
||||
nanovdb::cuda::Buffer<float, StreamRecordingResource> buf(a, StreamRecordingResource{&log}, 32, nanovdb::cuda::noInit);
|
||||
buf.setStream(b); // member update only, no synchronization
|
||||
EXPECT_EQ(buf.stream(), b);
|
||||
}
|
||||
ASSERT_EQ(log.allocStreams.size(), 1u);
|
||||
EXPECT_EQ(log.allocStreams.back(), a);
|
||||
ASSERT_EQ(log.deallocStreams.size(), 1u);
|
||||
EXPECT_EQ(log.deallocStreams.back(), b);
|
||||
ASSERT_EQ(cudaStreamSynchronize(a), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamSynchronize(b), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamDestroy(a), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamDestroy(b), cudaSuccess);
|
||||
}
|
||||
|
||||
TEST(TestBuffer, ResizePreservesPrefixAndRetainsNewStream)
|
||||
{
|
||||
cudaStream_t a = nullptr, c = nullptr;
|
||||
ASSERT_EQ(cudaStreamCreate(&a), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamCreate(&c), cudaSuccess);
|
||||
StreamLog log;
|
||||
const size_t n = 128, m = 256;
|
||||
{
|
||||
nanovdb::cuda::Buffer<int, StreamRecordingResource> buf(a, StreamRecordingResource{&log}, n, nanovdb::cuda::noInit);
|
||||
std::vector<int> pattern(n);
|
||||
for (size_t i = 0; i < n; ++i) pattern[i] = int(i) * 3 + 1;
|
||||
ASSERT_EQ(cudaMemcpyAsync(buf.data(), pattern.data(), n * sizeof(int), cudaMemcpyHostToDevice, a), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamSynchronize(a), cudaSuccess);
|
||||
|
||||
buf.resize(m, c); // grow on stream c
|
||||
EXPECT_EQ(buf.size(), m);
|
||||
EXPECT_EQ(buf.stream(), c);
|
||||
ASSERT_EQ(log.allocStreams.size(), 2u);
|
||||
EXPECT_EQ(log.allocStreams.back(), c); // new block allocated on c
|
||||
ASSERT_EQ(log.deallocStreams.size(), 1u);
|
||||
// The prefix copy on c is the old block's last use, so the old block is
|
||||
// freed on c as well; freeing it on a would let the allocator recycle
|
||||
// it concurrently with the copy.
|
||||
EXPECT_EQ(log.deallocStreams.back(), c);
|
||||
|
||||
std::vector<int> readback(m, -1);
|
||||
ASSERT_EQ(cudaMemcpyAsync(readback.data(), buf.data(), m * sizeof(int), cudaMemcpyDeviceToHost, c), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamSynchronize(c), cudaSuccess);
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
ASSERT_EQ(readback[i], pattern[i]) << "prefix element " << i << " lost across resize";
|
||||
|
||||
buf.resize(n / 2, c); // shrink: keeps the min(old,new) prefix
|
||||
std::vector<int> shrunk(n / 2, -1);
|
||||
ASSERT_EQ(cudaMemcpyAsync(shrunk.data(), buf.data(), (n / 2) * sizeof(int), cudaMemcpyDeviceToHost, c), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamSynchronize(c), cudaSuccess);
|
||||
for (size_t i = 0; i < n / 2; ++i)
|
||||
ASSERT_EQ(shrunk[i], pattern[i]) << "prefix element " << i << " lost across shrink";
|
||||
}
|
||||
EXPECT_EQ(log.allocStreams.size(), log.deallocStreams.size()); // every allocation freed
|
||||
ASSERT_EQ(cudaStreamSynchronize(a), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamSynchronize(c), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamDestroy(a), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamDestroy(c), cudaSuccess);
|
||||
}
|
||||
|
||||
//======================================================================
|
||||
// Move semantics (Buffer is move-only) and explicit deep copy
|
||||
//======================================================================
|
||||
|
||||
TEST(TestBuffer, MoveConstructorSteals)
|
||||
{
|
||||
Counters c;
|
||||
{
|
||||
nanovdb::cuda::Buffer<float, CountingResource> src(0, CountingResource{&c}, 64, nanovdb::cuda::noInit);
|
||||
float* p = src.data();
|
||||
nanovdb::cuda::Buffer<float, CountingResource> dst(std::move(src));
|
||||
EXPECT_EQ(dst.data(), p); // stolen, not reallocated
|
||||
EXPECT_EQ(dst.size(), 64u);
|
||||
EXPECT_EQ(src.data(), nullptr); // source emptied
|
||||
EXPECT_EQ(src.size(), 0u);
|
||||
EXPECT_TRUE(src.empty());
|
||||
EXPECT_EQ(c.allocs, 1); // no allocation on move
|
||||
}
|
||||
EXPECT_EQ(c.allocs, c.deallocs); // exactly one free, no double free
|
||||
EXPECT_EQ(c.deallocs, 1);
|
||||
ASSERT_EQ(cudaStreamSynchronize(0), cudaSuccess);
|
||||
}
|
||||
|
||||
TEST(TestBuffer, MoveAssignFreesDestination)
|
||||
{
|
||||
Counters c;
|
||||
{
|
||||
nanovdb::cuda::Buffer<float, CountingResource> a(0, CountingResource{&c}, 64, nanovdb::cuda::noInit);
|
||||
nanovdb::cuda::Buffer<float, CountingResource> b(0, CountingResource{&c}, 32, nanovdb::cuda::noInit);
|
||||
ASSERT_EQ(c.allocs, 2);
|
||||
float* p = a.data();
|
||||
b = std::move(a); // frees b's old block first
|
||||
EXPECT_EQ(c.deallocs, 1);
|
||||
EXPECT_EQ(b.data(), p);
|
||||
EXPECT_EQ(b.size(), 64u);
|
||||
EXPECT_EQ(a.data(), nullptr);
|
||||
}
|
||||
EXPECT_EQ(c.allocs, c.deallocs); // both blocks freed exactly once
|
||||
ASSERT_EQ(cudaStreamSynchronize(0), cudaSuccess);
|
||||
}
|
||||
|
||||
TEST(TestBuffer, SelfMoveAssignIsSafe)
|
||||
{
|
||||
Counters c;
|
||||
{
|
||||
nanovdb::cuda::Buffer<float, CountingResource> buf(0, CountingResource{&c}, 16, nanovdb::cuda::noInit);
|
||||
auto& self = buf; // avoids the compiler warning on a literal self-move
|
||||
buf = std::move(self);
|
||||
EXPECT_NE(buf.data(), nullptr); // still owns its block
|
||||
EXPECT_EQ(buf.size(), 16u);
|
||||
EXPECT_EQ(c.deallocs, 0);
|
||||
}
|
||||
EXPECT_EQ(c.allocs, 1);
|
||||
EXPECT_EQ(c.deallocs, 1);
|
||||
ASSERT_EQ(cudaStreamSynchronize(0), cudaSuccess);
|
||||
}
|
||||
|
||||
TEST(TestBuffer, CopyIsDeep)
|
||||
{
|
||||
cudaStream_t s = nullptr;
|
||||
ASSERT_EQ(cudaStreamCreate(&s), cudaSuccess);
|
||||
Counters c;
|
||||
const size_t n = 96;
|
||||
{
|
||||
nanovdb::cuda::Buffer<int, CountingResource> src(s, CountingResource{&c}, n, nanovdb::cuda::noInit);
|
||||
std::vector<int> pattern(n);
|
||||
for (size_t i = 0; i < n; ++i) pattern[i] = int(i) - 7;
|
||||
ASSERT_EQ(cudaMemcpyAsync(src.data(), pattern.data(), n * sizeof(int), cudaMemcpyHostToDevice, s), cudaSuccess);
|
||||
|
||||
auto dup = src.copy(s);
|
||||
EXPECT_EQ(c.allocs, 2); // deep copy allocates its own block
|
||||
EXPECT_NE(dup.data(), src.data());
|
||||
EXPECT_EQ(dup.size(), n);
|
||||
EXPECT_EQ(dup.stream(), s);
|
||||
|
||||
std::vector<int> readback(n, 0);
|
||||
ASSERT_EQ(cudaMemcpyAsync(readback.data(), dup.data(), n * sizeof(int), cudaMemcpyDeviceToHost, s), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamSynchronize(s), cudaSuccess);
|
||||
EXPECT_EQ(readback, pattern);
|
||||
}
|
||||
EXPECT_EQ(c.allocs, c.deallocs);
|
||||
ASSERT_EQ(cudaStreamSynchronize(s), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamDestroy(s), cudaSuccess);
|
||||
}
|
||||
|
||||
//======================================================================
|
||||
// CUDA graph capture: the async path (stream-ordered allocation, no hidden
|
||||
// synchronization or initialization) must be recordable in a graph. The
|
||||
// synchronous tier is excluded by construction -- it synchronizes.
|
||||
//======================================================================
|
||||
|
||||
TEST(TestBuffer, AsyncPathIsGraphCapturable)
|
||||
{
|
||||
cudaStream_t s = nullptr;
|
||||
ASSERT_EQ(cudaStreamCreate(&s), cudaSuccess);
|
||||
|
||||
cudaGraph_t graph = nullptr;
|
||||
ASSERT_EQ(cudaStreamBeginCapture(s, cudaStreamCaptureModeGlobal), cudaSuccess);
|
||||
{
|
||||
// Allocation, use, and free are all recorded as graph nodes; the
|
||||
// buffer is created and destroyed inside the capture so the graph
|
||||
// owns the allocation's whole lifetime.
|
||||
nanovdb::cuda::Buffer<int> buf(s, 256, nanovdb::cuda::noInit);
|
||||
ASSERT_NE(buf.data(), nullptr);
|
||||
ASSERT_EQ(cudaMemsetAsync(buf.data(), 0x5A, buf.size_bytes(), s), cudaSuccess);
|
||||
}
|
||||
ASSERT_EQ(cudaStreamEndCapture(s, &graph), cudaSuccess);
|
||||
ASSERT_NE(graph, nullptr);
|
||||
|
||||
cudaGraphExec_t exec = nullptr;
|
||||
ASSERT_EQ(cudaGraphInstantiate(&exec, graph, 0), cudaSuccess);
|
||||
ASSERT_EQ(cudaGraphLaunch(exec, s), cudaSuccess);
|
||||
ASSERT_EQ(cudaGraphLaunch(exec, s), cudaSuccess); // relaunchable
|
||||
ASSERT_EQ(cudaStreamSynchronize(s), cudaSuccess);
|
||||
|
||||
ASSERT_EQ(cudaGraphExecDestroy(exec), cudaSuccess);
|
||||
ASSERT_EQ(cudaGraphDestroy(graph), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamDestroy(s), cudaSuccess);
|
||||
}
|
||||
|
||||
//======================================================================
|
||||
// Buffer over a synchronous resource (PinnedResource): no stream anywhere
|
||||
//======================================================================
|
||||
|
||||
template<class B, class = void>
|
||||
struct HasSetStream : std::false_type {};
|
||||
template<class B>
|
||||
struct HasSetStream<B, std::void_t<decltype(std::declval<B&>().setStream(cudaStream_t{0}))>> : std::true_type {};
|
||||
|
||||
template<class B, class = void>
|
||||
struct HasStreamGetter : std::false_type {};
|
||||
template<class B>
|
||||
struct HasStreamGetter<B, std::void_t<decltype(std::declval<const B&>().stream())>> : std::true_type {};
|
||||
|
||||
using PinnedBufferF = nanovdb::cuda::Buffer<float, nanovdb::cuda::PinnedResource>;
|
||||
using DeviceBufferF = nanovdb::cuda::Buffer<float, nanovdb::cuda::DeviceResource>;
|
||||
|
||||
// A Buffer over a synchronous resource exposes no stream API at all.
|
||||
static_assert(!HasSetStream<PinnedBufferF>::value, "sync-resource Buffer must not expose setStream");
|
||||
static_assert(!HasStreamGetter<PinnedBufferF>::value, "sync-resource Buffer must not expose stream()");
|
||||
static_assert(HasSetStream<DeviceBufferF>::value, "async-resource Buffer exposes setStream");
|
||||
static_assert(HasStreamGetter<DeviceBufferF>::value, "async-resource Buffer exposes stream()");
|
||||
|
||||
TEST(TestBuffer, PinnedBufferIsPageLocked)
|
||||
{
|
||||
const size_t n = 300;
|
||||
PinnedBufferF buf(n, nanovdb::cuda::noInit); // synchronous: no stream parameter
|
||||
ASSERT_NE(buf.data(), nullptr);
|
||||
EXPECT_EQ(buf.size(), n);
|
||||
|
||||
cudaPointerAttributes attr{};
|
||||
ASSERT_EQ(cudaPointerGetAttributes(&attr, buf.data()), cudaSuccess);
|
||||
EXPECT_EQ(attr.type, cudaMemoryTypeHost); // genuine page-locked host memory
|
||||
}
|
||||
|
||||
TEST(TestBuffer, PinnedBufferNoInitResizeAndCopy)
|
||||
{
|
||||
const size_t n = 64;
|
||||
nanovdb::cuda::Buffer<int, nanovdb::cuda::PinnedResource> buf(n, nanovdb::cuda::noInit);
|
||||
ASSERT_NE(buf.data(), nullptr);
|
||||
for (size_t i = 0; i < n; ++i) buf.data()[i] = int(i) + 11;
|
||||
|
||||
buf.resize(2 * n); // synchronous: no stream parameter
|
||||
EXPECT_EQ(buf.size(), 2 * n);
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
ASSERT_EQ(buf.data()[i], int(i) + 11) << "prefix element " << i << " lost across resize";
|
||||
|
||||
auto dup = buf.copy();
|
||||
ASSERT_NE(dup.data(), nullptr);
|
||||
EXPECT_NE(dup.data(), buf.data()); // deep copy
|
||||
EXPECT_EQ(dup.size(), buf.size());
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
ASSERT_EQ(dup.data()[i], int(i) + 11);
|
||||
}
|
||||
|
||||
//======================================================================
|
||||
// Raw (untyped) form: Buffer<std::byte, DeviceResource>
|
||||
//======================================================================
|
||||
|
||||
TEST(TestBuffer, RawByteBufferRoundTrip)
|
||||
{
|
||||
cudaStream_t s = nullptr;
|
||||
ASSERT_EQ(cudaStreamCreate(&s), cudaSuccess);
|
||||
const size_t n = 1000;
|
||||
nanovdb::cuda::Buffer<std::byte> buf(s, n, nanovdb::cuda::noInit);
|
||||
EXPECT_EQ(buf.size(), n);
|
||||
EXPECT_EQ(buf.size_bytes(), n); // one byte per element
|
||||
|
||||
std::vector<std::byte> pattern(n);
|
||||
for (size_t i = 0; i < n; ++i) pattern[i] = std::byte(i % 251);
|
||||
ASSERT_EQ(cudaMemcpyAsync(buf.data(), pattern.data(), n, cudaMemcpyHostToDevice, s), cudaSuccess);
|
||||
std::vector<std::byte> readback(n);
|
||||
ASSERT_EQ(cudaMemcpyAsync(readback.data(), buf.data(), n, cudaMemcpyDeviceToHost, s), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamSynchronize(s), cudaSuccess);
|
||||
EXPECT_EQ(readback, pattern);
|
||||
buf.clear();
|
||||
ASSERT_EQ(cudaStreamSynchronize(s), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamDestroy(s), cudaSuccess);
|
||||
}
|
||||
|
||||
//======================================================================
|
||||
// BufferView: non-owning typed view with span semantics
|
||||
//======================================================================
|
||||
|
||||
static_assert(std::is_trivially_copyable_v<nanovdb::cuda::BufferView<std::byte>>,
|
||||
"BufferView must be trivially copyable");
|
||||
static_assert(std::is_trivially_copyable_v<nanovdb::cuda::BufferView<const std::byte>>,
|
||||
"BufferView over const elements must be trivially copyable");
|
||||
|
||||
TEST(TestBuffer, BufferViewWrapsHostArray)
|
||||
{
|
||||
float host[5] = {0.f, 1.f, 2.f, 3.f, 4.f};
|
||||
|
||||
nanovdb::cuda::BufferView<float> empty;
|
||||
EXPECT_EQ(empty.data(), nullptr);
|
||||
EXPECT_EQ(empty.size(), 0u);
|
||||
EXPECT_TRUE(empty.empty());
|
||||
|
||||
nanovdb::cuda::BufferView<float> view(host, 5);
|
||||
EXPECT_EQ(view.data(), host);
|
||||
EXPECT_EQ(view.size(), 5u);
|
||||
EXPECT_EQ(view.size_bytes(), 5 * sizeof(float));
|
||||
EXPECT_FALSE(view.empty());
|
||||
view.data()[2] = 42.f; // writable through the view
|
||||
EXPECT_EQ(host[2], 42.f);
|
||||
|
||||
view.clear(); // detaches, does not free
|
||||
EXPECT_EQ(view.data(), nullptr);
|
||||
EXPECT_TRUE(view.empty());
|
||||
EXPECT_EQ(host[2], 42.f); // underlying storage untouched
|
||||
|
||||
// A null base pointer is only valid for an empty view.
|
||||
EXPECT_THROW((nanovdb::cuda::BufferView<float>(nullptr, 5)), std::runtime_error);
|
||||
EXPECT_NO_THROW((nanovdb::cuda::BufferView<float>(nullptr, 0)));
|
||||
}
|
||||
|
||||
TEST(TestBuffer, BufferViewConstElements)
|
||||
{
|
||||
const std::byte host[4] = {std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4}};
|
||||
nanovdb::cuda::BufferView<const std::byte> view(host, 4);
|
||||
EXPECT_EQ(view.data(), host);
|
||||
EXPECT_EQ(view.size_bytes(), 4u);
|
||||
EXPECT_EQ(view.data()[3], std::byte{4}); // read-only access
|
||||
}
|
||||
|
||||
//======================================================================
|
||||
// Zero-copy GridHandle over a BufferView: a handle that indexes grids in
|
||||
// storage owned elsewhere
|
||||
//======================================================================
|
||||
|
||||
TEST(TestBuffer, GridHandleOverBufferViewIsZeroCopy)
|
||||
{
|
||||
// Build a small host grid with the default (owning) HostBuffer.
|
||||
auto owner = nanovdb::tools::createLevelSetSphere<float>(20.0, nanovdb::Vec3d(0), 1.0, 3.0, nanovdb::Vec3d(0), "sphere");
|
||||
ASSERT_NE(owner.data(), nullptr);
|
||||
const auto* grid = owner.grid<float>();
|
||||
ASSERT_NE(grid, nullptr);
|
||||
|
||||
{
|
||||
// Wrap the owning handle's storage in a non-owning view...
|
||||
nanovdb::cuda::BufferView<std::byte> view(static_cast<std::byte*>(owner.data()), owner.bufferSize());
|
||||
// ...and construct a second, zero-copy handle over the same bytes.
|
||||
nanovdb::GridHandle<nanovdb::cuda::BufferView<std::byte>> viewHandle(std::move(view));
|
||||
|
||||
EXPECT_EQ(viewHandle.gridCount(), owner.gridCount());
|
||||
EXPECT_EQ(viewHandle.data(), owner.data()); // same bytes, not a copy
|
||||
|
||||
const auto* viewGrid = viewHandle.grid<float>();
|
||||
ASSERT_NE(viewGrid, nullptr);
|
||||
EXPECT_EQ(viewGrid, grid); // zero copy: identical grid address
|
||||
EXPECT_EQ(viewGrid->gridType(), grid->gridType());
|
||||
EXPECT_EQ(viewGrid->activeVoxelCount(), grid->activeVoxelCount());
|
||||
EXPECT_EQ(viewGrid->worldBBox(), grid->worldBBox());
|
||||
EXPECT_STREQ(viewGrid->gridName(), grid->gridName());
|
||||
} // view handle destroyed first: detaches without freeing
|
||||
|
||||
// The owning handle is still intact and is the single owner of the bytes.
|
||||
ASSERT_NE(owner.data(), nullptr);
|
||||
EXPECT_NE(owner.grid<float>(), nullptr);
|
||||
EXPECT_EQ(owner.grid<float>()->activeVoxelCount(), grid->activeVoxelCount());
|
||||
} // owner destroyed here: the one and only free
|
||||
|
||||
} // unnamed namespace
|
||||
@@ -209,63 +209,4 @@ TEST(TestMemoryResource, PointsToGrid_InjectedInstanceSeam)
|
||||
ASSERT_EQ(cudaFree(d_coords), cudaSuccess);
|
||||
}
|
||||
|
||||
//======================================================================
|
||||
// A Point grid built through a custom (non-default) resource must still
|
||||
// encode its point coordinates. The encode path must not depend on the
|
||||
// resource type; only allocation does.
|
||||
//======================================================================
|
||||
|
||||
TEST(TestMemoryResource, PointsToGrid_PointEncodedWithCustomResource)
|
||||
{
|
||||
using BuildT = nanovdb::Point;
|
||||
using Vec3T = nanovdb::Vec3d;
|
||||
|
||||
const size_t pointCount = 256;
|
||||
std::vector<Vec3T> points;
|
||||
points.reserve(pointCount);
|
||||
std::srand(98765);
|
||||
const int max = 128, min = -max;
|
||||
auto op = [&]() { return double(std::rand() % (max - min) + min); };
|
||||
while (points.size() < pointCount) points.emplace_back(op(), op(), op());
|
||||
|
||||
Vec3T* d_points = nullptr;
|
||||
const size_t pointSize = points.size() * sizeof(Vec3T);
|
||||
ASSERT_EQ(cudaMalloc(&d_points, pointSize), cudaSuccess);
|
||||
ASSERT_EQ(cudaMemcpy(d_points, points.data(), pointSize, cudaMemcpyHostToDevice), cudaSuccess);
|
||||
|
||||
const double voxelSize = 8.0;
|
||||
CountingResource res;
|
||||
nanovdb::tools::cuda::PointsToGrid<BuildT, CountingResource> converter(voxelSize, nanovdb::Vec3d(0.0), 0, res);
|
||||
auto handle = converter.getHandle(d_points, pointCount);
|
||||
ASSERT_EQ(cudaStreamSynchronize(0), cudaSuccess);
|
||||
ASSERT_EQ(cudaFree(d_points), cudaSuccess);
|
||||
|
||||
EXPECT_TRUE(handle.deviceData());
|
||||
EXPECT_TRUE(handle.deviceGrid<BuildT>());
|
||||
|
||||
handle.deviceDownload();
|
||||
auto* grid = handle.grid<BuildT>();
|
||||
ASSERT_TRUE(grid);
|
||||
EXPECT_EQ(pointCount, grid->pointCount());
|
||||
|
||||
// Every input point must be recoverable from the encoded per-voxel point data.
|
||||
nanovdb::PointAccessor<Vec3T, BuildT> acc(*grid);
|
||||
ASSERT_TRUE(acc);
|
||||
for (size_t i = 0; i < points.size(); ++i) {
|
||||
const nanovdb::Coord ijk = grid->worldToIndex(points[i]).round();
|
||||
ASSERT_TRUE(acc.probeLeaf(ijk) != nullptr);
|
||||
ASSERT_TRUE(acc.isActive(ijk));
|
||||
const Vec3T *start = nullptr, *stop = nullptr;
|
||||
const uint64_t count = acc.voxelPoints(ijk, start, stop);
|
||||
ASSERT_TRUE(start);
|
||||
ASSERT_TRUE(stop);
|
||||
bool found = false;
|
||||
for (uint64_t j = 0; !found && j < count; ++j)
|
||||
found = nanovdb::math::isApproxZero<double>((points[i] - start[j]).lengthSqr());
|
||||
EXPECT_TRUE(found);
|
||||
}
|
||||
|
||||
EXPECT_GT(res.allocs, 0);
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
@@ -10,9 +10,6 @@
|
||||
#include <cuda_runtime_api.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <thread> // for std::thread
|
||||
#include <algorithm> // for std::sort, std::unique
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include <thrust/fill.h>
|
||||
#include <thrust/universal_vector.h>
|
||||
@@ -246,7 +243,7 @@ TEST(TestNanoVDBMultiGPU, DenseLeaf_DistributedCudaPointsToGrid_UnifiedBuffer)
|
||||
EXPECT_TRUE(data);
|
||||
grid = handle.grid<BuildT>();
|
||||
EXPECT_TRUE(grid);
|
||||
EXPECT_EQ(voxelCount, grid->activeVoxelCount());
|
||||
EXPECT_TRUE(grid->activeVoxelCount() == 512);
|
||||
EXPECT_EQ(nanovdb::Vec3d(1.0), grid->voxelSize());
|
||||
|
||||
cudaCheck(cudaFree(voxels));
|
||||
@@ -418,118 +415,3 @@ TEST(TestNanoVDBMultiGPU, ManyTiles_DistributedCudaPointsToGrid)
|
||||
cudaCheck(cudaFree(voxels));
|
||||
cudaSetDevice(current);
|
||||
}// ManyTiles_DistributedCudaPointsToGrid
|
||||
|
||||
/// @brief Regression test for the multi-GPU shared-tile race. All coordinates
|
||||
/// fall inside a single upper-node tile (each upper node spans 4096
|
||||
/// voxels per axis, so ijk in [0, 4095] maps to tile (0,0,0)), yet they
|
||||
/// populate many lower and leaf nodes. With the default even initial
|
||||
/// split this one tile is shared across every GPU, so the builder must
|
||||
/// consolidate it onto a single device. A cross-device race in leaf
|
||||
/// construction would drop a device's contribution and undercount the
|
||||
/// active voxels, which the exact-count assertion below catches
|
||||
/// deterministically.
|
||||
TEST(TestNanoVDBMultiGPU, SingleUpperNode_DistributedCudaPointsToGrid_UnifiedBuffer)
|
||||
{
|
||||
int current = 0;
|
||||
cudaCheck(cudaGetDevice(¤t));
|
||||
|
||||
using BufferT = nanovdb::cuda::UnifiedBuffer;
|
||||
using BuildT = nanovdb::ValueOnIndex;
|
||||
|
||||
const size_t inputCount = 1 << 18;// 262144
|
||||
nanovdb::Coord* voxels = nullptr;
|
||||
cudaCheck(cudaMallocManaged(&voxels, inputCount * sizeof(nanovdb::Coord)));
|
||||
std::srand(24680);
|
||||
auto op = [](){ return rand() % 4096; };// stays within a single upper node
|
||||
for (size_t i = 0; i < inputCount; ++i)
|
||||
voxels[i] = nanovdb::Coord(op(), op(), op());
|
||||
|
||||
// Deterministic expected active-voxel count (the input may contain duplicates).
|
||||
std::vector<uint64_t> packed(inputCount);
|
||||
for (size_t i = 0; i < inputCount; ++i)
|
||||
packed[i] = (uint64_t(voxels[i][0]) << 24) | (uint64_t(voxels[i][1]) << 12) | uint64_t(voxels[i][2]);
|
||||
std::sort(packed.begin(), packed.end());
|
||||
const size_t uniqueCount = static_cast<size_t>(std::unique(packed.begin(), packed.end()) - packed.begin());
|
||||
|
||||
nanovdb::cuda::DeviceMesh deviceMesh;
|
||||
nanovdb::tools::cuda::DistributedPointsToGrid<BuildT> converter(deviceMesh);
|
||||
auto handle = converter.getHandle(voxels, inputCount);
|
||||
|
||||
EXPECT_TRUE(handle.deviceData());
|
||||
EXPECT_TRUE(handle.deviceGrid<BuildT>());
|
||||
handle.deviceDownload();
|
||||
auto *grid = handle.grid<BuildT>();
|
||||
EXPECT_TRUE(grid);
|
||||
EXPECT_EQ(nanovdb::Vec3d(1.0), grid->voxelSize());
|
||||
// The input occupies exactly one upper-node tile, so the shared-tile
|
||||
// consolidation path is exercised.
|
||||
EXPECT_EQ(1u, grid->tree().nodeCount(2));
|
||||
// Every unique input voxel must be active exactly once.
|
||||
EXPECT_EQ(static_cast<uint64_t>(uniqueCount), grid->activeVoxelCount());
|
||||
|
||||
nanovdb::util::forEach(0, inputCount, 1, [&](const nanovdb::util::Range1D &r){
|
||||
auto acc = grid->getAccessor();
|
||||
for (size_t i=r.begin(); i!=r.end(); ++i) {
|
||||
const nanovdb::Coord &ijk = voxels[i];
|
||||
EXPECT_TRUE(acc.probeLeaf(ijk)!=nullptr);
|
||||
EXPECT_TRUE(acc.isActive(ijk));
|
||||
EXPECT_TRUE(acc.getValue(ijk) > 0u);
|
||||
}
|
||||
});
|
||||
|
||||
cudaCheck(cudaFree(voxels));
|
||||
cudaSetDevice(current);
|
||||
}// SingleUpperNode_DistributedCudaPointsToGrid_UnifiedBuffer
|
||||
|
||||
/// @brief Cross-checks the distributed builder against the trusted single-GPU
|
||||
/// PointsToGrid on an input that forces a single tile to be split across
|
||||
/// devices. Index assignment order may differ between the two builders,
|
||||
/// so we compare topology (node counts, active-voxel count) and voxel
|
||||
/// occupancy rather than the ValueOnIndex indices themselves.
|
||||
TEST(TestNanoVDBMultiGPU, MatchesSingleGpu_DistributedCudaPointsToGrid)
|
||||
{
|
||||
int current = 0;
|
||||
cudaCheck(cudaGetDevice(¤t));
|
||||
|
||||
using BufferT = nanovdb::cuda::UnifiedBuffer;
|
||||
using BuildT = nanovdb::ValueOnIndex;
|
||||
|
||||
const size_t inputCount = 1 << 17;// 131072, all within a single upper node
|
||||
nanovdb::Coord* voxels = nullptr;
|
||||
cudaCheck(cudaMallocManaged(&voxels, inputCount * sizeof(nanovdb::Coord)));
|
||||
std::srand(1357);
|
||||
auto op = [](){ return rand() % 4096; };
|
||||
for (size_t i = 0; i < inputCount; ++i)
|
||||
voxels[i] = nanovdb::Coord(op(), op(), op());
|
||||
|
||||
nanovdb::cuda::DeviceMesh deviceMesh;
|
||||
nanovdb::tools::cuda::DistributedPointsToGrid<BuildT> converter(deviceMesh);
|
||||
auto distributedHandle = converter.getHandle(voxels, inputCount);
|
||||
distributedHandle.deviceDownload();
|
||||
auto *distributedGrid = distributedHandle.grid<BuildT>();
|
||||
EXPECT_TRUE(distributedGrid);
|
||||
|
||||
cudaSetDevice(current);
|
||||
auto referenceHandle = nanovdb::tools::cuda::voxelsToGrid<BuildT, nanovdb::Coord*, BufferT>(voxels, inputCount);
|
||||
referenceHandle.deviceDownload();
|
||||
auto *referenceGrid = referenceHandle.grid<BuildT>();
|
||||
EXPECT_TRUE(referenceGrid);
|
||||
|
||||
EXPECT_EQ(referenceGrid->activeVoxelCount(), distributedGrid->activeVoxelCount());
|
||||
EXPECT_EQ(referenceGrid->tree().nodeCount(0), distributedGrid->tree().nodeCount(0));
|
||||
EXPECT_EQ(referenceGrid->tree().nodeCount(1), distributedGrid->tree().nodeCount(1));
|
||||
EXPECT_EQ(referenceGrid->tree().nodeCount(2), distributedGrid->tree().nodeCount(2));
|
||||
|
||||
nanovdb::util::forEach(0, inputCount, 1, [&](const nanovdb::util::Range1D &r){
|
||||
auto distributedAcc = distributedGrid->getAccessor();
|
||||
auto referenceAcc = referenceGrid->getAccessor();
|
||||
for (size_t i=r.begin(); i!=r.end(); ++i) {
|
||||
const nanovdb::Coord &ijk = voxels[i];
|
||||
EXPECT_TRUE(referenceAcc.isActive(ijk));
|
||||
EXPECT_TRUE(distributedAcc.isActive(ijk));
|
||||
}
|
||||
});
|
||||
|
||||
cudaCheck(cudaFree(voxels));
|
||||
cudaSetDevice(current);
|
||||
}// MatchesSingleGpu_DistributedCudaPointsToGrid
|
||||
|
||||
@@ -791,31 +791,20 @@ TEST(TestNanoVDBCUDA, FewTiles_CudaPointsToGrid)
|
||||
}// FewTiles_CudaPointsToGrid
|
||||
|
||||
/// @brief Exercises the segmented sort path (>= 32 tiles) in PointsToGrid.
|
||||
/// Coordinates are placed in 4^3 = 64 upper internal node tiles
|
||||
/// Coordinates in [-8000, 8000] produce 4^3 = 64 upper internal node tiles
|
||||
/// (each upper node covers 4096 voxels per dimension), exceeding the threshold of 32.
|
||||
/// Each tile contains 4^3 voxels in one leaf, giving every segment multiple keys to sort
|
||||
/// without creating the very large number of lower nodes produced by sparse random voxels.
|
||||
TEST(TestNanoVDBCUDA, ManyTiles_CudaPointsToGrid)
|
||||
{
|
||||
using BuildT = nanovdb::ValueOnIndex;
|
||||
const size_t voxelCount = 1 << 20;// 1048576
|
||||
std::vector<nanovdb::Coord> voxels;
|
||||
voxels.reserve(1 << 12);// 64 upper tiles * 64 voxels per tile = 4096
|
||||
for (int tx = -2; tx < 2; ++tx) {
|
||||
for (int ty = -2; ty < 2; ++ty) {
|
||||
for (int tz = -2; tz < 2; ++tz) {
|
||||
const nanovdb::Coord base(tx * 4096 + 64, ty * 4096 + 64, tz * 4096 + 64);
|
||||
for (int x = 0; x < 4; ++x) {
|
||||
for (int y = 0; y < 4; ++y) {
|
||||
for (int z = 0; z < 4; ++z) {
|
||||
voxels.emplace_back(base + nanovdb::Coord(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
voxels.reserve(voxelCount);
|
||||
std::srand(54321);
|
||||
const int max = 8000, min = -max;
|
||||
auto op = [&](){return rand() % (max - min) + min;};
|
||||
while (voxels.size() < voxelCount) voxels.push_back(nanovdb::Coord(op(), op(), op()));
|
||||
}
|
||||
const size_t voxelCount = voxels.size();
|
||||
ASSERT_EQ(size_t(1 << 12), voxelCount);
|
||||
|
||||
nanovdb::Coord* d_coords;
|
||||
cudaCheck(cudaMalloc(&d_coords, voxels.size() * sizeof(nanovdb::Coord)));
|
||||
@@ -830,7 +819,6 @@ TEST(TestNanoVDBCUDA, ManyTiles_CudaPointsToGrid)
|
||||
EXPECT_TRUE(grid);
|
||||
EXPECT_TRUE(grid->valueCount() > 0);
|
||||
EXPECT_EQ(nanovdb::Vec3d(1.0), grid->voxelSize());
|
||||
EXPECT_EQ(64u, grid->tree().nodeCount(2));
|
||||
|
||||
nanovdb::util::forEach(voxels, [&](const nanovdb::util::Range1D &r){
|
||||
auto acc = grid->getAccessor();
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/// @file TestUtilCuda.cu
|
||||
///
|
||||
/// @brief Unit tests for the util::cuda allocation wrappers: the cached
|
||||
/// memory-pool capability query, and an allocate/use/free round-trip
|
||||
/// through whichever mode the build selects (stream-ordered by default,
|
||||
/// synchronous under NANOVDB_USE_SYNC_CUDA_MALLOC). This file is compiled
|
||||
/// into two test binaries -- one per mode -- so both are exercised. On a
|
||||
/// device without memory pools the default (async) wrappers fail by
|
||||
/// design; such a device must build with NANOVDB_USE_SYNC_CUDA_MALLOC,
|
||||
/// so the async round-trip skips when pools are absent.
|
||||
|
||||
#include <nanovdb/util/cuda/Util.h>
|
||||
|
||||
#include <cuda_runtime_api.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
TEST(TestUtilCuda, MemoryPoolsSupportedMatchesAttribute)
|
||||
{
|
||||
int count = 0;
|
||||
ASSERT_EQ(cudaGetDeviceCount(&count), cudaSuccess);
|
||||
ASSERT_GT(count, 0);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
int attr = 0;
|
||||
ASSERT_EQ(cudaDeviceGetAttribute(&attr, cudaDevAttrMemoryPoolsSupported, i), cudaSuccess);
|
||||
EXPECT_EQ(nanovdb::util::cuda::memoryPoolsSupported(i), attr != 0) << "device " << i;
|
||||
}
|
||||
// Out-of-range device ids are unsupported, not undefined.
|
||||
EXPECT_FALSE(nanovdb::util::cuda::memoryPoolsSupported(-1));
|
||||
EXPECT_FALSE(nanovdb::util::cuda::memoryPoolsSupported(count));
|
||||
}
|
||||
|
||||
TEST(TestUtilCuda, MallocAsyncRoundTrip)
|
||||
{
|
||||
// Allocate/use/free through the wrapper in whichever mode the build selects:
|
||||
// synchronous under the macro, stream-ordered otherwise. Without the macro
|
||||
// the async path requires memory pools, so skip when the device lacks them.
|
||||
int device = 0;
|
||||
ASSERT_EQ(cudaGetDevice(&device), cudaSuccess);
|
||||
#ifndef NANOVDB_USE_SYNC_CUDA_MALLOC
|
||||
if (!nanovdb::util::cuda::memoryPoolsSupported(device))
|
||||
GTEST_SKIP() << "device " << device << " lacks memory pools; the async wrappers fail by design "
|
||||
"(build with NANOVDB_USE_SYNC_CUDA_MALLOC)";
|
||||
#endif
|
||||
cudaStream_t s = nullptr;
|
||||
ASSERT_EQ(cudaStreamCreate(&s), cudaSuccess);
|
||||
|
||||
const size_t n = 4096;
|
||||
void* d = nullptr;
|
||||
ASSERT_EQ(nanovdb::util::cuda::mallocAsync(&d, n, s), cudaSuccess);
|
||||
ASSERT_NE(d, nullptr);
|
||||
ASSERT_EQ(cudaMemsetAsync(d, 0x3C, n, s), cudaSuccess);
|
||||
|
||||
std::vector<unsigned char> host(n, 0);
|
||||
ASSERT_EQ(cudaMemcpyAsync(host.data(), d, n, cudaMemcpyDeviceToHost, s), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamSynchronize(s), cudaSuccess);
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
ASSERT_EQ(host[i], 0x3Cu) << "byte " << i;
|
||||
|
||||
ASSERT_EQ(nanovdb::util::cuda::freeAsync(d, s), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamSynchronize(s), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamDestroy(s), cudaSuccess);
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
#include <cuda.h>
|
||||
#include <cuda_runtime_api.h>
|
||||
#include <vector>
|
||||
#include <nanovdb/util/Util.h> // for stderr and NANOVDB_ASSERT
|
||||
|
||||
// change 1 -> 0 to only perform asserts during debug builds
|
||||
@@ -71,54 +70,23 @@ namespace nanovdb {// =========================================================
|
||||
namespace util::cuda {// ======================================================
|
||||
|
||||
//#define NANOVDB_USE_SYNC_CUDA_MALLOC
|
||||
// cudaMallocAsync and cudaFreeAsync were introduced in CUDA 11.2, and even on newer
|
||||
// toolkits a device may not expose stream-ordered memory pools at runtime — fractional
|
||||
// vGPU configurations commonly disable them, making cudaMallocAsync fail with
|
||||
// cudaErrorNotSupported. The wrappers below therefore behave in two modes: when
|
||||
// CUDA < 11.2 or NANOVDB_USE_SYNC_CUDA_MALLOC is defined they map to synchronous
|
||||
// cudaMalloc/cudaFree; otherwise they use cudaMallocAsync/cudaFreeAsync and, on a
|
||||
// device that lacks memory pools, fail with an actionable diagnostic rather than
|
||||
// silently substituting synchronous allocation (which would make an async resource
|
||||
// misrepresent its own semantics — the choice of a synchronous backend belongs to the
|
||||
// caller, not to a hidden runtime fallback). Callers select synchronous allocation by
|
||||
// defining NANOVDB_USE_SYNC_CUDA_MALLOC, which -- because the wrappers are inline
|
||||
// functions -- must be defined identically in every translation unit to avoid violating
|
||||
// the one-definition rule.
|
||||
|
||||
/// @brief Returns true if @c device supports stream-ordered memory pools, i.e.
|
||||
/// cudaMallocAsync/cudaFreeAsync. Queried once per process for all
|
||||
/// devices and cached; out-of-range device ids return false.
|
||||
inline bool memoryPoolsSupported(int device)
|
||||
{
|
||||
#if (CUDART_VERSION < 11020)
|
||||
(void)device;
|
||||
return false;
|
||||
#else
|
||||
static const auto supported = [] {
|
||||
int count = 0;
|
||||
if (cudaGetDeviceCount(&count) != cudaSuccess || count < 0) count = 0;
|
||||
std::vector<char> s(static_cast<size_t>(count), 0);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
int attr = 0;
|
||||
if (cudaDeviceGetAttribute(&attr, cudaDevAttrMemoryPoolsSupported, i) == cudaSuccess)
|
||||
s[static_cast<size_t>(i)] = char(attr != 0);
|
||||
}
|
||||
return s;
|
||||
}();
|
||||
return device >= 0 && static_cast<size_t>(device) < supported.size() && supported[static_cast<size_t>(device)] != 0;
|
||||
#endif
|
||||
}
|
||||
// cudaMallocAsync and cudaFreeAsync were introduced in CUDA 11.2 so we introduce
|
||||
// custom implementations that map to cudaMalloc and cudaFree below. If NANOVDB_USE_SYNC_CUDA_MALLOC
|
||||
// is defined these implementations will also be defined, which is useful in virtualized environments
|
||||
// that slice up the GPU and share it between instances as vGPU's. GPU unified memory is usually disabled
|
||||
// out of security considerations. Asynchronous CUDA malloc/free depends on GPU unified memory, so it
|
||||
// is not possible to use cudaMallocAsync and cudaFreeAsync in such environments.
|
||||
|
||||
#if (CUDART_VERSION < 11020) || defined(NANOVDB_USE_SYNC_CUDA_MALLOC) // 11.2 introduced cudaMallocAsync and cudaFreeAsync
|
||||
|
||||
/// @brief Wrapper forced to synchronous cudaMalloc; see the mode comment above.
|
||||
/// @brief Simple wrapper that calls cudaMalloc
|
||||
/// @param d_ptr Device pointer to allocated device memory
|
||||
/// @param size Number of bytes to allocate
|
||||
/// @param dummy The stream establishing the stream ordering contract and the memory pool to allocate from (ignored)
|
||||
/// @return Cuda error code
|
||||
inline cudaError_t mallocAsync(void** d_ptr, size_t size, cudaStream_t){return cudaMalloc(d_ptr, size);}
|
||||
|
||||
/// @brief Wrapper forced to synchronous cudaFree; see the mode comment above.
|
||||
/// @brief Simple wrapper that calls cudaFree
|
||||
/// @param d_ptr Device pointer that will be freed
|
||||
/// @param dummy The stream establishing the stream ordering promise (ignored)
|
||||
/// @return Cuda error code
|
||||
@@ -126,43 +94,18 @@ inline cudaError_t freeAsync(void* d_ptr, cudaStream_t){return cudaFree(d_ptr);}
|
||||
|
||||
#else
|
||||
|
||||
/// @brief Wrapper that calls cudaMallocAsync. On a device without stream-ordered
|
||||
/// memory pools it emits an actionable diagnostic and returns
|
||||
/// cudaErrorNotSupported rather than silently allocating synchronously.
|
||||
/// @brief Simple wrapper that calls cudaMallocAsync
|
||||
/// @param d_ptr Device pointer to allocated device memory
|
||||
/// @param size Number of bytes to allocate
|
||||
/// @param stream The stream establishing the stream ordering contract and the memory pool to allocate from
|
||||
/// @return Cuda error code
|
||||
inline cudaError_t mallocAsync(void** d_ptr, size_t size, cudaStream_t stream)
|
||||
{
|
||||
int device = 0;
|
||||
const cudaError_t err = cudaGetDevice(&device);
|
||||
if (err != cudaSuccess) return err;
|
||||
if (!memoryPoolsSupported(device)) {
|
||||
fprintf(stderr,
|
||||
"NanoVDB: device %d does not support stream-ordered CUDA memory pools required by "
|
||||
"cudaMallocAsync. Define NANOVDB_USE_SYNC_CUDA_MALLOC when building to allocate "
|
||||
"synchronously with cudaMalloc/cudaFree instead.\n",
|
||||
device);
|
||||
return cudaErrorNotSupported;
|
||||
}
|
||||
return cudaMallocAsync(d_ptr, size, stream);
|
||||
}
|
||||
inline cudaError_t mallocAsync(void** d_ptr, size_t size, cudaStream_t stream){return cudaMallocAsync(d_ptr, size, stream);}
|
||||
|
||||
/// @brief Wrapper that calls cudaFreeAsync. Mirrors mallocAsync's pool check so a
|
||||
/// device without memory pools reports cudaErrorNotSupported rather than
|
||||
/// calling cudaFreeAsync where it cannot succeed.
|
||||
/// @brief Simple wrapper that calls cudaFreeAsync
|
||||
/// @param d_ptr Device pointer that will be freed
|
||||
/// @param stream The stream establishing the stream ordering promise
|
||||
/// @return Cuda error code
|
||||
inline cudaError_t freeAsync(void* d_ptr, cudaStream_t stream)
|
||||
{
|
||||
int device = 0;
|
||||
const cudaError_t err = cudaGetDevice(&device);
|
||||
if (err != cudaSuccess) return err;
|
||||
if (!memoryPoolsSupported(device)) return cudaErrorNotSupported;
|
||||
return cudaFreeAsync(d_ptr, stream);
|
||||
}
|
||||
inline cudaError_t freeAsync(void* d_ptr, cudaStream_t stream){return cudaFreeAsync(d_ptr, stream);}
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
Reference in New Issue
Block a user