Compare commits
70
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c81d904ded | ||
|
|
a89012d569 | ||
|
|
a14a735de9 | ||
|
|
9df9ec060d | ||
|
|
71da3a862e | ||
|
|
c814fae1fe | ||
|
|
2176f69923 | ||
|
|
83890bd879 | ||
|
|
eb0f1bfd83 | ||
|
|
c6353069c8 | ||
|
|
d19e483564 | ||
|
|
b2b4d30648 | ||
|
|
2a301b1290 | ||
|
|
e68ff4bc8e | ||
|
|
e679862fca | ||
|
|
568538e9ec | ||
|
|
c3ee2009c0 | ||
|
|
7946f17edb | ||
|
|
204fce09fb | ||
|
|
385584be80 | ||
|
|
2728de45a1 | ||
|
|
3f35101729 | ||
|
|
8a8ed7ca3e | ||
|
|
0377b2202d | ||
|
|
5eb1ec0c4d | ||
|
|
157c413b73 | ||
|
|
20615f1b31 | ||
|
|
e88f2f832e | ||
|
|
63d1878f30 | ||
|
|
d422d856d8 | ||
|
|
25715e5c18 | ||
|
|
6b05f50a67 | ||
|
|
ba9a2c0974 | ||
|
|
6717cca14a | ||
|
|
e538a0646b | ||
|
|
586dde77a0 | ||
|
|
bb597a2109 | ||
|
|
b795681895 | ||
|
|
230d640768 | ||
|
|
2ad18b6aeb | ||
|
|
b082b5ec7e | ||
|
|
8a91e1e096 | ||
|
|
457b671c34 | ||
|
|
111b12f9c1 | ||
|
|
f95115304e | ||
|
|
5b5e936723 | ||
|
|
e3351803dc | ||
|
|
92a4f48f8f | ||
|
|
b0b99d6ce7 | ||
|
|
1ca9bb066e | ||
|
|
59fcab450a | ||
|
|
65c9d4884c | ||
|
|
ad251341d0 | ||
|
|
06ad21bf70 | ||
|
|
2e30d61283 | ||
|
|
f20dd2a50e | ||
|
|
72a4bfe64e | ||
|
|
5fcaa5a347 | ||
|
|
44d5e657a7 | ||
|
|
9e6d47cb88 | ||
|
|
bbb19a9c27 | ||
|
|
04c16f94ad | ||
|
|
3cdaa03ba8 | ||
|
|
5bb2eba69d | ||
|
|
0e5fbf3c19 | ||
|
|
465599ddab | ||
|
|
992aace6cf | ||
|
|
fb1c692d05 | ||
|
|
42afea5d40 | ||
|
|
c3cde0fea6 |
+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
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
|
||||
name: TEACHME
|
||||
|
||||
# Compile-checks the NanoVDB TEACHME tutorial against the real headers. Runs
|
||||
# when EITHER the NanoVDB headers or the tutorial change, so an API change that
|
||||
# breaks a lesson code block is caught here (the whole point of the harness).
|
||||
# Compile-only: no GPU is required.
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'feature/**'
|
||||
- 'pr/**'
|
||||
paths:
|
||||
- 'nanovdb/nanovdb/**'
|
||||
- 'doc/nanovdb/TEACHME/**'
|
||||
- '.github/workflows/teachme.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
paths:
|
||||
- 'nanovdb/nanovdb/**'
|
||||
- 'doc/nanovdb/TEACHME/**'
|
||||
- '.github/workflows/teachme.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
# Allow subsequent pushes to the same PR or REF to cancel any previous jobs.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
teachme-docs-compile:
|
||||
runs-on: ${{ (github.repository_owner == 'AcademySoftwareFoundation' && 'ubuntu-22.04-8c-32g-300h') || 'ubuntu-latest' }}
|
||||
name: teachme-docs-compile
|
||||
container:
|
||||
image: aswf/ci-openvdb:2024-clang17.2
|
||||
env:
|
||||
# Compile-only target; matches the arch used by the other NanoVDB jobs.
|
||||
CUDAARCHS: "80"
|
||||
# Each TU compiles the kitchen-sink fixture (most of NanoVDB's heavy
|
||||
# headers), so nvcc is memory-hungry. Cap parallel compiles to stay
|
||||
# within the runner's RAM — unbounded -j OOMs the 8-core/32 GB runner.
|
||||
TEACHME_JOBS: "4"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: setup_cuda_12
|
||||
run: |
|
||||
echo "/usr/local/cuda-12/bin" >> $GITHUB_PATH
|
||||
echo "LD_LIBRARY_PATH=/usr/local/cuda-12/lib64:$LD_LIBRARY_PATH" >> $GITHUB_ENV
|
||||
- name: run_harness
|
||||
# Reference-checks every #include / prose header path, then compiles
|
||||
# every tagged code block. The OpenVDB-dependent blocks are skipped
|
||||
# (still reference-checked) when OpenVDB is not installed in the image.
|
||||
run: bash doc/nanovdb/TEACHME/test/run.sh
|
||||
+1
-1
@@ -6,10 +6,10 @@
|
||||
|
||||
# Admin/General
|
||||
/.github/ @kmuseth @apradhana @jmlait @idclip @danrbailey @richhones
|
||||
/.github/workflows/nanovdb.yml @kmuseth @apradhana
|
||||
/ci/ @kmuseth @apradhana @jmlait @idclip @danrbailey @richhones
|
||||
/cmake/ @kmuseth @apradhana @jmlait @idclip @danrbailey @richhones
|
||||
/doc/ @kmuseth @apradhana @jmlait @idclip @danrbailey @richhones
|
||||
/pendingchanges/ @kmuseth @apradhana @jmlait @idclip @danrbailey @richhones
|
||||
/tsc/ @kmuseth @apradhana @jmlait @idclip @danrbailey @richhones
|
||||
CODEOWNERS @kmuseth @apradhana @jmlait @idclip @danrbailey @richhones
|
||||
LICENSE @kmuseth @apradhana @jmlait @idclip @danrbailey @richhones
|
||||
|
||||
@@ -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}"
|
||||
@@ -0,0 +1,67 @@
|
||||
# TEACHME
|
||||
|
||||
This directory contains interactive lesson documents designed to be loaded by
|
||||
an LLM coding agent (Claude Code, Cursor, or similar) to teach a user NanoVDB
|
||||
interactively.
|
||||
|
||||
The lesson teaches how to *use* NanoVDB to read, build, transform, and render
|
||||
sparse volumetric data on CPU and GPU. It is a single self-contained markdown
|
||||
file the agent reads at the start of a session.
|
||||
|
||||
## Getting started
|
||||
|
||||
The easiest way to use this lesson is with [Claude Code](https://docs.anthropic.com/en/docs/claude-code)
|
||||
(CLI or IDE extension) from the root of an OpenVDB checkout. The agent can
|
||||
read the lesson files *and* the NanoVDB source code, so it can verify API
|
||||
details and help you debug exercises in real time.
|
||||
|
||||
**Example prompts:**
|
||||
|
||||
To learn how to use NanoVDB:
|
||||
|
||||
```
|
||||
Read doc/nanovdb/TEACHME/nanovdb_user_lesson.md and teach me how to use NanoVDB.
|
||||
```
|
||||
|
||||
You can also give the agent context about your background so it can tailor
|
||||
the lesson:
|
||||
|
||||
```
|
||||
Read doc/nanovdb/TEACHME/nanovdb_user_lesson.md and teach me NanoVDB. I have
|
||||
C++ and CUDA experience but I've never used OpenVDB or any sparse 3D data
|
||||
structure before.
|
||||
```
|
||||
|
||||
Keep the matching cheatsheet (`*_cheatsheet.md`) open in your editor while
|
||||
working through exercises — it's a quick reference for the APIs and
|
||||
invariants covered in the lesson.
|
||||
|
||||
## How it works
|
||||
|
||||
The lesson is a self-contained markdown file that serves as both a
|
||||
curriculum and an instructor prompt. The LLM acts as an interactive
|
||||
instructor: teaching concepts module by module, quizzing the student, and
|
||||
adapting to their responses.
|
||||
|
||||
The lesson includes:
|
||||
|
||||
- Teacher instructions (persona, pacing, scope)
|
||||
- Module-by-module curriculum with embedded concepts and code examples
|
||||
- Quiz questions and an answer key
|
||||
- Exercises with progressive difficulty
|
||||
- A capstone project
|
||||
- A reference table at the end
|
||||
|
||||
## Available lessons
|
||||
|
||||
| Lesson | Cheat sheet | Covers |
|
||||
|---|---|---|
|
||||
| [nanovdb_user_lesson.md](nanovdb_user_lesson.md) | [nanovdb_user_cheatsheet.md](nanovdb_user_cheatsheet.md) | Reading `.nvdb` files, `ReadAccessor`, `NodeManager`, math + sampling, HDDA ray-march, GPU kernels with `Grid`, GPU topology builders (`PointsToGrid`, `MeshToGrid`, `DilateGrid`, etc.), `IndexGrid` + `VoxelBlockManager`, OpenVDB ↔ NanoVDB conversion |
|
||||
|
||||
## Not covered
|
||||
|
||||
- The OpenVDB side (the dynamic, mutable tree used at film/sim time) — this
|
||||
lesson treats OpenVDB only at the conversion boundary.
|
||||
- Houdini / Maya integration — outside the scope.
|
||||
- Gaussian splatting and fVDB-specific topics — see the fVDB repo's own
|
||||
TEACHME at <https://github.com/openvdb/fvdb-core/tree/main/docs/TEACHME>.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Build + editor + render artifacts (not source)
|
||||
build/
|
||||
compile_commands.json
|
||||
.clangd
|
||||
*.ppm
|
||||
sphere.png
|
||||
@@ -0,0 +1,27 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(nanovdb_capstone LANGUAGES CXX CUDA)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CUDA_STANDARD 17)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # emit build/compile_commands.json
|
||||
if(NOT DEFINED CMAKE_CUDA_ARCHITECTURES)
|
||||
set(CMAKE_CUDA_ARCHITECTURES 89) # RTX 6000 Ada; override with -DCMAKE_CUDA_ARCHITECTURES=...
|
||||
endif()
|
||||
|
||||
# NanoVDB headers pull in CUDA lambdas / relaxed-constexpr paths.
|
||||
string(APPEND CMAKE_CUDA_FLAGS " --extended-lambda --expt-relaxed-constexpr")
|
||||
|
||||
# This file is at doc/nanovdb/TEACHME/capstone/CMakeLists.txt; the NanoVDB
|
||||
# include root (the dir containing the nanovdb/ header subdir) is the repo's
|
||||
# `nanovdb/` subdir — repo root is four dirs up.
|
||||
get_filename_component(_teachme "${CMAKE_CURRENT_SOURCE_DIR}" DIRECTORY) # TEACHME
|
||||
get_filename_component(_docnv "${_teachme}" DIRECTORY) # doc/nanovdb
|
||||
get_filename_component(_doc "${_docnv}" DIRECTORY) # doc
|
||||
get_filename_component(REPO_ROOT "${_doc}" DIRECTORY) # repo root
|
||||
set(NANOVDB_INCLUDE_ROOT "${REPO_ROOT}/nanovdb")
|
||||
message(STATUS "NanoVDB include root: ${NANOVDB_INCLUDE_ROOT}")
|
||||
|
||||
add_executable(raymarch raymarch.cu)
|
||||
target_include_directories(raymarch PRIVATE "${NANOVDB_INCLUDE_ROOT}")
|
||||
# Binary lands in the build dir (conventional). raymarch writes sphere.ppm to
|
||||
# the current working directory, so run it from wherever you want the output.
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Convert a binary P6 PPM to PNG using only the Python standard library
|
||||
(the container has no ImageMagick / PIL). Usage: python3 ppm2png.py in.ppm out.png"""
|
||||
import sys, struct, zlib, binascii
|
||||
|
||||
def main(src, dst):
|
||||
data = open(src, "rb").read()
|
||||
i = 0
|
||||
def tok(i):
|
||||
n = len(data)
|
||||
while i < n and data[i] in b" \t\n\r": i += 1
|
||||
s = i
|
||||
while i < n and data[i] not in b" \t\n\r": i += 1
|
||||
if s == i: raise ValueError("truncated or non-binary PPM")
|
||||
return data[s:i], i
|
||||
magic, i = tok(i)
|
||||
assert magic == b"P6", "not a binary PPM"
|
||||
w, i = tok(i); h, i = tok(i); mx, i = tok(i)
|
||||
i += 1 # single whitespace after maxval
|
||||
W, H = int(w), int(h)
|
||||
px = data[i:i + W * H * 3]
|
||||
|
||||
def chunk(typ, payload):
|
||||
return (struct.pack(">I", len(payload)) + typ + payload +
|
||||
struct.pack(">I", binascii.crc32(typ + payload) & 0xffffffff))
|
||||
|
||||
raw = bytearray()
|
||||
for y in range(H):
|
||||
raw.append(0) # filter: none
|
||||
raw += px[y * W * 3:(y + 1) * W * 3]
|
||||
png = (b"\x89PNG\r\n\x1a\n"
|
||||
+ chunk(b"IHDR", struct.pack(">IIBBBBB", W, H, 8, 2, 0, 0, 0))
|
||||
+ chunk(b"IDAT", zlib.compress(bytes(raw), 9))
|
||||
+ chunk(b"IEND", b""))
|
||||
open(dst, "wb").write(png)
|
||||
print(f"wrote {dst} ({W}x{H})")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv[1], sys.argv[2])
|
||||
@@ -0,0 +1,111 @@
|
||||
// Capstone scaffold: GPU level-set ray-march renderer.
|
||||
//
|
||||
// The host plumbing is DONE (build SDF, upload to device, launch, read back,
|
||||
// write PPM) and the light direction + ambient level are handed to the kernel
|
||||
// as parameters. Your job is the kernel body — the per-pixel ray-march + shade.
|
||||
// As shipped it compiles and runs but only writes a placeholder gradient.
|
||||
//
|
||||
// Build: cmake -S . -B build && cmake --build build
|
||||
// Run: ./build/raymarch [ambient] [lx ly lz] (writes sphere.ppm)
|
||||
// View: python3 ppm2png.py sphere.ppm sphere.png (then open sphere.png)
|
||||
#include <nanovdb/NanoVDB.h>
|
||||
#include <nanovdb/tools/CreatePrimitives.h>
|
||||
#include <nanovdb/cuda/DeviceBuffer.h>
|
||||
#include <nanovdb/cuda/GridHandle.cuh>
|
||||
#include <nanovdb/math/Ray.h>
|
||||
#include <nanovdb/math/HDDA.h> // nanovdb::math::ZeroCrossing
|
||||
#include <nanovdb/math/Stencils.h> // nanovdb::math::GradStencil
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
using GridT = nanovdb::NanoGrid<float>;
|
||||
|
||||
__global__ void render(const GridT* grid, unsigned char* img, int W, int H,
|
||||
nanovdb::Vec3f lightDir, float ambient)
|
||||
{
|
||||
const int x = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
const int y = blockIdx.y * blockDim.y + threadIdx.y;
|
||||
if (x >= W || y >= H) return;
|
||||
const int pid = (y * W + x) * 3;
|
||||
|
||||
// ===================== YOUR WORK STARTS HERE =====================
|
||||
// Placeholder so it compiles & runs: a horizontal grey gradient.
|
||||
unsigned char shade = (unsigned char)(255.f * x / W);
|
||||
|
||||
// The aesthetic inputs are given to you as parameters: `lightDir` and
|
||||
// `ambient` (set in main, overridable on the command line). You write the
|
||||
// ray-march and the shading math. Signatures are in the cheat sheet.
|
||||
//
|
||||
// 1. Camera ray (WORLD space). Pinhole: an eye point and, per pixel, a
|
||||
// direction through the image plane. You decide the math — remember
|
||||
// aspect ratio and that image y grows downward. Wrap it in a Ray.
|
||||
//
|
||||
// 2. The SDF lives in INDEX space, so convert your world ray to index
|
||||
// space before you trace it. (Ray has a method for this.)
|
||||
//
|
||||
// 3. Find where the ray first crosses the surface (the SDF sign change).
|
||||
// Module 5 gave you a helper in math/HDDA.h that drives an HDDA and
|
||||
// reports the hit voxel, the value, and the ray parameter.
|
||||
//
|
||||
// 4. On a hit, the surface normal is the (normalized) SDF gradient at the
|
||||
// hit voxel. Module 5's gradient stencil computes it. (Watch its
|
||||
// template parameter.)
|
||||
//
|
||||
// 5. Shade: a Lambertian intensity from the normal and the provided
|
||||
// `lightDir`, lifted off the floor by `ambient`; write it as greyscale.
|
||||
// On a MISS, leave the background.
|
||||
// ====================== YOUR WORK ENDS HERE ======================
|
||||
|
||||
img[pid] = shade; img[pid + 1] = shade; img[pid + 2] = shade;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
// Shading parameters passed into the kernel. Defaults, overridable on the
|
||||
// command line: ./raymarch [ambient] [lx ly lz]
|
||||
float ambient = 0.15f;
|
||||
nanovdb::Vec3f lightDir(-0.577f, 0.577f, -0.577f); // (-1,1,-1) normalized
|
||||
if (argc >= 2) ambient = (float)std::atof(argv[1]);
|
||||
if (argc >= 5) lightDir = nanovdb::Vec3f((float)std::atof(argv[2]),
|
||||
(float)std::atof(argv[3]),
|
||||
(float)std::atof(argv[4]));
|
||||
lightDir.normalize();
|
||||
|
||||
// Build a level-set sphere SDF on the host (radius 100, voxel size 1).
|
||||
auto handle = nanovdb::tools::createLevelSetSphere<float>(
|
||||
/*radius=*/100.0, /*center=*/nanovdb::Vec3d(0.0),
|
||||
/*voxelSize=*/1.0, /*halfWidth=*/3.0);
|
||||
|
||||
// Move to the device. copy<DeviceBuffer>() fills the HOST side of the dual
|
||||
// buffer; deviceUpload() pushes it to the GPU — skip it and deviceGrid()
|
||||
// returns nullptr.
|
||||
auto devHandle = handle.copy<nanovdb::cuda::DeviceBuffer>();
|
||||
devHandle.deviceUpload();
|
||||
const GridT* dGrid = devHandle.deviceGrid<float>();
|
||||
if (!dGrid) { std::printf("no device grid\n"); return 1; }
|
||||
|
||||
const int W = 512, H = 512;
|
||||
unsigned char* dImg = nullptr;
|
||||
cudaMalloc(&dImg, size_t(W) * H * 3);
|
||||
const dim3 block(16, 16), gridDim((W + 15) / 16, (H + 15) / 16);
|
||||
render<<<gridDim, block>>>(dGrid, dImg, W, H, lightDir, ambient);
|
||||
cudaDeviceSynchronize();
|
||||
if (auto e = cudaGetLastError(); e != cudaSuccess) {
|
||||
std::printf("CUDA error: %s\n", cudaGetErrorString(e));
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::vector<unsigned char> img(size_t(W) * H * 3);
|
||||
cudaMemcpy(img.data(), dImg, img.size(), cudaMemcpyDeviceToHost);
|
||||
cudaFree(dImg);
|
||||
|
||||
FILE* f = std::fopen("sphere.ppm", "wb");
|
||||
std::fprintf(f, "P6\n%d %d\n255\n", W, H);
|
||||
std::fwrite(img.data(), 1, img.size(), f);
|
||||
std::fclose(f);
|
||||
std::printf("wrote sphere.ppm (%dx%d)\n", W, H);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// Capstone: GPU level-set ray-march renderer.
|
||||
// Build an SDF sphere -> move to device -> one thread per pixel ->
|
||||
// ZeroCrossing to find the surface -> Lambertian shade via SDF gradient
|
||||
// normal -> write PPM.
|
||||
#include <nanovdb/NanoVDB.h>
|
||||
#include <nanovdb/tools/CreatePrimitives.h>
|
||||
#include <nanovdb/cuda/DeviceBuffer.h>
|
||||
#include <nanovdb/cuda/GridHandle.cuh>
|
||||
#include <nanovdb/math/Ray.h>
|
||||
#include <nanovdb/math/HDDA.h> // ZeroCrossing lives here
|
||||
#include <nanovdb/math/Stencils.h> // GradStencil
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
using GridT = nanovdb::NanoGrid<float>;
|
||||
|
||||
__global__ void render(const GridT* grid, unsigned char* img, int W, int H,
|
||||
nanovdb::Vec3f lightDir, float ambient)
|
||||
{
|
||||
const int x = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
const int y = blockIdx.y * blockDim.y + threadIdx.y;
|
||||
if (x >= W || y >= H) return;
|
||||
const int pid = (y * W + x) * 3;
|
||||
|
||||
// --- pinhole camera in WORLD space ---
|
||||
const nanovdb::Vec3f eye(0.f, 0.f, -300.f);
|
||||
const float aspect = float(W) / float(H);
|
||||
const float tanHalfFov = tanf(0.5f * 45.f * 3.14159265f / 180.f);
|
||||
const float u = (2.f * ((x + 0.5f) / W) - 1.f) * tanHalfFov * aspect;
|
||||
const float v = (1.f - 2.f * ((y + 0.5f) / H)) * tanHalfFov; // flip y
|
||||
nanovdb::Vec3f dir(u, v, 1.f);
|
||||
dir.normalize();
|
||||
|
||||
nanovdb::math::Ray<float> wRay(eye, dir);
|
||||
auto iRay = wRay.worldToIndexF(*grid); // ray in index space
|
||||
|
||||
auto acc = grid->getAccessor();
|
||||
nanovdb::Coord ijk;
|
||||
float t = 0.f, val = 0.f;
|
||||
|
||||
unsigned char shade = 0; // background (black)
|
||||
if (nanovdb::math::ZeroCrossing(iRay, acc, ijk, val, t)) {
|
||||
nanovdb::math::GradStencil<GridT> stencil(*grid);
|
||||
stencil.moveTo(ijk);
|
||||
nanovdb::Vec3f n = stencil.gradient(); // SDF gradient = outward normal
|
||||
n.normalize();
|
||||
const float diff = fmaxf(0.f, n.dot(lightDir));
|
||||
const float I = fminf(1.f, ambient + (1.f - ambient) * diff);
|
||||
shade = (unsigned char)(255.f * I); // greyscale
|
||||
}
|
||||
img[pid] = shade; img[pid + 1] = shade; img[pid + 2] = shade;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
// Shading parameters (passed into the kernel). Defaults, overridable on the
|
||||
// command line: ./raymarch [ambient] [lx ly lz]
|
||||
float ambient = 0.15f;
|
||||
nanovdb::Vec3f lightDir(-0.577f, 0.577f, -0.577f); // (-1,1,-1) normalized
|
||||
if (argc >= 2) ambient = (float)std::atof(argv[1]);
|
||||
if (argc >= 5) lightDir = nanovdb::Vec3f((float)std::atof(argv[2]),
|
||||
(float)std::atof(argv[3]),
|
||||
(float)std::atof(argv[4]));
|
||||
lightDir.normalize();
|
||||
|
||||
// 1. Build a level-set sphere SDF on the host (radius 100, voxel size 1).
|
||||
auto handle = nanovdb::tools::createLevelSetSphere<float>(
|
||||
/*radius=*/100.0, /*center=*/nanovdb::Vec3d(0.0),
|
||||
/*voxelSize=*/1.0, /*halfWidth=*/3.0);
|
||||
|
||||
// 2. Move it to the device. copy<DeviceBuffer>() fills the HOST side of the
|
||||
// dual buffer; deviceUpload() pushes the bytes to the device. Only then
|
||||
// is deviceGrid() non-null.
|
||||
auto devHandle = handle.copy<nanovdb::cuda::DeviceBuffer>();
|
||||
devHandle.deviceUpload();
|
||||
const GridT* dGrid = devHandle.deviceGrid<float>();
|
||||
if (!dGrid) { std::printf("no device grid\n"); return 1; }
|
||||
|
||||
// 3. Render.
|
||||
const int W = 512, H = 512;
|
||||
unsigned char* dImg = nullptr;
|
||||
cudaMalloc(&dImg, size_t(W) * H * 3);
|
||||
const dim3 block(16, 16), grid((W + 15) / 16, (H + 15) / 16);
|
||||
render<<<grid, block>>>(dGrid, dImg, W, H, lightDir, ambient);
|
||||
cudaDeviceSynchronize();
|
||||
if (auto e = cudaGetLastError(); e != cudaSuccess) {
|
||||
std::printf("CUDA error: %s\n", cudaGetErrorString(e));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 4. Copy back and write PPM.
|
||||
std::vector<unsigned char> img(size_t(W) * H * 3);
|
||||
cudaMemcpy(img.data(), dImg, img.size(), cudaMemcpyDeviceToHost);
|
||||
cudaFree(dImg);
|
||||
|
||||
FILE* f = std::fopen("sphere.ppm", "wb");
|
||||
std::fprintf(f, "P6\n%d %d\n255\n", W, H);
|
||||
std::fwrite(img.data(), 1, img.size(), f);
|
||||
std::fclose(f);
|
||||
std::printf("wrote sphere.ppm (%dx%d)\n", W, H);
|
||||
return 0;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,592 @@
|
||||
# NanoVDB User Cheat Sheet
|
||||
|
||||
A one-page reference for the APIs covered in `nanovdb_user_lesson.md`. Keep
|
||||
this open while working through exercises.
|
||||
|
||||
---
|
||||
|
||||
## In one paragraph
|
||||
|
||||
NanoVDB is a single-header, read-only, GPU-friendly sparse 3D data
|
||||
structure. The whole grid (one or more) lives in one contiguous,
|
||||
32-byte-aligned byte buffer. `cudaMemcpy` it to the device and it works
|
||||
as-is. Topology is *frozen* after construction; values can change, the set
|
||||
of active voxels cannot. Every primitive is `__hostdev__` (callable from
|
||||
both host and device with identical code).
|
||||
|
||||
---
|
||||
|
||||
## Includes & CMake
|
||||
|
||||
```cpp openvdb
|
||||
#include <nanovdb/NanoVDB.h> // grid, accessor, math
|
||||
#include <nanovdb/GridHandle.h> // owns the buffer
|
||||
#include <nanovdb/HostBuffer.h> // CPU allocator
|
||||
#include <nanovdb/NodeManager.h> // sequential iteration
|
||||
#include <nanovdb/io/IO.h> // file IO
|
||||
#include <nanovdb/math/HDDA.h> // ray-march
|
||||
#include <nanovdb/math/SampleFromVoxels.h> // sampling
|
||||
|
||||
// GPU
|
||||
#include <nanovdb/cuda/DeviceBuffer.h>
|
||||
#include <nanovdb/cuda/GridHandle.cuh>
|
||||
#include <nanovdb/cuda/NodeManager.cuh>
|
||||
|
||||
// Builders & operators (GPU)
|
||||
#include <nanovdb/tools/cuda/PointsToGrid.cuh>
|
||||
#include <nanovdb/tools/cuda/MeshToGrid.cuh>
|
||||
#include <nanovdb/tools/cuda/DilateGrid.cuh>
|
||||
#include <nanovdb/tools/cuda/CoarsenGrid.cuh>
|
||||
#include <nanovdb/tools/cuda/RefineGrid.cuh>
|
||||
#include <nanovdb/tools/cuda/PruneGrid.cuh>
|
||||
#include <nanovdb/tools/cuda/MergeGrids.cuh>
|
||||
#include <nanovdb/tools/cuda/SignedFloodFill.cuh>
|
||||
|
||||
// CPU builders & primitives
|
||||
#include <nanovdb/tools/GridBuilder.h>
|
||||
#include <nanovdb/tools/CreatePrimitives.h>
|
||||
|
||||
// IndexGrid / VBM
|
||||
#include <nanovdb/tools/VoxelBlockManager.h>
|
||||
#include <nanovdb/tools/cuda/VoxelBlockManager.cuh>
|
||||
|
||||
// Stats, validation, blind data
|
||||
#include <nanovdb/tools/GridStats.h>
|
||||
#include <nanovdb/tools/GridChecksum.h>
|
||||
#include <nanovdb/tools/GridValidator.h>
|
||||
#include <nanovdb/tools/cuda/AddBlindData.cuh>
|
||||
|
||||
// OpenVDB bridge (gated behind NANOVDB_USE_OPENVDB)
|
||||
#include <nanovdb/tools/CreateNanoGrid.h>
|
||||
#include <nanovdb/tools/NanoToOpenVDB.h>
|
||||
```
|
||||
|
||||
```cmake
|
||||
find_package(nanovdb CONFIG REQUIRED)
|
||||
target_link_libraries(my_app PRIVATE nanovdb::nanovdb)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build types (`NanoGrid<T>`)
|
||||
|
||||
| `T` | Stored per voxel |
|
||||
|-------------------|-------------------------------------------|
|
||||
| `float`, `double` | Scalar (SDF, density) |
|
||||
| `int32_t`, `int64_t` | Integer |
|
||||
| `Vec3f`, `Vec3d` | 3-vector (velocity, color) |
|
||||
| `bool` | Active state only |
|
||||
| `ValueIndex` | Sequential index over all voxels |
|
||||
| `ValueOnIndex` | Sequential index over **active** voxels |
|
||||
| `ValueMask` | Topology only (no per-voxel values) |
|
||||
| `Half` | 16-bit float |
|
||||
| `Fp4`/`Fp8`/`Fp16`/`FpN` | Quantized float (4/8/16 bits) |
|
||||
| `Point` | Point counts (for point grids) |
|
||||
|
||||
---
|
||||
|
||||
## File IO
|
||||
|
||||
```cpp
|
||||
namespace io = nanovdb::io;
|
||||
|
||||
auto h = io::readGrid("file.nvdb"); // first grid
|
||||
auto h2 = io::readGrid("file.nvdb", 2); // grid #2 (0-based)
|
||||
auto hN = io::readGrid("file.nvdb", "density"); // by name
|
||||
auto hAll = io::readGrid("file.nvdb", -1); // all → one handle
|
||||
auto v = io::readGrids("file.nvdb"); // vector<handle>, one per segment
|
||||
|
||||
io::writeGrid("out.nvdb", h); // single
|
||||
io::writeGrid("out.nvdb", h, io::Codec::BLOSC); // compressed
|
||||
io::writeGrids("out.nvdb", handles); // many
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Inspecting a grid
|
||||
|
||||
```cpp
|
||||
const auto* grid = handle.grid<float>(); // nullptr if T mismatch
|
||||
if (!grid) throw std::runtime_error("type mismatch");
|
||||
|
||||
grid->gridName(); // const char*
|
||||
grid->gridClass(); // GridClass enum (LevelSet, FogVolume, etc.)
|
||||
grid->gridType(); // GridType enum (Float, Vec3f, OnIndex, ...)
|
||||
grid->voxelSize(); // Vec3d
|
||||
grid->activeVoxelCount(); // uint64_t
|
||||
grid->indexBBox(); // CoordBBox
|
||||
grid->worldBBox(); // BBox<Vec3d>
|
||||
grid->tree().background(); // T
|
||||
grid->worldToIndex(p); // Vec3<T>, p is world-space
|
||||
grid->indexToWorld(p); // Vec3<T>, p is index-space (fractional)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Random access (ReadAccessor)
|
||||
|
||||
```cpp
|
||||
// One accessor per thread; not thread-safe; never copy between CPU & GPU
|
||||
auto acc = grid->getAccessor();
|
||||
float v = acc.getValue(nanovdb::Coord(1, 2, 3));
|
||||
bool on = acc.isActive(nanovdb::Coord(1, 2, 3));
|
||||
const auto* leaf = acc.getNode<nanovdb::NanoLeaf<float>>(); // cached leaf ptr, if needed
|
||||
|
||||
// probe* = value/state/leaf-existence in ONE descent (disambiguates the
|
||||
// two "got background" cases: inactive-vs-no-node)
|
||||
float pv;
|
||||
bool active = acc.probeValue(nanovdb::Coord(1,2,3), pv); // pv set; ret = state
|
||||
const nanovdb::NanoLeaf<float>* lf = acc.probeLeaf(nanovdb::Coord(1,2,3)); // nullptr if no leaf
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sequential access (NodeManager)
|
||||
|
||||
```cpp
|
||||
#include <nanovdb/NodeManager.h>
|
||||
|
||||
auto nmh = nanovdb::createNodeManager(*grid);
|
||||
const auto* mgr = nmh.mgr<float>();
|
||||
|
||||
auto leafN = mgr->leafCount();
|
||||
auto lowerN = mgr->lowerCount();
|
||||
auto upperN = mgr->upperCount();
|
||||
|
||||
for (uint64_t i = 0; i < leafN; ++i) {
|
||||
const auto& leaf = mgr->leaf(i);
|
||||
for (auto it = leaf.beginValueOn(); it; ++it) {
|
||||
float v = *it;
|
||||
auto c = it.getCoord();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
GPU. Pass the device-side `NodeManager*` to the kernel and index leaves by
|
||||
block, voxels within a leaf by thread:
|
||||
|
||||
```cpp no-compile
|
||||
#include <nanovdb/cuda/NodeManager.cuh>
|
||||
|
||||
auto dnmh = nanovdb::cuda::createNodeManager<float>(dGrid);
|
||||
const auto* dmgr = dnmh.template deviceMgr<float>(); // device pointer
|
||||
|
||||
// One block per leaf; 64 threads cover the 512 voxels in each leaf.
|
||||
perLeafKernel<<<dmgr->leafCount(), 64>>>(dmgr); // kernel defined just below
|
||||
```
|
||||
|
||||
The kernel:
|
||||
|
||||
```cpp
|
||||
__global__ void perLeafKernel(const nanovdb::NodeManager<float>* mgr)
|
||||
{
|
||||
using LeafT = nanovdb::NanoLeaf<float>;
|
||||
const uint32_t leafIdx = blockIdx.x;
|
||||
if (leafIdx >= mgr->leafCount()) return;
|
||||
|
||||
const LeafT& leaf = mgr->leaf(leafIdx);
|
||||
|
||||
// 512 voxels per leaf (LeafT::SIZE == 1<<3*LOG2DIM == 8^3).
|
||||
for (uint32_t n = threadIdx.x; n < LeafT::SIZE; n += blockDim.x) {
|
||||
if (!leaf.isActive(n)) continue;
|
||||
float v = leaf.getValue(n);
|
||||
nanovdb::Coord c = leaf.offsetToGlobalCoord(n);
|
||||
// ... do something with (v, c)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Same methods exist on `lower(i)` and `upper(i)` (internal nodes) — you'll
|
||||
mostly want `leaf(i)` since that's where actual voxel values live.
|
||||
|
||||
Indexed leaf-voxel API (callable from `__hostdev__` code):
|
||||
|
||||
| Call | Returns |
|
||||
|-------------------------------|-------------------------------------------|
|
||||
| `leaf.getValue(n)` | voxel value at offset `n` (0..511) |
|
||||
| `leaf.isActive(n)` | active state at offset `n` |
|
||||
| `leaf.offsetToGlobalCoord(n)` | the global `Coord` for offset `n` |
|
||||
| `leaf.origin()` | the leaf's `(8,8,8)`-aligned origin Coord |
|
||||
| `LeafT::SIZE` | `512`, voxels per leaf |
|
||||
|
||||
---
|
||||
|
||||
## Math primitives (all `__hostdev__`)
|
||||
|
||||
```cpp
|
||||
nanovdb::Coord c(1, 2, 3);
|
||||
auto c2 = nanovdb::Coord::Floor(nanovdb::Vec3f(1.7f, 2.1f, 3.9f)); // (1, 2, 3)
|
||||
|
||||
nanovdb::Vec3f v(1, 2, 3);
|
||||
auto d = v.dot(v); // 14
|
||||
auto n = v.length(); // sqrt(14)
|
||||
auto u = v.normalize();
|
||||
|
||||
nanovdb::math::BBox<nanovdb::Vec3f> b(nanovdb::Vec3f(0,0,0),
|
||||
nanovdb::Vec3f(10,10,10));
|
||||
b.expand(nanovdb::Vec3f(5,5,5));
|
||||
b.isInside(nanovdb::Vec3f(1,1,1));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sampling
|
||||
|
||||
```cpp
|
||||
#include <nanovdb/math/SampleFromVoxels.h>
|
||||
|
||||
auto acc = grid->getAccessor();
|
||||
auto sampler = nanovdb::math::createSampler<1>(acc); // 0=nearest, 1=trilinear, 2=triquadratic, 3=tricubic
|
||||
auto idxPt = grid->worldToIndex(worldPt);
|
||||
float v = sampler(idxPt);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## HDDA ray-march
|
||||
|
||||
```cpp
|
||||
#include <nanovdb/math/Ray.h>
|
||||
#include <nanovdb/math/HDDA.h>
|
||||
|
||||
auto acc = grid->getAccessor();
|
||||
nanovdb::math::Ray<float> wRay({0,0,-5}, {0,0,1});
|
||||
auto iRay = wRay.worldToIndexF(*grid);
|
||||
|
||||
// High-level: ZeroCrossing drives an HDDA and stops at the first sign change.
|
||||
nanovdb::Coord ijk;
|
||||
float v, t;
|
||||
if (nanovdb::math::ZeroCrossing(iRay, acc, ijk, v, t)) { /* hit at ijk */ }
|
||||
|
||||
// Low-level: HDDA ctor takes the ray AND the start dimension from getDim.
|
||||
nanovdb::Coord start(0, 0, 0);
|
||||
nanovdb::math::HDDA<decltype(iRay), nanovdb::Coord> hdda(iRay, acc.getDim(start, iRay));
|
||||
while (hdda.step()) {
|
||||
if (acc.isActive(hdda.voxel())) break; // first active voxel
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GPU: transfer + kernel
|
||||
|
||||
```cpp
|
||||
#include <nanovdb/cuda/DeviceBuffer.h>
|
||||
#include <nanovdb/cuda/GridHandle.cuh>
|
||||
|
||||
auto devH = hostH.copy<nanovdb::cuda::DeviceBuffer>();
|
||||
devH.deviceUpload(); // REQUIRED: push to device, else deviceGrid()==nullptr
|
||||
const auto* dGrid = devH.deviceGrid<float>();
|
||||
|
||||
__global__ void k(const nanovdb::NanoGrid<float>* g,
|
||||
const nanovdb::Coord* coords, float* out, int N) {
|
||||
int tid = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (tid >= N) return;
|
||||
auto acc = g->getAccessor();
|
||||
out[tid] = acc.getValue(coords[tid]);
|
||||
}
|
||||
|
||||
k<<<(N+127)/128, 128>>>(dGrid, dCoords, dOut, N);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GPU builders
|
||||
|
||||
```cpp
|
||||
// Points → grid (builder-pattern class, not a free function)
|
||||
nanovdb::tools::cuda::PointsToGrid<nanovdb::Point> p2g(/*voxelSize=*/0.05);
|
||||
auto ptsH = p2g.getHandle(dPoints, nPts);
|
||||
const auto* ptsGrid = ptsH.deviceGrid<nanovdb::Point>();
|
||||
|
||||
// Mesh → OnIndexGrid (narrow-band). Verts are Vec3f, triangles Vec3i.
|
||||
const nanovdb::Vec3f* dVerts; size_t vertN;
|
||||
const nanovdb::Vec3i* dTris; size_t triN;
|
||||
nanovdb::tools::cuda::MeshToGrid<nanovdb::ValueOnIndex> m2g(
|
||||
dVerts, vertN, dTris, triN, nanovdb::Map(voxelSize));
|
||||
m2g.setNarrowBandWidth(3.f);
|
||||
auto meshH = m2g.getHandle();
|
||||
const auto* meshGrid = meshH.deviceGrid<nanovdb::ValueOnIndex>();
|
||||
|
||||
// MeshToGrid + per-active-voxel UDF sidecar
|
||||
auto [gridH, udfBuf] = m2g.getHandleAndUDF();
|
||||
```
|
||||
|
||||
## CPU builder
|
||||
|
||||
```cpp
|
||||
// Build a tree on the host, then bake into a packed NanoVDB grid.
|
||||
nanovdb::tools::build::Grid<float> g(/*background=*/0.0f,
|
||||
"my_grid", nanovdb::GridClass::FogVolume);
|
||||
g.setTransform(/*scale=*/0.1); // voxel size
|
||||
auto acc = g.getAccessor(); // not thread-safe
|
||||
acc.setValue(nanovdb::Coord(0, 0, 0), 1.0f);
|
||||
auto h = nanovdb::tools::createNanoGrid(g); // GridHandle<HostBuffer>
|
||||
```
|
||||
|
||||
## Primitive helpers
|
||||
|
||||
```cpp
|
||||
auto h = nanovdb::tools::createLevelSetSphere<float>(
|
||||
/*radius=*/10.0, /*center=*/{0,0,0}, /*voxelSize=*/0.1);
|
||||
// also: createLevelSetTorus, createLevelSetBox, createFogVolumeSphere, ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Topological operators (GPU)
|
||||
|
||||
```cpp
|
||||
// Builder classes (NOT free functions); ValueOnIndex grids only. Construct
|
||||
// from a device grid pointer, getHandle() returns a new GridHandle.
|
||||
using OnIdx = nanovdb::ValueOnIndex;
|
||||
auto* g = devH.deviceGrid<OnIdx>();
|
||||
|
||||
auto dilatedH = nanovdb::tools::cuda::DilateGrid<OnIdx>(g).getHandle();
|
||||
auto coarseH = nanovdb::tools::cuda::CoarsenGrid<OnIdx>(g).getHandle();
|
||||
auto fineH = nanovdb::tools::cuda::RefineGrid<OnIdx>(g).getHandle();
|
||||
const nanovdb::Mask<3>* leafMask = nullptr; // PruneGrid needs a per-leaf mask
|
||||
auto prunedH = nanovdb::tools::cuda::PruneGrid<OnIdx>(g, leafMask).getHandle();
|
||||
auto mergedH = nanovdb::tools::cuda::MergeGrids<OnIdx>(g, g).getHandle();
|
||||
|
||||
// signedFloodFill is the exception: a free function, in place, on a scalar
|
||||
// SDF grid (not OnIndex).
|
||||
nanovdb::tools::cuda::signedFloodFill(devH.deviceGrid<float>());
|
||||
```
|
||||
|
||||
CPU morphology equivalents live under `nanovdb/util/MorphologyHelpers.h`.
|
||||
|
||||
## Re-home sidecar data after a topology change (Injection.cuh)
|
||||
|
||||
A topology op renumbers the active-voxel indices, so the old sidecar no
|
||||
longer matches. Move values across, and find the new voxels:
|
||||
|
||||
```cpp
|
||||
#include <nanovdb/util/cuda/Injection.cuh>
|
||||
#include <nanovdb/util/cuda/DeviceGridTraits.cuh>
|
||||
#include <nanovdb/util/cuda/Util.h>
|
||||
|
||||
using OnIdx = nanovdb::ValueOnIndex;
|
||||
const auto* srcGrid = devH.deviceGrid<OnIdx>(); // old grid
|
||||
const auto* dstGrid = devHandle.deviceGrid<OnIdx>(); // new (e.g. dilated) grid
|
||||
const float* oldSidecar = nullptr;
|
||||
float* newSidecar = nullptr; // size = new activeVoxelCount(+1)
|
||||
|
||||
// Copy overlapping values (non-overlap dst voxels left untouched):
|
||||
auto srcLeaves = nanovdb::util::cuda::DeviceGridTraits<OnIdx>::getTreeData(srcGrid).mNodeCount[0];
|
||||
nanovdb::util::cuda::operatorKernel<
|
||||
nanovdb::util::cuda::InjectGridDataFunctor<OnIdx, float>>
|
||||
<<<srcLeaves, 256>>>(srcGrid, dstGrid, oldSidecar, newSidecar);
|
||||
|
||||
// Per-leaf overlap mask (new voxels = dstMask & ~overlap):
|
||||
auto dstLeaves = nanovdb::util::cuda::DeviceGridTraits<OnIdx>::getTreeData(dstGrid).mNodeCount[0];
|
||||
auto maskBuf = nanovdb::cuda::DeviceBuffer::create(dstLeaves*sizeof(nanovdb::Mask<3>), nullptr, false);
|
||||
auto* overlap = static_cast<nanovdb::Mask<3>*>(maskBuf.deviceData());
|
||||
unsigned nt = 128, nb = (dstLeaves + nt - 1) / nt;
|
||||
nanovdb::util::cuda::lambdaKernel<<<nb, nt>>>(dstLeaves,
|
||||
nanovdb::util::cuda::InjectGridMaskFunctor<OnIdx>(), srcGrid, dstGrid, overlap);
|
||||
```
|
||||
|
||||
Simpler: pre-fill `newSidecar` with a sentinel; slots still holding it after
|
||||
the data inject are the new voxels.
|
||||
|
||||
---
|
||||
|
||||
## Stats, validation & integrity
|
||||
|
||||
Nodes cache bbox/min/max/avg per node. Mutating values in place leaves them
|
||||
**stale** — which silently breaks HDDA space-skipping and ray-tracing.
|
||||
Recompute after any manual edit.
|
||||
|
||||
```cpp
|
||||
#include <nanovdb/tools/GridStats.h> // CPU
|
||||
// #include <nanovdb/tools/cuda/GridStats.cuh> // GPU (device grid + stream)
|
||||
|
||||
nanovdb::NanoGrid<float>* mutableGrid = nullptr; // non-const: it gets written
|
||||
nanovdb::tools::updateGridStats(mutableGrid, nanovdb::tools::StatsMode::All);
|
||||
// StatsMode: Disable | BBox | MinMax | All(=Default)
|
||||
|
||||
// Extrema over an index-space box, computed directly (no cache):
|
||||
auto ex = nanovdb::tools::getExtrema(*grid, nanovdb::CoordBBox(nanovdb::Coord(-10), nanovdb::Coord(10)));
|
||||
float lo = ex.min(), hi = ex.max();
|
||||
```
|
||||
|
||||
Certify a grid after load / build / device transfer:
|
||||
|
||||
```cpp
|
||||
#include <nanovdb/tools/GridChecksum.h>
|
||||
#include <nanovdb/tools/GridValidator.h>
|
||||
|
||||
nanovdb::NanoGrid<float>* mutableGrid = nullptr; // non-const: edited grid
|
||||
|
||||
bool good = nanovdb::tools::validateChecksum(grid, nanovdb::CheckMode::Full);
|
||||
nanovdb::tools::updateChecksum(mutableGrid, nanovdb::CheckMode::Full); // after a legit edit
|
||||
|
||||
bool valid = nanovdb::tools::isValid(grid, nanovdb::CheckMode::Full, /*verbose=*/false);
|
||||
char err[(uint32_t)nanovdb::CheckMode::StrLen] = {}; // CheckMode is scoped → cast
|
||||
nanovdb::tools::checkGrid(grid, err, nanovdb::CheckMode::Full); // err="" if valid
|
||||
// CheckMode: Disable/Empty | Half/Partial/Default | Full
|
||||
// GPU variants: tools/cuda/GridChecksum.cuh, tools/cuda/GridValidator.cuh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## IndexGrid & VBM
|
||||
|
||||
```cpp no-compile
|
||||
// Build a ValueOnIndex grid from an OpenVDB grid
|
||||
auto h = nanovdb::tools::createNanoGrid<openvdb::FloatGrid,
|
||||
nanovdb::ValueOnIndex>(*ovGrid);
|
||||
|
||||
// Or from a mesh on the GPU (already returns OnIndexGrid)
|
||||
nanovdb::tools::cuda::MeshToGrid<nanovdb::ValueOnIndex> mesher(dV, vN, dT, tN, nanovdb::Map(vs));
|
||||
auto devH = mesher.getHandle();
|
||||
const auto* g = devH.deviceGrid<nanovdb::ValueOnIndex>();
|
||||
|
||||
// External attribute array — same length as active voxel count
|
||||
size_t N = g->activeVoxelCount();
|
||||
thrust::device_vector<float> attr(N);
|
||||
|
||||
// Read attribute at a coordinate
|
||||
auto acc = g->getAccessor();
|
||||
uint64_t idx = acc.getValue(c); // index into attr
|
||||
float a = attr[idx];
|
||||
|
||||
// VBM: SIMT-friendly block iteration (Log2BlockWidth >= 6, i.e. >=64/block)
|
||||
auto vbmH = nanovdb::tools::buildVoxelBlockManager<6>(g);
|
||||
// see tools/cuda/VoxelBlockManager.cuh for the GPU decode pattern
|
||||
```
|
||||
|
||||
ChannelAccessor — read the sidecar by attribute, not by index (one typed
|
||||
call instead of getValue(ijk) → array[idx]). Note: IndexT defaults to
|
||||
`ValueIndex`, so spell out `ValueOnIndex`:
|
||||
|
||||
```cpp
|
||||
__global__ void readChan(const nanovdb::NanoGrid<nanovdb::ValueOnIndex>* g,
|
||||
float* dValues, nanovdb::Coord c, float* out) {
|
||||
nanovdb::ChannelAccessor<float, nanovdb::ValueOnIndex> ca(*g, dValues);
|
||||
*out = ca.getValue(c); // attribute value (not the index)
|
||||
// ca(c) same; ca.getIndex(c) for the raw index; ca.probeValue(c, v) for state+value
|
||||
// ctor (*g, channelID) instead reads an internal blind-data channel
|
||||
}
|
||||
```
|
||||
|
||||
Bake a sidecar back into a value grid (round-trip partner of the split;
|
||||
needed by value-typed tools like `signedFloodFill`):
|
||||
|
||||
```cpp
|
||||
#include <nanovdb/tools/cuda/IndexToGrid.cuh>
|
||||
|
||||
const auto* idxGrid = devH.deviceGrid<nanovdb::ValueOnIndex>();
|
||||
const float* dValues = nullptr; // per-active-voxel sidecar
|
||||
auto floatH = nanovdb::tools::cuda::indexToGrid<float>(idxGrid, dValues);
|
||||
const auto* floatGrid = floatH.deviceGrid<float>(); // values now in-grid
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## OpenVDB ↔ NanoVDB
|
||||
|
||||
```cpp openvdb
|
||||
#include <nanovdb/tools/CreateNanoGrid.h>
|
||||
#include <nanovdb/tools/NanoToOpenVDB.h>
|
||||
|
||||
// OpenVDB → NanoVDB (optionally with type change)
|
||||
auto h = nanovdb::tools::createNanoGrid<openvdb::FloatGrid, float>(*ovGrid);
|
||||
auto h_half = nanovdb::tools::createNanoGrid<openvdb::FloatGrid,
|
||||
nanovdb::Half>(*ovGrid);
|
||||
|
||||
// NanoVDB → OpenVDB
|
||||
auto ovGrid = nanovdb::tools::nanoToOpenVDB<float>(handle);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Blind data (attached arrays)
|
||||
|
||||
Read (host or device — `__hostdev__`):
|
||||
|
||||
```cpp
|
||||
uint32_t n = grid->blindDataCount();
|
||||
const nanovdb::GridBlindMetaData& meta = grid->blindMetaData(0);
|
||||
// meta.mValueCount, mValueSize, mDataType, mSemantic, mDataClass, mName
|
||||
|
||||
int id = grid->findBlindDataForSemantic(nanovdb::GridBlindDataSemantic::PointPosition);
|
||||
// or: int id = grid->findBlindData("name");
|
||||
const nanovdb::Vec3f* pos = grid->getBlindData<nanovdb::Vec3f>(id); // nullptr if T mismatch
|
||||
```
|
||||
|
||||
Attach on the GPU (returns a NEW handle with the array appended; fixes the
|
||||
checksum):
|
||||
|
||||
```cpp
|
||||
#include <nanovdb/tools/cuda/AddBlindData.cuh>
|
||||
|
||||
const float* dChannel = nullptr; uint64_t valueCount = 0;
|
||||
auto h = nanovdb::tools::cuda::addBlindData(
|
||||
dGrid, dChannel, valueCount,
|
||||
nanovdb::GridBlindDataClass::ChannelArray,
|
||||
nanovdb::GridBlindDataSemantic::Unknown, "my_channel");
|
||||
const auto* g = h.deviceGrid<float>();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common idioms
|
||||
|
||||
```cpp
|
||||
// Iterate active voxels in a grid (CPU, leaf-by-leaf)
|
||||
auto nmh = nanovdb::createNodeManager(*grid);
|
||||
auto* mgr = nmh.mgr<float>();
|
||||
for (uint64_t L = 0; L < mgr->leafCount(); ++L) {
|
||||
const auto& leaf = mgr->leaf(L);
|
||||
for (auto it = leaf.beginValueOn(); it; ++it) {
|
||||
// *it is the value, it.getCoord() is the ijk
|
||||
}
|
||||
}
|
||||
|
||||
// Sample a grid at a list of world-space points (pts is a std::vector<Vec3f>)
|
||||
auto sampler = nanovdb::math::createSampler<1>(acc);
|
||||
std::vector<float> sampled;
|
||||
for (const auto& w : pts)
|
||||
sampled.push_back(sampler(grid->worldToIndex(w)));
|
||||
|
||||
// Save a level-set sphere to disk
|
||||
auto h = nanovdb::tools::createLevelSetSphere<float>(10.0, {0,0,0}, 0.1);
|
||||
nanovdb::io::writeGrid("sphere.nvdb", h);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- **Type mismatch returns nullptr.** Always check `handle.grid<T>()`.
|
||||
- **Accessors are not thread-safe.** One per thread/warp.
|
||||
- **Accessors don't cross CPU/GPU.** Build inside the kernel.
|
||||
- **Topology is frozen.** To "add a voxel" you must rebuild the grid.
|
||||
- **32-byte alignment.** Don't allocate the buffer yourself with
|
||||
`new char[N]` — use `HostBuffer` or `DeviceBuffer`, which guarantee
|
||||
alignment.
|
||||
- **No runtime `std::` in `__hostdev__` code.** Containers, iostreams,
|
||||
allocators, `std::sort` etc. won't work on device. For type traits and
|
||||
compile-time helpers, NanoVDB uses its own `nanovdb::util::is_same` /
|
||||
`util::enable_if` / `util::conditional` / `util::remove_const` (in
|
||||
`util/Util.h`) — these compile under C++11 across compilers and don't
|
||||
need `--expt-relaxed-constexpr`. Where NanoVDB needs more, it pulls from
|
||||
CCCL's `cuda::std::` (e.g. `cuda::std::numeric_limits` in `math/Math.h`).
|
||||
Plain `std::` compile-time facilities work with relaxed constexpr but
|
||||
NanoVDB's own headers avoid them for portability.
|
||||
- **HDDA is essential for sparse ray-march.** Without it, you're stepping
|
||||
one voxel at a time through empty space.
|
||||
|
||||
---
|
||||
|
||||
## Tools / binaries
|
||||
|
||||
| Tool | Purpose |
|
||||
|---------------------|-------------------------------------------|
|
||||
| `nanovdb_print` | Print metadata for grids in a `.nvdb` |
|
||||
| `nanovdb_convert` | OpenVDB ↔ NanoVDB on the CLI |
|
||||
| `nanovdb_validate` | ABI / format validation |
|
||||
|
||||
Run with `--help` for options.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
# Transient harness outputs
|
||||
gen/
|
||||
build/
|
||||
@@ -0,0 +1,87 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(teachme_tests LANGUAGES CXX CUDA)
|
||||
|
||||
# Compile-only gate: object libraries compile each generated translation unit
|
||||
# to an object file without linking. That is exactly what we want — catch API
|
||||
# errors at compile time; the fixture's extern stand-ins never need to resolve.
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CUDA_STANDARD 17)
|
||||
if(NOT DEFINED CMAKE_CUDA_ARCHITECTURES)
|
||||
set(CMAKE_CUDA_ARCHITECTURES 89) # RTX 6000 Ada; any arch suffices to compile
|
||||
endif()
|
||||
|
||||
# NanoVDB headers pull in CUDA lambdas / relaxed-constexpr code paths.
|
||||
# diag 177 = "function declared but never referenced": every wrapped block
|
||||
# becomes an unreferenced static function on purpose, so silence it.
|
||||
string(APPEND CMAKE_CUDA_FLAGS
|
||||
" --extended-lambda --expt-relaxed-constexpr --diag-suppress 177")
|
||||
|
||||
# NanoVDB include root: this file is doc/nanovdb/TEACHME/test/CMakeLists.txt, so
|
||||
# the repo root is four dirs up. Headers are at nanovdb/nanovdb/NanoVDB.h, so the
|
||||
# include root is the repo's `nanovdb/` subdir (a sibling of `doc/`).
|
||||
get_filename_component(_teachme_dir "${CMAKE_CURRENT_SOURCE_DIR}" DIRECTORY) # TEACHME
|
||||
get_filename_component(_docnv_dir "${_teachme_dir}" DIRECTORY) # doc/nanovdb
|
||||
get_filename_component(_doc_dir "${_docnv_dir}" DIRECTORY) # doc
|
||||
get_filename_component(REPO_ROOT "${_doc_dir}" DIRECTORY) # repo root
|
||||
set(NANOVDB_INCLUDE_ROOT "${REPO_ROOT}/nanovdb")
|
||||
set(OPENVDB_INCLUDE_ROOT "${REPO_ROOT}/openvdb")
|
||||
|
||||
message(STATUS "NanoVDB include root: ${NANOVDB_INCLUDE_ROOT}")
|
||||
|
||||
# Generated TUs + manifest live in ${GEN_DIR} (passed in, defaults to ./gen).
|
||||
if(NOT DEFINED GEN_DIR)
|
||||
set(GEN_DIR "${CMAKE_CURRENT_SOURCE_DIR}/gen")
|
||||
endif()
|
||||
|
||||
set(_manifest "${GEN_DIR}/manifest.txt")
|
||||
if(NOT EXISTS "${_manifest}")
|
||||
message(FATAL_ERROR "No manifest at ${_manifest}. Run the extractor first "
|
||||
"(see run.sh).")
|
||||
endif()
|
||||
|
||||
file(STRINGS "${_manifest}" _lines)
|
||||
set(_core_srcs "")
|
||||
set(_ovdb_srcs "")
|
||||
foreach(_line IN LISTS _lines)
|
||||
if(_line STREQUAL "")
|
||||
continue()
|
||||
endif()
|
||||
separate_arguments(_parts UNIX_COMMAND "${_line}")
|
||||
list(GET _parts 0 _stem)
|
||||
list(GET _parts 1 _needs_ovdb)
|
||||
set(_src "${GEN_DIR}/${_stem}.cu")
|
||||
if(_needs_ovdb STREQUAL "1")
|
||||
list(APPEND _ovdb_srcs "${_src}")
|
||||
else()
|
||||
list(APPEND _core_srcs "${_src}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# Core (NanoVDB-only) translation units.
|
||||
if(_core_srcs)
|
||||
add_library(teachme_blocks OBJECT ${_core_srcs})
|
||||
target_include_directories(teachme_blocks PRIVATE
|
||||
"${NANOVDB_INCLUDE_ROOT}" "${GEN_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
list(LENGTH _core_srcs _n_core)
|
||||
message(STATUS "teachme_blocks: ${_n_core} core TU(s)")
|
||||
endif()
|
||||
|
||||
# OpenVDB-dependent translation units — only if OpenVDB is available.
|
||||
if(_ovdb_srcs)
|
||||
find_package(OpenVDB QUIET)
|
||||
if(OpenVDB_FOUND)
|
||||
add_library(teachme_blocks_openvdb OBJECT ${_ovdb_srcs})
|
||||
target_compile_definitions(teachme_blocks_openvdb PRIVATE NANOVDB_USE_OPENVDB)
|
||||
target_include_directories(teachme_blocks_openvdb PRIVATE
|
||||
"${NANOVDB_INCLUDE_ROOT}" "${OPENVDB_INCLUDE_ROOT}"
|
||||
"${GEN_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
target_link_libraries(teachme_blocks_openvdb PRIVATE OpenVDB::openvdb)
|
||||
list(LENGTH _ovdb_srcs _n_ovdb)
|
||||
message(STATUS "teachme_blocks_openvdb: ${_n_ovdb} OpenVDB TU(s)")
|
||||
else()
|
||||
list(LENGTH _ovdb_srcs _n_ovdb)
|
||||
message(WARNING "OpenVDB not found — skipping ${_n_ovdb} OpenVDB TU(s). "
|
||||
"These blocks are reference-checked but not compiled.")
|
||||
endif()
|
||||
endif()
|
||||
@@ -0,0 +1,89 @@
|
||||
# TEACHME test harness
|
||||
|
||||
Keeps the code in the lesson honest. Two gates:
|
||||
|
||||
1. **Reference validation** (`teachme_tests.py check-refs`) — every
|
||||
`#include <...>` in a code block must resolve to a real header in the
|
||||
NanoVDB tree (or be explicitly exempt). Catches stale-path rot — e.g. a
|
||||
header that was renamed out from under the docs.
|
||||
|
||||
2. **Compilation** (`teachme_tests.py extract` + CMake) — code blocks tagged
|
||||
as compilable are extracted into `.cu` translation units and compiled
|
||||
(compile-only, `-c`) against the real NanoVDB headers. Catches API-shape
|
||||
drift — wrong namespace, a free function that's actually a class, a
|
||||
renamed method, a changed signature.
|
||||
|
||||
Run both with `./run.sh` from this directory.
|
||||
|
||||
## Why compile-only
|
||||
|
||||
The harness compiles (`nvcc -c`) but does not link or run. NanoVDB is
|
||||
header-only and the goal is to catch *API* errors (does this call name a
|
||||
real symbol with a compatible signature?), which all surface at compile
|
||||
time. Skipping the link step means a fixture header can declare the
|
||||
"given" symbols a fragment assumes (`grid`, `acc`, device pointers) as
|
||||
`extern` globals — unresolved at link, but fully type-checked at compile.
|
||||
|
||||
Everything compiles as CUDA (`.cu`) because even host-only examples pull
|
||||
in CUDA code paths (under `__CUDACC__`) when they include headers like
|
||||
`NodeManager.h` or `io/IO.h`.
|
||||
|
||||
## Block tagging
|
||||
|
||||
Code blocks are fenced with an info string: a language followed by
|
||||
whitespace-separated tags, e.g.
|
||||
|
||||
```cpp compile
|
||||
```cpp compile-global continuation
|
||||
```cpp skip-refs
|
||||
|
||||
**Opt-out model:** every `cpp` / `cu` block is compiled by default. A block
|
||||
is wrapped in a function body unless it opens a top-level definition
|
||||
(`__global__`, `template`, `struct`, `class`, `namespace`, `enum`, `using`,
|
||||
`typedef`), in which case it is auto-emitted at file scope. Tags only adjust
|
||||
the defaults:
|
||||
|
||||
| Tag | Meaning |
|
||||
|----------------|------------------------------------------------------------------------------------------|
|
||||
| *(none)* | Compiled. Auto: file scope if it opens a top-level definition, else function-wrapped. |
|
||||
| `no-compile` | Reference-check includes only; do **not** compile. For illustrative/partial fragments. |
|
||||
| `global` | Force file-scope emission (override the auto-detection). |
|
||||
| `main` | Block is already a complete program (its own `main`); compile verbatim. |
|
||||
| `continuation` | Append this block to the previous block's translation unit (shares its symbols). |
|
||||
| `openvdb` | Block needs OpenVDB; compiled only when OpenVDB is found. |
|
||||
| `skip-refs` | Do **not** validate includes in this block — for intentionally-stale counter-examples. |
|
||||
|
||||
A translation unit is a sequence of segments. A non-`continuation` block
|
||||
starts a new TU; each following `continuation` block adds a segment. At emit
|
||||
time, file-scope segments go to file scope (in order) and function segments
|
||||
each get their own wrapper function — so a kernel definition followed by a
|
||||
`continuation` launch line compose into one valid TU.
|
||||
|
||||
Every TU includes `fixture.cuh`, which pulls in the common NanoVDB headers
|
||||
(a deliberately broad "kitchen-sink" set, so terse fragments compile without
|
||||
restating includes) and declares the stand-in "given" symbols (`grid`,
|
||||
`acc`, device pointers, etc.) as `extern`. Including the heavy `tools/cuda/*`
|
||||
headers in every TU makes per-TU compilation slower; that's an accepted
|
||||
trade for letting fragments stay terse. If the suite grows large, switch to
|
||||
per-TU includes driven by each block's own `#include` lines.
|
||||
|
||||
## Coverage
|
||||
|
||||
`check-refs` reports how many code blocks are compile-tagged vs
|
||||
reference-only, so the fraction of the lesson that is actually
|
||||
compile-gated is visible (and ideally climbing).
|
||||
|
||||
## Exemptions
|
||||
|
||||
Includes that are intentionally not real are skipped by the validator:
|
||||
- ellipsis placeholders such as `#include <openvdb/...>`
|
||||
- any include inside a `skip-refs` block (e.g. the deprecated
|
||||
`nanovdb/util/cuda/CudaDeviceBuffer.h` shown as a "this path is stale"
|
||||
counter-example)
|
||||
|
||||
## Files
|
||||
|
||||
- `teachme_tests.py` — the extractor + reference validator
|
||||
- `fixture.cuh` — common headers + `extern` stand-in symbols
|
||||
- `CMakeLists.txt` — compiles the generated TUs
|
||||
- `run.sh` — driver: check-refs → extract → cmake configure+build → report
|
||||
@@ -0,0 +1,92 @@
|
||||
// fixture.cuh — common headers + stand-in "given" symbols for TEACHME
|
||||
// code-block compilation.
|
||||
//
|
||||
// Lesson fragments assume context: "you have a grid", "you have device
|
||||
// points", etc. The harness compiles each block (compile-only, never linked)
|
||||
// with this header in scope, so those assumed symbols need declarations with
|
||||
// the right *types* — enough for the compiler to type-check member calls and
|
||||
// catch API drift. They are declared `extern` (no definitions): a block that
|
||||
// declares its own local `grid`/`acc`/etc. simply shadows the extern, and
|
||||
// since we never link, the unresolved externs cost nothing.
|
||||
//
|
||||
// Grow this file as the tagging pass surfaces more assumed names. Keep the
|
||||
// types matching how the lesson uses each symbol.
|
||||
|
||||
#pragma once
|
||||
|
||||
// ---- common headers (header guards make re-includes in blocks harmless) ----
|
||||
// Core read path + IO + math + CUDA.
|
||||
#include <nanovdb/NanoVDB.h>
|
||||
#include <nanovdb/GridHandle.h>
|
||||
#include <nanovdb/HostBuffer.h>
|
||||
#include <nanovdb/NodeManager.h>
|
||||
#include <nanovdb/io/IO.h>
|
||||
#include <nanovdb/math/Ray.h>
|
||||
#include <nanovdb/math/HDDA.h>
|
||||
#include <nanovdb/math/SampleFromVoxels.h>
|
||||
#include <nanovdb/math/Stencils.h>
|
||||
#include <nanovdb/cuda/DeviceBuffer.h>
|
||||
#include <nanovdb/cuda/GridHandle.cuh>
|
||||
#include <nanovdb/cuda/NodeManager.cuh>
|
||||
// Commonly-assumed "tools" headers — ambient so terse fragments compile
|
||||
// without restating the include. (OpenVDB-conversion headers are NOT here;
|
||||
// blocks that need them are tagged `openvdb` and include them explicitly.)
|
||||
#include <nanovdb/tools/CreatePrimitives.h>
|
||||
#include <nanovdb/tools/CreateNanoGrid.h>
|
||||
#include <nanovdb/tools/GridBuilder.h>
|
||||
#include <nanovdb/tools/VoxelBlockManager.h>
|
||||
#include <nanovdb/tools/cuda/PointsToGrid.cuh>
|
||||
#include <nanovdb/tools/cuda/MeshToGrid.cuh>
|
||||
#include <nanovdb/tools/cuda/DilateGrid.cuh>
|
||||
#include <nanovdb/tools/cuda/CoarsenGrid.cuh>
|
||||
#include <nanovdb/tools/cuda/RefineGrid.cuh>
|
||||
#include <nanovdb/tools/cuda/PruneGrid.cuh>
|
||||
#include <nanovdb/tools/cuda/MergeGrids.cuh>
|
||||
#include <nanovdb/tools/cuda/SignedFloodFill.cuh>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
|
||||
// ---- stand-in "given" symbols (extern: declared, never defined) ----
|
||||
// Handles
|
||||
extern nanovdb::GridHandle<nanovdb::HostBuffer> handle;
|
||||
extern nanovdb::GridHandle<nanovdb::HostBuffer> hostH;
|
||||
extern nanovdb::GridHandle<nanovdb::cuda::DeviceBuffer> devH;
|
||||
extern nanovdb::GridHandle<nanovdb::cuda::DeviceBuffer> devHandle;
|
||||
extern std::vector<nanovdb::GridHandle<nanovdb::HostBuffer>> handles;
|
||||
|
||||
// Grids (host + device pointers into a float grid)
|
||||
extern const nanovdb::NanoGrid<float>* grid;
|
||||
extern const nanovdb::NanoGrid<float>* dGrid;
|
||||
|
||||
// A read accessor + sampler over the float grid (fragments often assume these
|
||||
// already exist from a prior step).
|
||||
extern nanovdb::DefaultReadAccessor<float> acc;
|
||||
|
||||
// Device point / coord / output buffers used by kernel launches.
|
||||
// dPoints is non-const: PointsToGrid<Point> writes input points into the
|
||||
// grid's blind data via getPoint<element_type>(...) = ..., which requires a
|
||||
// non-const element type.
|
||||
extern nanovdb::Vec3f* dPoints;
|
||||
extern std::size_t nPts;
|
||||
extern const nanovdb::Vec3f* dPts;
|
||||
extern const nanovdb::Vec3f* worldPts;
|
||||
extern const nanovdb::Coord* dCoords;
|
||||
extern const nanovdb::Coord* coords;
|
||||
extern float* dOut;
|
||||
extern float* out;
|
||||
extern int N;
|
||||
|
||||
// Host-side query containers + single query points
|
||||
extern std::vector<nanovdb::Vec3f> pts;
|
||||
extern std::vector<nanovdb::Coord> queries;
|
||||
extern nanovdb::Vec3f worldPt; // a single world-space query point
|
||||
extern nanovdb::Vec3f p; // generic world-space point
|
||||
|
||||
// Misc scalars
|
||||
extern double voxelSize;
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
# Drive the TEACHME test harness: reference-check -> extract -> compile.
|
||||
#
|
||||
# Usage: ./run.sh
|
||||
# Exits nonzero if any reference is dangling or any compile-tagged block
|
||||
# fails to compile.
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TEACHME="$(dirname "$HERE")"
|
||||
GEN="$HERE/gen"
|
||||
BUILD="$HERE/build"
|
||||
|
||||
MD=(
|
||||
"$TEACHME/nanovdb_user_lesson.md"
|
||||
"$TEACHME/nanovdb_user_cheatsheet.md"
|
||||
"$TEACHME/README.md"
|
||||
)
|
||||
|
||||
echo "==> [1/3] reference check"
|
||||
python3 "$HERE/teachme_tests.py" check-refs "${MD[@]}"
|
||||
|
||||
echo "==> [2/3] extract compile-tagged blocks"
|
||||
rm -rf "$GEN"
|
||||
mkdir -p "$GEN"
|
||||
python3 "$HERE/teachme_tests.py" extract "${MD[@]}" --out "$GEN"
|
||||
|
||||
n_tu=$(find "$GEN" -name '*.cu' | wc -l | tr -d ' ')
|
||||
if [ "$n_tu" -eq 0 ]; then
|
||||
echo "==> [3/3] compile: no compile-tagged blocks yet — nothing to build"
|
||||
echo "PASS (reference check only)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "==> [3/3] compile $n_tu translation unit(s)"
|
||||
mkdir -p "$BUILD"
|
||||
cmake -S "$HERE" -B "$BUILD" -DGEN_DIR="$GEN" -DCMAKE_BUILD_TYPE=Release \
|
||||
> "$BUILD/configure.log" 2>&1 || { cat "$BUILD/configure.log"; exit 1; }
|
||||
|
||||
# Cap parallelism: each TU pulls in the kitchen-sink fixture (most of NanoVDB's
|
||||
# heavy headers), so nvcc is memory-hungry. Default to all cores locally; CI
|
||||
# sets TEACHME_JOBS lower so the parallel nvcc processes stay within the
|
||||
# runner's RAM (8 unbounded jobs OOM a 32 GB runner).
|
||||
jobs="${TEACHME_JOBS:-$(nproc)}"
|
||||
|
||||
# Keep going (--keep-going / -k) so every failing TU is reported, not just the
|
||||
# first. Capture cmake's status directly (don't pipe through tee, which would
|
||||
# mask it).
|
||||
set +e
|
||||
cmake --build "$BUILD" --parallel "$jobs" -- -k > "$BUILD/build.log" 2>&1
|
||||
status=$?
|
||||
set -e
|
||||
cat "$BUILD/build.log"
|
||||
if [ "$status" -eq 0 ]; then
|
||||
echo "PASS: all $n_tu translation unit(s) compiled"
|
||||
else
|
||||
echo "FAIL: one or more translation units did not compile (see above)"
|
||||
exit 1
|
||||
fi
|
||||
Executable
+319
@@ -0,0 +1,319 @@
|
||||
#!/usr/bin/env python3
|
||||
"""TEACHME lesson test harness: reference validation + block extraction.
|
||||
|
||||
Two subcommands:
|
||||
|
||||
check-refs <md...> Validate every #include in code blocks resolves
|
||||
against the NanoVDB include root. Reports
|
||||
compile-coverage. Exits nonzero on dangling refs.
|
||||
|
||||
extract <md...> --out <dir> Extract compile-tagged code blocks into .cu
|
||||
translation units under <dir>, ready for CMake.
|
||||
|
||||
See README.md for the block tagging convention.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
# Repo layout: this file is at doc/nanovdb/TEACHME/test/teachme_tests.py, so the
|
||||
# repo root is four levels up. The NanoVDB include root (the dir that contains
|
||||
# the `nanovdb/` header subdir) is the repo's `nanovdb/`; OpenVDB's is `openvdb/`.
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
REPO_ROOT = os.path.abspath(os.path.join(HERE, "..", "..", "..", ".."))
|
||||
NANOVDB_INCLUDE_ROOT = os.path.join(REPO_ROOT, "nanovdb")
|
||||
OPENVDB_INCLUDE_ROOT = os.path.join(REPO_ROOT, "openvdb")
|
||||
INCLUDE_ROOTS = [NANOVDB_INCLUDE_ROOT, OPENVDB_INCLUDE_ROOT]
|
||||
|
||||
FENCE_RE = re.compile(r"^```(\w+)?(.*)$")
|
||||
INCLUDE_RE = re.compile(r'^\s*#\s*include\s*<([^>]+)>')
|
||||
|
||||
# Inline-code spans in prose that look like header paths, e.g.
|
||||
# `nanovdb/tools/GridBuilder.h`. Used to catch stale "Read: ..." references.
|
||||
PROSE_PATH_RE = re.compile(r'`((?:nanovdb|openvdb)/[\w./]+\.(?:h|cuh|cc|cpp))`')
|
||||
# A line carrying this marker is exempt from prose-path checking (for
|
||||
# intentionally-stale counter-examples).
|
||||
STALE_OK = "teachme:stale-ok"
|
||||
|
||||
|
||||
def resolves(inc):
|
||||
"""True if an include path exists under any include root."""
|
||||
return any(os.path.isfile(os.path.join(r, inc)) for r in INCLUDE_ROOTS)
|
||||
|
||||
# Languages whose blocks we care about.
|
||||
CODE_LANGS = {"cpp", "cu", "c++"}
|
||||
|
||||
# Opt-OUT model: every cpp/cu block is compiled unless tagged `no-compile`.
|
||||
# A line that begins a file-scope definition forces file-scope emission even
|
||||
# without an explicit `global` tag.
|
||||
FILE_SCOPE_RE = re.compile(r'^\s*(template\b|struct\b|class\b|namespace\b|'
|
||||
r'__global__\b|enum\b|using\b|typedef\b)')
|
||||
|
||||
|
||||
class Block:
|
||||
__slots__ = ("path", "start_line", "lang", "tags", "lines")
|
||||
|
||||
def __init__(self, path, start_line, lang, tags):
|
||||
self.path = path
|
||||
self.start_line = start_line
|
||||
self.lang = lang
|
||||
self.tags = tags
|
||||
self.lines = []
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
return "".join(self.lines)
|
||||
|
||||
|
||||
def parse_blocks(path):
|
||||
"""Yield Block objects for every fenced code block in a markdown file."""
|
||||
blocks = []
|
||||
with open(path, encoding="utf-8") as f:
|
||||
lines = f.readlines()
|
||||
i = 0
|
||||
in_block = None
|
||||
for lineno, line in enumerate(lines, 1):
|
||||
m = FENCE_RE.match(line)
|
||||
if m:
|
||||
if in_block is None:
|
||||
lang = (m.group(1) or "").lower()
|
||||
tags = set(m.group(2).split())
|
||||
in_block = Block(path, lineno, lang, tags)
|
||||
else:
|
||||
blocks.append(in_block)
|
||||
in_block = None
|
||||
continue
|
||||
if in_block is not None:
|
||||
in_block.lines.append(line)
|
||||
if in_block is not None:
|
||||
sys.stderr.write(f"{path}: unterminated code fence opened at "
|
||||
f"line {in_block.start_line}\n")
|
||||
return blocks
|
||||
|
||||
|
||||
def is_exempt_include(inc):
|
||||
"""Includes that are intentionally not real headers."""
|
||||
if "..." in inc: # ellipsis placeholder, e.g. openvdb/...
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def scan_prose_paths(path):
|
||||
"""Yield (lineno, token) for header-path references in prose inline code,
|
||||
skipping lines inside fenced code blocks and lines marked stale-ok."""
|
||||
with open(path, encoding="utf-8") as f:
|
||||
lines = f.readlines()
|
||||
in_fence = False
|
||||
for lineno, line in enumerate(lines, 1):
|
||||
if FENCE_RE.match(line) and line.lstrip().startswith("```"):
|
||||
in_fence = not in_fence
|
||||
continue
|
||||
if in_fence or STALE_OK in line:
|
||||
continue
|
||||
for tok in PROSE_PATH_RE.findall(line):
|
||||
yield lineno, tok
|
||||
|
||||
|
||||
def cmd_check_refs(args):
|
||||
code_blocks = 0
|
||||
compiled_blocks = 0
|
||||
dangling = [] # (path, lineno, inc, kind)
|
||||
for path in args.md:
|
||||
# 1) #include lines inside code blocks
|
||||
for b in parse_blocks(path):
|
||||
if b.lang not in CODE_LANGS:
|
||||
continue
|
||||
code_blocks += 1
|
||||
if "no-compile" not in b.tags:
|
||||
compiled_blocks += 1
|
||||
if "skip-refs" in b.tags:
|
||||
continue
|
||||
for off, line in enumerate(b.lines):
|
||||
im = INCLUDE_RE.match(line)
|
||||
if not im:
|
||||
continue
|
||||
inc = im.group(1).strip()
|
||||
if is_exempt_include(inc):
|
||||
continue
|
||||
if not resolves(inc):
|
||||
dangling.append((path, b.start_line + 1 + off, inc, "include"))
|
||||
# 2) header-path references in prose ("Read: `nanovdb/.../X.h`")
|
||||
for lineno, tok in scan_prose_paths(path):
|
||||
if not resolves(tok):
|
||||
dangling.append((path, lineno, tok, "prose"))
|
||||
|
||||
if dangling:
|
||||
sys.stderr.write("DANGLING REFERENCES:\n")
|
||||
for path, lineno, ref, kind in dangling:
|
||||
rel = os.path.relpath(path)
|
||||
sys.stderr.write(f" {rel}:{lineno}: [{kind}] {ref!r} does not "
|
||||
f"resolve under any include root\n")
|
||||
|
||||
pct = (100.0 * compiled_blocks / code_blocks) if code_blocks else 0.0
|
||||
print(f"code blocks: {code_blocks} compiled: {compiled_blocks} "
|
||||
f"({pct:.0f}%) no-compile: {code_blocks - compiled_blocks}")
|
||||
if dangling:
|
||||
print(f"FAIL: {len(dangling)} dangling reference(s)")
|
||||
return 1
|
||||
print("OK: all references resolve")
|
||||
return 0
|
||||
|
||||
|
||||
def split_block(block):
|
||||
"""Split one block's lines into ordered (kind, lines) sub-segments.
|
||||
|
||||
`kind` is 'global' (file scope) or 'fn' (function body). Explicit `global`
|
||||
/ `main` tags force the whole block. Otherwise we brace-walk: a top-level
|
||||
definition (matched by FILE_SCOPE_RE at brace depth 0, e.g. a __global__
|
||||
kernel) becomes a file-scope chunk spanning until its braces balance;
|
||||
everything else at depth 0 (launches, statements) becomes a function chunk.
|
||||
This lets a single block mix a kernel definition and its launch.
|
||||
"""
|
||||
if "global" in block.tags:
|
||||
return [("global", block.lines)]
|
||||
if "main" in block.tags:
|
||||
return [("main", block.lines)]
|
||||
|
||||
segs = []
|
||||
fn_buf = []
|
||||
lines = block.lines
|
||||
i, n = 0, len(lines)
|
||||
|
||||
def flush_fn():
|
||||
if fn_buf:
|
||||
segs.append(("fn", fn_buf[:]))
|
||||
fn_buf.clear()
|
||||
|
||||
while i < n:
|
||||
line = lines[i]
|
||||
stripped = line.lstrip()
|
||||
if stripped.startswith("#"): # preprocessor: neutral (hoisted later)
|
||||
fn_buf.append(line)
|
||||
i += 1
|
||||
continue
|
||||
if FILE_SCOPE_RE.match(line): # start of a top-level definition
|
||||
flush_fn()
|
||||
chunk = []
|
||||
depth = 0
|
||||
saw_brace = False
|
||||
while i < n:
|
||||
l = lines[i]
|
||||
chunk.append(l)
|
||||
i += 1
|
||||
depth += l.count("{") - l.count("}")
|
||||
if "{" in l:
|
||||
saw_brace = True
|
||||
if saw_brace and depth <= 0:
|
||||
break
|
||||
if not saw_brace and l.rstrip().endswith(";"):
|
||||
break # brace-less decl, e.g. `using X = ...;`
|
||||
segs.append(("global", chunk))
|
||||
continue
|
||||
fn_buf.append(line) # ordinary statement → function body
|
||||
i += 1
|
||||
flush_fn()
|
||||
return segs if segs else [("fn", lines)]
|
||||
|
||||
|
||||
def cmd_extract(args):
|
||||
os.makedirs(args.out, exist_ok=True)
|
||||
# A TU has a list of (kind, lines, line_no) sub-segments + an openvdb flag.
|
||||
tus = []
|
||||
current = None
|
||||
|
||||
for path in args.md:
|
||||
for b in parse_blocks(path):
|
||||
if b.lang not in CODE_LANGS:
|
||||
continue
|
||||
# Opt-out: compile everything except blocks tagged no-compile.
|
||||
if "no-compile" in b.tags:
|
||||
continue
|
||||
subsegs = [(k, lines, b.start_line) for (k, lines) in split_block(b)]
|
||||
cont = "continuation" in b.tags
|
||||
if cont and current is not None:
|
||||
current["segs"].extend(subsegs)
|
||||
if "openvdb" in b.tags:
|
||||
current["openvdb"] = True
|
||||
else:
|
||||
current = {
|
||||
"segs": list(subsegs),
|
||||
"openvdb": "openvdb" in b.tags,
|
||||
"src": os.path.basename(path),
|
||||
"line": b.start_line,
|
||||
}
|
||||
tus.append(current)
|
||||
|
||||
manifest = []
|
||||
for n, tu in enumerate(tus):
|
||||
stem = f"tu_{n:03d}_{os.path.splitext(tu['src'])[0]}_{tu['line']}"
|
||||
fname = os.path.join(args.out, stem + ".cu")
|
||||
# Hoist all preprocessor directives (#include/#define/#pragma) to file
|
||||
# scope, deduped in first-seen order, so function-wrapped segments
|
||||
# never carry an #include into block scope (illegal for headers that
|
||||
# define static __device__ functions).
|
||||
hoisted = []
|
||||
seen_hoist = set()
|
||||
seg_render = [] # (kind, body_lines, line)
|
||||
for kind, lines, line in tu["segs"]:
|
||||
body = []
|
||||
for ln in lines:
|
||||
if ln.lstrip().startswith("#"):
|
||||
if ln not in seen_hoist:
|
||||
seen_hoist.add(ln)
|
||||
hoisted.append(ln)
|
||||
else:
|
||||
body.append(ln)
|
||||
if body:
|
||||
seg_render.append((kind, body, line))
|
||||
|
||||
out = ['#include "fixture.cuh"\n']
|
||||
out += hoisted
|
||||
out.append("\n")
|
||||
fn_count = 0
|
||||
for kind, body, line in seg_render:
|
||||
out.append(f"// --- from {tu['src']}:{line} ---\n")
|
||||
if kind in ("global", "main"):
|
||||
out += body
|
||||
else: # fn
|
||||
out.append(f"static void _seg_{fn_count}() {{\n")
|
||||
out += body
|
||||
out.append("\n}\n")
|
||||
fn_count += 1
|
||||
out.append("\n")
|
||||
with open(fname, "w", encoding="utf-8") as f:
|
||||
f.writelines(out)
|
||||
manifest.append((stem, tu["openvdb"]))
|
||||
|
||||
# Emit a manifest CMake can read: <stem> <needs_openvdb 0|1>
|
||||
with open(os.path.join(args.out, "manifest.txt"), "w") as f:
|
||||
for stem, ovdb in manifest:
|
||||
f.write(f"{stem} {1 if ovdb else 0}\n")
|
||||
|
||||
n_ovdb = sum(1 for _, o in manifest if o)
|
||||
print(f"extracted {len(manifest)} translation unit(s) "
|
||||
f"({n_ovdb} need OpenVDB) into {args.out}")
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
p1 = sub.add_parser("check-refs", help="validate #include references")
|
||||
p1.add_argument("md", nargs="+", help="markdown files")
|
||||
p1.set_defaults(func=cmd_check_refs)
|
||||
|
||||
p2 = sub.add_parser("extract", help="extract compile-tagged blocks to .cu")
|
||||
p2.add_argument("md", nargs="+", help="markdown files")
|
||||
p2.add_argument("--out", required=True, help="output directory")
|
||||
p2.set_defaults(func=cmd_extract)
|
||||
|
||||
args = ap.parse_args()
|
||||
sys.exit(args.func(args))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -6,3 +6,7 @@ Welcome to the NanoVDB documentation page.
|
||||
* @subpage NanoVDB_FAQ
|
||||
* @subpage NanoVDB_SourceTree
|
||||
* @subpage NanoVDB_HelloWorld
|
||||
|
||||
For a hands-on introduction, point an LLM coding agent (Claude Code, Cursor, or
|
||||
similar) at the [TEACHME lessons](https://github.com/AcademySoftwareFoundation/openvdb/tree/master/doc/nanovdb/TEACHME)
|
||||
and ask it to teach you the NanoVDB API interactively.
|
||||
|
||||
@@ -31,6 +31,7 @@ option(NANOVDB_BUILD_PYTHON_MODULE "Build the nanovdb Python module" OFF)
|
||||
option(NANOVDB_USE_INTRINSICS "Build with hardware intrinsics support" OFF)
|
||||
option(NANOVDB_USE_CUDA "Build with CUDA support" OFF)
|
||||
option(NANOVDB_CUDA_KEEP_PTX "Keep CUDA PTX" OFF)
|
||||
option(NANOVDB_CUDA_WERROR "Treat all NVCC diagnostics as errors" ON)
|
||||
option(NANOVDB_USE_OLD_ACCESSOR "Use the old (buggy) ReadAccessor instead of the new fixed one" OFF)
|
||||
|
||||
option(NANOVDB_USE_OPENVDB "Build with OpenVDB support" OFF)
|
||||
@@ -96,6 +97,21 @@ if(NANOVDB_USE_CUDA)
|
||||
|
||||
set(CMAKE_CUDA_FLAGS "${NANOVDB_CUDA_EXTENDED_LAMBDA} -use_fast_math -lineinfo ${CMAKE_CUDA_FLAGS}")
|
||||
|
||||
if(NANOVDB_CUDA_WERROR OR OPENVDB_CXX_STRICT)
|
||||
# Device-side analog of the host -Werror configured in OpenVDBCXX.cmake:
|
||||
# every nvcc front-end diagnostic becomes an error. NANOVDB_CUDA_WERROR
|
||||
# enables it independently of the host-side strict flags.
|
||||
set(CMAKE_CUDA_FLAGS "--Werror=all-warnings ${CMAKE_CUDA_FLAGS}")
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
# NVCC analog of the /wd4251 and /wd4275 suppressions in OpenVDBCXX.cmake:
|
||||
# it's not possible to use STL types in DLL interfaces in a portable and
|
||||
# reliable way, so the dll-interface diagnostics (#1394 and #1388) fire on
|
||||
# every include of the OpenVDB core headers from a CUDA source.
|
||||
set(CMAKE_CUDA_FLAGS "--diag-suppress=1388,1394 ${CMAKE_CUDA_FLAGS}")
|
||||
endif()
|
||||
|
||||
# workaround for win32 bug when nvcc "--keep" is used.
|
||||
if(WIN32)
|
||||
if(NANOVDB_CUDA_KEEP_PTX)
|
||||
|
||||
+208
-51
@@ -8,8 +8,9 @@
|
||||
|
||||
\date January 8, 2020
|
||||
|
||||
\brief Defines GridHandle, which manages a host, and possibly a device,
|
||||
memory buffer containing one or more NanoVDB grids.
|
||||
\brief Defines GridHandle, which manages a memory buffer containing one
|
||||
or more NanoVDB grids: host-resident, dual host/device, or a
|
||||
single-space device-only buffer.
|
||||
*/
|
||||
|
||||
#ifndef NANOVDB_GRID_HANDLE_H_HAS_BEEN_INCLUDED
|
||||
@@ -30,18 +31,63 @@ namespace nanovdb {
|
||||
|
||||
struct GridHandleMetaData {uint64_t offset, size; GridType gridType;};
|
||||
|
||||
namespace detail {
|
||||
|
||||
/// @brief Allocates @c bytes of host-readable storage for a GridHandle:
|
||||
/// through @c BufferT::create for buffers that provide it, and through
|
||||
/// @c pool's resource for a host-accessible single-space buffer (e.g. a
|
||||
/// pinned-resource cuda::Buffer). The braced third argument selects the
|
||||
/// uninitialized-storage constructor without naming its tag type, so
|
||||
/// this header stays CUDA-free.
|
||||
template<typename BufferT>
|
||||
inline BufferT createHostStorage(uint64_t bytes, const BufferT& pool)
|
||||
{
|
||||
if constexpr (BufferHasHostSingle<BufferT>::value && BufferHasStream<BufferT>::value) {
|
||||
return BufferT(pool.stream(), pool.resource(), bytes, {});// stream-ordered resource: allocate on the pool's retained stream
|
||||
} else if constexpr (BufferHasHostSingle<BufferT>::value) {
|
||||
return BufferT(pool.resource(), bytes, {});
|
||||
} else {
|
||||
return BufferT::create(bytes, &pool);
|
||||
}
|
||||
}
|
||||
|
||||
}// namespace detail
|
||||
|
||||
/// @brief This class serves to manage a buffer containing one or more NanoVDB Grids.
|
||||
///
|
||||
/// @note It is important to note that this class does NOT depend on OpenVDB.
|
||||
template<typename BufferT = HostBuffer>
|
||||
class GridHandle
|
||||
{
|
||||
static_assert(!(BufferTraits<BufferT>::hasDeviceDual && BufferHasDeviceSingle<BufferT>::value),
|
||||
"a buffer cannot be both dual-space and single-space");
|
||||
static_assert(!(BufferHasDeviceSingle<BufferT>::value || BufferHasHostSingle<BufferT>::value)
|
||||
|| BufferHasByteElements<BufferT>::value,
|
||||
"GridHandle requires byte-addressed single-space storage, e.g. cuda::Buffer<std::byte, R>");
|
||||
|
||||
std::vector<GridHandleMetaData> mMetaData;
|
||||
BufferT mBuffer;
|
||||
|
||||
template <typename T>
|
||||
static T* no_const(const T* ptr) { return const_cast<T*>(ptr); }
|
||||
|
||||
/// @brief Shared lookup behind grid() and deviceGrid(): the @a n'th grid
|
||||
/// within @a base, or nullptr when @a base is null, @a n is out of
|
||||
/// range, or the value type does not match the grid.
|
||||
template<typename ValueT>
|
||||
const NanoGrid<ValueT>* gridAt(const void* base, uint32_t n) const
|
||||
{
|
||||
if (base == nullptr || n >= mMetaData.size() || mMetaData[n].gridType != toGridType<ValueT>()) return nullptr;
|
||||
return util::PtrAdd<NanoGrid<ValueT>>(base, mMetaData[n].offset);
|
||||
}
|
||||
|
||||
/// @brief Adopts a buffer whose metadata is already known, so a deep copy
|
||||
/// does not re-parse and a non-default-constructible buffer (e.g.
|
||||
/// over a ResourceRef) never needs default construction.
|
||||
GridHandle(BufferT&& buffer, std::vector<GridHandleMetaData> meta)
|
||||
: mMetaData(std::move(meta))
|
||||
, mBuffer(std::move(buffer)) {}
|
||||
|
||||
public:
|
||||
using BufferType = BufferT;
|
||||
|
||||
@@ -55,7 +101,15 @@ public:
|
||||
/// @brief Move constructor from a host buffer
|
||||
/// @param buffer buffer containing one or more NanoGrids that will be moved into this GridHandle
|
||||
/// @throw Will throw and error with the buffer does not contain a valid NanoGrid!
|
||||
template<typename T = BufferT, typename util::disable_if<BufferTraits<T>::hasDeviceDual, int>::type = 0>
|
||||
template<typename T = BufferT, typename util::disable_if<BufferTraits<T>::hasDeviceDual || BufferHasDeviceSingle<T>::value, int>::type = 0>
|
||||
GridHandle(T&& buffer);
|
||||
|
||||
/// @brief Move constructor from a single-space device buffer: the grid
|
||||
/// metadata is read through the device on the buffer's stream.
|
||||
/// @param buffer buffer containing one or more NanoGrids that will be moved into this GridHandle
|
||||
/// @throw Will throw an error if the buffer does not contain a valid NanoGrid
|
||||
/// @note The implementation of this template specialization is in nanovdb/cuda/GridHandle.cuh since it requires CUDA
|
||||
template<typename T = BufferT, typename util::enable_if<BufferHasDeviceSingle<T>::value, int>::type = 0, typename = void>
|
||||
GridHandle(T&& buffer);
|
||||
|
||||
/// @brief Constructs an empty GridHandle
|
||||
@@ -65,14 +119,14 @@ public:
|
||||
GridHandle(const GridHandle&) = delete;
|
||||
|
||||
/// @brief Move copy-constructor
|
||||
GridHandle(GridHandle&& other) noexcept {
|
||||
mBuffer = std::move(other.mBuffer);
|
||||
mMetaData = std::move(other.mMetaData);
|
||||
}
|
||||
GridHandle(GridHandle&& other) noexcept
|
||||
: mMetaData(std::move(other.mMetaData))
|
||||
, mBuffer(std::move(other.mBuffer)) {}
|
||||
|
||||
/// @brief clear this GridHandle to an empty handle
|
||||
void reset() {
|
||||
mBuffer.clear();
|
||||
if constexpr (BufferHasDestroy<BufferT>::value) mBuffer.destroy();
|
||||
else mBuffer.clear();
|
||||
mMetaData.clear();
|
||||
}
|
||||
|
||||
@@ -87,11 +141,25 @@ public:
|
||||
}
|
||||
|
||||
/// @brief Performs a deep copy of the GridHandle, possibly templated on a different buffer type
|
||||
/// @tparam OtherBufferT Buffer type of the deep copy
|
||||
/// @param buffer optional buffer used for allocation
|
||||
/// @tparam OtherBufferT Buffer type of the deep copy. A single-space device
|
||||
/// handle only copies to its own buffer type (device-to-device,
|
||||
/// ordered on the source's retained stream), so call it as
|
||||
/// copy<BufferT>() -- the default OtherBufferT does not compile.
|
||||
/// @param buffer optional buffer used for allocation; a compile error for
|
||||
/// single-space handles, which allocate through the source buffer's
|
||||
/// resource (use the no-argument overload)
|
||||
/// @return A new handle of the specified buffer type that contains a deep copy of the current handle
|
||||
template <typename OtherBufferT = HostBuffer>
|
||||
GridHandle<OtherBufferT> copy(const OtherBufferT& buffer = OtherBufferT()) const;
|
||||
GridHandle<OtherBufferT> copy(const OtherBufferT& buffer) const;
|
||||
|
||||
/// @brief Deep copy without a pool argument. The single-space path
|
||||
/// allocates through the source buffer's resource and never
|
||||
/// constructs a pool buffer, so it works for buffers that are not
|
||||
/// default-constructible, e.g. over a ResourceRef. The host path
|
||||
/// default-constructs the pool argument, so it requires a
|
||||
/// default-constructible OtherBufferT.
|
||||
template <typename OtherBufferT = HostBuffer>
|
||||
GridHandle<OtherBufferT> copy() const;
|
||||
|
||||
/// @brief Return a reference to the buffer
|
||||
BufferT& buffer() { return mBuffer; }
|
||||
@@ -99,13 +167,15 @@ public:
|
||||
/// @brief Return a const reference to the buffer
|
||||
const BufferT& buffer() const { return mBuffer; }
|
||||
|
||||
/// @brief Returns a non-const pointer to the data.
|
||||
//@{
|
||||
/// @brief Returns a pointer to the host data; not available for a
|
||||
/// single-space device buffer, which has no host-readable bytes.
|
||||
/// @warning Note that the return pointer can be NULL if the GridHandle was not initialized
|
||||
template<typename U = BufferT, typename util::disable_if<BufferHasDeviceSingle<U>::value, int>::type = 0>
|
||||
void* data() { return mBuffer.data(); }
|
||||
|
||||
/// @brief Returns a const pointer to the data.
|
||||
/// @warning Note that the return pointer can be NULL if the GridHandle was not initialized
|
||||
template<typename U = BufferT, typename util::disable_if<BufferHasDeviceSingle<U>::value, int>::type = 0>
|
||||
const void* data() const { return mBuffer.data(); }
|
||||
//@}
|
||||
|
||||
template<typename U = BufferT>
|
||||
typename util::enable_if<BufferTraits<U>::hasDeviceDual, const void*>::type
|
||||
@@ -120,6 +190,16 @@ public:
|
||||
typename util::enable_if<BufferTraits<U>::hasDeviceDual, void*>::type
|
||||
deviceData(int device) { return mBuffer.deviceData(device); }
|
||||
|
||||
//@{
|
||||
/// @brief For a single-space buffer the device data is the buffer itself.
|
||||
template<typename U = BufferT>
|
||||
typename util::enable_if<BufferHasDeviceSingle<U>::value, const void*>::type
|
||||
deviceData() const { return mBuffer.data(); }
|
||||
template<typename U = BufferT>
|
||||
typename util::enable_if<BufferHasDeviceSingle<U>::value, void*>::type
|
||||
deviceData() { return mBuffer.data(); }
|
||||
//@}
|
||||
|
||||
//@{
|
||||
/// @brief Returns the size in bytes of the raw memory buffer managed by this GridHandle.
|
||||
[[deprecated("Use GridHandle::bufferSize instead.")]] uint64_t size() const { return mBuffer.size(); }
|
||||
@@ -140,7 +220,7 @@ public:
|
||||
/// @param n Index of the (host) grid pointer to be returned
|
||||
/// @warning Note that the return pointer can be NULL if the GridHandle no host grid, @a n is invalid
|
||||
/// or if the template parameter does not match the specified grid!
|
||||
template<typename ValueT>
|
||||
template<typename ValueT, typename U = BufferT, typename util::disable_if<BufferHasDeviceSingle<U>::value, int>::type = 0>
|
||||
const NanoGrid<ValueT>* grid(uint32_t n = 0) const;
|
||||
|
||||
/// @brief Returns a host pointer to the @a n'th NanoVDB grid encoded in this GridHandle.
|
||||
@@ -148,7 +228,7 @@ public:
|
||||
/// @param n Index of the (host) grid pointer to be returned
|
||||
/// @warning Note that the return pointer can be NULL if the GridHandle no host grid, @a n is invalid
|
||||
/// or if the template parameter does not match the specified grid!
|
||||
template<typename ValueT>
|
||||
template<typename ValueT, typename U = BufferT, typename util::disable_if<BufferHasDeviceSingle<U>::value, int>::type = 0>
|
||||
NanoGrid<ValueT>* grid(uint32_t n = 0) {return const_cast<NanoGrid<ValueT>*>(static_cast<const GridHandle*>(this)->template grid<ValueT>(n));}
|
||||
|
||||
/// @brief Return a const pointer to the @a n'th grid encoded in this GridHandle on the device, e.g. GPU
|
||||
@@ -169,6 +249,18 @@ public:
|
||||
typename util::enable_if<BufferTraits<U>::hasDeviceDual, NanoGrid<ValueT>*>::type
|
||||
deviceGrid(uint32_t n=0){return const_cast<NanoGrid<ValueT>*>(static_cast<const GridHandle*>(this)->template deviceGrid<ValueT>(n));}
|
||||
|
||||
//@{
|
||||
/// @brief Return a pointer to the @a n'th grid of a single-space device buffer
|
||||
/// @warning Note that the return pointer can be NULL if the GridHandle is empty, @a n is invalid,
|
||||
/// or if the template parameter does not match the specified grid.
|
||||
template<typename ValueT, typename U = BufferT>
|
||||
typename util::enable_if<BufferHasDeviceSingle<U>::value, const NanoGrid<ValueT>*>::type
|
||||
deviceGrid(uint32_t n=0) const { return this->template gridAt<ValueT>(mBuffer.data(), n); }
|
||||
template<typename ValueT, typename U = BufferT>
|
||||
typename util::enable_if<BufferHasDeviceSingle<U>::value, NanoGrid<ValueT>*>::type
|
||||
deviceGrid(uint32_t n=0){return const_cast<NanoGrid<ValueT>*>(static_cast<const GridHandle*>(this)->template deviceGrid<ValueT>(n));}
|
||||
//@}
|
||||
|
||||
/// @brief Upload the grid to the device, e.g. from CPU to GPU
|
||||
/// @note This method is only available if the buffer supports devices
|
||||
template<typename U = BufferT>
|
||||
@@ -230,17 +322,21 @@ public:
|
||||
/// @brief Access to the GridData of the n'th grid in the current handle
|
||||
/// @param n zero-based ID of the grid
|
||||
/// @return Const pointer to the n'th GridData in the current handle
|
||||
template<typename U = BufferT, typename util::disable_if<BufferHasDeviceSingle<U>::value, int>::type = 0>
|
||||
const GridData* gridData(uint32_t n = 0) const;
|
||||
|
||||
/// @brief Returns a const point to the @a n'th grid meta data
|
||||
/// @param n zero-based ID of the grid
|
||||
/// @warning Note that the return pointer can be NULL if the GridHandle was not initialized
|
||||
template<typename U = BufferT, typename util::disable_if<BufferHasDeviceSingle<U>::value, int>::type = 0>
|
||||
const GridMetaData* gridMetaData(uint32_t n = 0) const;
|
||||
|
||||
/// @brief Write a specific grid in this buffer to an output stream
|
||||
/// @param os output stream that the buffer will be written to
|
||||
/// @param n zero-based index of the grid to be written to stream
|
||||
void write(std::ostream& os, uint32_t n) const {
|
||||
static_assert(!BufferHasDeviceSingle<BufferT>::value,
|
||||
"GridHandle::write requires host-accessible grids: cuda::copyTo a host-readable handle first");
|
||||
if (const GridData* data = this->gridData(n)) {
|
||||
os.write((const char*)data, data->mGridSize);
|
||||
} else {
|
||||
@@ -251,6 +347,9 @@ public:
|
||||
/// @brief Write the entire grid buffer to an output stream
|
||||
/// @param os output stream that the buffer will be written to
|
||||
void write(std::ostream& os) const {
|
||||
static_assert(!BufferHasDeviceSingle<BufferT>::value,
|
||||
"GridHandle::write requires host-accessible grids: cuda::copyTo a host-readable handle first");
|
||||
|
||||
for (uint32_t n=0; n<this->gridCount(); ++n) this->write(os, n);
|
||||
}
|
||||
|
||||
@@ -328,6 +427,7 @@ public:
|
||||
// --------------------------> Implementation of private methods in GridHandle <------------------------------------
|
||||
|
||||
template<typename BufferT>
|
||||
template<typename U, typename util::disable_if<BufferHasDeviceSingle<U>::value, int>::type>
|
||||
inline const GridData* GridHandle<BufferT>::gridData(uint32_t n) const
|
||||
{
|
||||
const void *data = this->data();
|
||||
@@ -336,6 +436,7 @@ inline const GridData* GridHandle<BufferT>::gridData(uint32_t n) const
|
||||
}// const GridData* GridHandle<BufferT>::gridData(uint32_t n) const
|
||||
|
||||
template<typename BufferT>
|
||||
template<typename U, typename util::disable_if<BufferHasDeviceSingle<U>::value, int>::type>
|
||||
inline const GridMetaData* GridHandle<BufferT>::gridMetaData(uint32_t n) const
|
||||
{
|
||||
const auto *data = this->data();
|
||||
@@ -355,11 +456,11 @@ inline __hostdev__ void cpyGridHandleMeta(const GridData *data, GridHandleMetaDa
|
||||
|
||||
// template specialization of move constructor from a host buffer
|
||||
template<typename BufferT>
|
||||
template<typename T, typename util::disable_if<BufferTraits<T>::hasDeviceDual, int>::type>
|
||||
template<typename T, typename util::disable_if<BufferTraits<T>::hasDeviceDual || BufferHasDeviceSingle<T>::value, int>::type>
|
||||
GridHandle<BufferT>::GridHandle(T&& buffer)
|
||||
: mBuffer(std::move(buffer))
|
||||
{
|
||||
static_assert(util::is_same<T,BufferT>::value, "Expected U==BufferT");
|
||||
mBuffer = std::move(buffer);
|
||||
if (auto *data = reinterpret_cast<const GridData*>(mBuffer.data())) {
|
||||
if (!data->isValid()) throw std::runtime_error("GridHandle was constructed with an invalid host buffer");
|
||||
mMetaData.resize(data->mGridCount);
|
||||
@@ -371,19 +472,38 @@ template<typename BufferT>
|
||||
template <typename OtherBufferT>
|
||||
inline GridHandle<OtherBufferT> GridHandle<BufferT>::copy(const OtherBufferT& other) const
|
||||
{
|
||||
if (mBuffer.isEmpty()) return GridHandle<OtherBufferT>();// return an empty handle
|
||||
auto buffer = OtherBufferT::create(mBuffer.size(), &other);
|
||||
static_assert(!(BufferHasDeviceSingle<BufferT>::value || BufferHasDeviceSingle<OtherBufferT>::value),
|
||||
"GridHandle::copy(pool) cannot honor a pool argument for a single-space device buffer, "
|
||||
"whose copy allocates through the source buffer's resource: use the no-argument copy()");
|
||||
if (mBuffer.size() == 0) return GridHandle<OtherBufferT>();// return an empty handle
|
||||
auto buffer = detail::createHostStorage<OtherBufferT>(mBuffer.size(), other);
|
||||
std::memcpy(buffer.data(), mBuffer.data(), mBuffer.size());// deep copy of buffer
|
||||
return GridHandle<OtherBufferT>(std::move(buffer));
|
||||
}// GridHandle<OtherBufferT> GridHandle<BufferT>::copy(const OtherBufferT& other) const
|
||||
|
||||
template<typename BufferT>
|
||||
template<typename ValueT>
|
||||
template <typename OtherBufferT>
|
||||
inline GridHandle<OtherBufferT> GridHandle<BufferT>::copy() const
|
||||
{
|
||||
if constexpr (BufferHasDeviceSingle<BufferT>::value || BufferHasDeviceSingle<OtherBufferT>::value) {
|
||||
static_assert(util::is_same<OtherBufferT, BufferT>::value && BufferHasDeviceSingle<BufferT>::value,
|
||||
"GridHandle::copy is same-space only: a single-space device handle copies to its own "
|
||||
"buffer type; use cuda::copyTo (cuda/GridHandle.cuh) to move grids across address spaces");
|
||||
// Device-to-device deep copy; for a stream-ordered resource it is
|
||||
// ordered on the source's retained stream, so synchronize that stream
|
||||
// before reading the result. Metadata is host-resident, so the copy
|
||||
// adopts it directly with no device re-parse.
|
||||
return GridHandle(mBuffer.copy(), mMetaData);
|
||||
} else {
|
||||
return this->copy(OtherBufferT());
|
||||
}
|
||||
}// GridHandle<OtherBufferT> GridHandle<BufferT>::copy() const
|
||||
|
||||
template<typename BufferT>
|
||||
template<typename ValueT, typename U, typename util::disable_if<BufferHasDeviceSingle<U>::value, int>::type>
|
||||
inline const NanoGrid<ValueT>* GridHandle<BufferT>::grid(uint32_t n) const
|
||||
{
|
||||
const void *data = mBuffer.data();
|
||||
if (data == nullptr || n >= mMetaData.size() || mMetaData[n].gridType != toGridType<ValueT>()) return nullptr;
|
||||
return util::PtrAdd<NanoGrid<ValueT>>(data, mMetaData[n].offset);
|
||||
return this->template gridAt<ValueT>(mBuffer.data(), n);
|
||||
}// const NanoGrid<ValueT>* GridHandle<BufferT>::grid(uint32_t n) const
|
||||
|
||||
template<typename BufferT>
|
||||
@@ -391,14 +511,14 @@ template<typename ValueT, typename U>
|
||||
inline typename util::enable_if<BufferTraits<U>::hasDeviceDual, const NanoGrid<ValueT>*>::type
|
||||
GridHandle<BufferT>::deviceGrid(uint32_t n) const
|
||||
{
|
||||
const void *data = mBuffer.deviceData();
|
||||
if (data == nullptr || n >= mMetaData.size() || mMetaData[n].gridType != toGridType<ValueT>()) return nullptr;
|
||||
return util::PtrAdd<NanoGrid<ValueT>>(data, mMetaData[n].offset);
|
||||
return this->template gridAt<ValueT>(mBuffer.deviceData(), n);
|
||||
}// GridHandle<BufferT>::deviceGrid(uint32_t n) cons
|
||||
|
||||
template<typename BufferT>
|
||||
void GridHandle<BufferT>::read(std::istream& is, const BufferT& pool)
|
||||
{
|
||||
static_assert(!BufferHasDeviceSingle<BufferT>::value,
|
||||
"GridHandle::read requires a host-accessible buffer: read into a host-readable handle, then cuda::copyTo");
|
||||
const std::streampos start = is.tellg();// remember where the raw buffer begins
|
||||
GridData data;
|
||||
is.read((char*)&data, sizeof(GridData));
|
||||
@@ -409,7 +529,7 @@ void GridHandle<BufferT>::read(std::istream& is, const BufferT& pool)
|
||||
is.read((char*)&data, sizeof(GridData));
|
||||
sum += data.mGridSize;
|
||||
}
|
||||
auto buffer = BufferT::create(size + sum, &pool);
|
||||
auto buffer = detail::createHostStorage(size + sum, pool);
|
||||
is.seekg(start);// rewind to the start of the raw buffer
|
||||
is.read((char*)(buffer.data()), buffer.size());
|
||||
*this = GridHandle(std::move(buffer));
|
||||
@@ -422,6 +542,8 @@ void GridHandle<BufferT>::read(std::istream& is, const BufferT& pool)
|
||||
template<typename BufferT>
|
||||
void GridHandle<BufferT>::read(std::istream& is, uint32_t n, const BufferT& pool)
|
||||
{
|
||||
static_assert(!BufferHasDeviceSingle<BufferT>::value,
|
||||
"GridHandle::read requires a host-accessible buffer: read into a host-readable handle, then cuda::copyTo");
|
||||
GridData data;
|
||||
is.read((char*)&data, sizeof(GridData));
|
||||
if (data.isValid()) {
|
||||
@@ -430,7 +552,7 @@ void GridHandle<BufferT>::read(std::istream& is, uint32_t n, const BufferT& pool
|
||||
is.seekg(data.mGridSize - sizeof(GridData), std::ios::cur);// skip grid
|
||||
is.read((char*)&data, sizeof(GridData));
|
||||
}
|
||||
auto buffer = BufferT::create(data.mGridSize, &pool);
|
||||
auto buffer = detail::createHostStorage(data.mGridSize, pool);
|
||||
is.seekg(-sizeof(GridData), std::ios::cur);// rewind
|
||||
is.read((char*)(buffer.data()), data.mGridSize);
|
||||
tools::updateGridCount((GridData*)buffer.data(), 0u, 1u);
|
||||
@@ -444,6 +566,8 @@ void GridHandle<BufferT>::read(std::istream& is, uint32_t n, const BufferT& pool
|
||||
template<typename BufferT>
|
||||
void GridHandle<BufferT>::read(std::istream& is, const std::string &gridName, const BufferT& pool)
|
||||
{
|
||||
static_assert(!BufferHasDeviceSingle<BufferT>::value,
|
||||
"GridHandle::read requires a host-accessible buffer: read into a host-readable handle, then cuda::copyTo");
|
||||
static const std::streamsize byteSize = sizeof(GridData);
|
||||
GridData data;
|
||||
is.read((char*)&data, byteSize);
|
||||
@@ -456,7 +580,7 @@ void GridHandle<BufferT>::read(std::istream& is, const std::string &gridName, co
|
||||
is.seekg(-byteSize, std::ios::cur);// rewind
|
||||
}
|
||||
if (n>data.mGridCount) throw std::runtime_error("No raw grid named \""+gridName+"\"");
|
||||
auto buffer = BufferT::create(data.mGridSize, &pool);
|
||||
auto buffer = detail::createHostStorage(data.mGridSize, pool);
|
||||
is.read((char*)(buffer.data()), data.mGridSize);
|
||||
tools::updateGridCount((GridData*)buffer.data(), 0u, 1u);
|
||||
*this = GridHandle(std::move(buffer));
|
||||
@@ -476,6 +600,10 @@ template<typename BufferT, template <class, class...> class VectorT = std::vecto
|
||||
inline VectorT<GridHandle<BufferT>>
|
||||
splitGrids(const GridHandle<BufferT> &handle, const BufferT* other = nullptr)
|
||||
{
|
||||
static_assert(!BufferHasDeviceSingle<BufferT>::value,
|
||||
"splitGrids requires a buffer type providing create(): cuda::copyTo a HostBuffer handle first");
|
||||
static_assert(!BufferHasHostSingle<BufferT>::value,
|
||||
"splitGrids requires a buffer type providing create(): copy the handle to a HostBuffer first");
|
||||
using HandleT = GridHandle<BufferT>;
|
||||
const void *ptr = handle.data();
|
||||
if (ptr == nullptr) return VectorT<HandleT>();
|
||||
@@ -493,6 +621,51 @@ splitGrids(const GridHandle<BufferT> &handle, const BufferT* other = nullptr)
|
||||
return handles;
|
||||
}// splitGrids
|
||||
|
||||
/// @brief Combines (or merges) multiple GridHandles into a single GridHandle containing all grids
|
||||
/// @tparam BufferT Type of the input and output grid buffers
|
||||
/// @param handles array of non-owning pointers to the GridHandles to be combined
|
||||
/// @param count number of pointers in @a handles
|
||||
/// @param pool optional pool used for allocation of output GridHandle
|
||||
/// @return single GridHandle containing all input grids
|
||||
/// @note This overload borrows the input handles, so it can be used when the
|
||||
/// handles are owned elsewhere (e.g. by a language binding) and can be
|
||||
/// neither moved from nor cheaply copied.
|
||||
template<typename BufferT>
|
||||
inline GridHandle<BufferT>
|
||||
mergeGrids(const GridHandle<BufferT>* const* handles, size_t count, const BufferT* pool = nullptr)
|
||||
{
|
||||
static_assert(!BufferHasDeviceSingle<BufferT>::value,
|
||||
"mergeGrids requires a buffer type providing create(): cuda::copyTo HostBuffer handles first");
|
||||
static_assert(!BufferHasHostSingle<BufferT>::value,
|
||||
"mergeGrids requires a buffer type providing create(): copy the handles to HostBuffer first");
|
||||
uint64_t size = 0u;
|
||||
uint32_t counter = 0u, gridCount = 0u;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
gridCount += handles[i]->gridCount();
|
||||
for (uint32_t n=0; n<handles[i]->gridCount(); ++n) size += handles[i]->gridSize(n);
|
||||
}
|
||||
// Return an empty handle when there is nothing to merge: BufferT::create(0)
|
||||
// can produce a non-null zero-byte region, from which the GridHandle
|
||||
// constructor would attempt to read a GridData header.
|
||||
if (gridCount == 0u) return GridHandle<BufferT>();
|
||||
auto buffer = BufferT::create(size, pool);
|
||||
void *dst = buffer.data();
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
const GridHandle<BufferT> &h = *handles[i];
|
||||
for (uint32_t n=0; n<h.gridCount(); ++n) {
|
||||
// gridData(n) applies the per-grid offset, so padded (non-contiguous)
|
||||
// source buffers are copied correctly.
|
||||
const GridData *src = h.gridData(n);
|
||||
NANOVDB_ASSERT(src && src->isValid());
|
||||
const uint64_t gridSize = h.gridSize(n);
|
||||
std::memcpy(dst, src, gridSize);
|
||||
tools::updateGridCount(reinterpret_cast<GridData*>(dst), counter++, gridCount);
|
||||
dst = util::PtrAdd(dst, gridSize);
|
||||
}
|
||||
}
|
||||
return GridHandle<BufferT>(std::move(buffer));
|
||||
}// mergeGrids
|
||||
|
||||
/// @brief Combines (or merges) multiple GridHandles into a single GridHandle containing all grids
|
||||
/// @tparam BufferT Type of the input and output grid buffers
|
||||
/// @param handles Vector of GridHandles to be combined
|
||||
@@ -502,26 +675,10 @@ template<typename BufferT, template <class, class...> class VectorT>
|
||||
inline GridHandle<BufferT>
|
||||
mergeGrids(const VectorT<GridHandle<BufferT>> &handles, const BufferT* pool = nullptr)
|
||||
{
|
||||
uint64_t size = 0u;
|
||||
uint32_t counter = 0u, gridCount = 0u;
|
||||
for (auto &h : handles) {
|
||||
gridCount += h.gridCount();
|
||||
for (uint32_t n=0; n<h.gridCount(); ++n) size += h.gridSize(n);
|
||||
}
|
||||
auto buffer = BufferT::create(size, pool);
|
||||
void *dst = buffer.data();
|
||||
for (auto &h : handles) {
|
||||
const void *src = h.data();
|
||||
for (uint32_t n=0; n<h.gridCount(); ++n) {
|
||||
std::memcpy(dst, src, h.gridSize(n));
|
||||
GridData *data = reinterpret_cast<GridData*>(dst);
|
||||
NANOVDB_ASSERT(data->isValid());
|
||||
tools::updateGridCount(data, counter++, gridCount);
|
||||
dst = util::PtrAdd(dst, data->mGridSize);
|
||||
src = util::PtrAdd(src, data->mGridSize);
|
||||
}
|
||||
}
|
||||
return GridHandle<BufferT>(std::move(buffer));
|
||||
std::vector<const GridHandle<BufferT>*> ptrs;
|
||||
ptrs.reserve(handles.size());
|
||||
for (auto &h : handles) ptrs.push_back(&h);
|
||||
return mergeGrids<BufferT>(ptrs.data(), ptrs.size(), pool);
|
||||
}// mergeGrids
|
||||
|
||||
} // namespace nanovdb
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
#include <cassert>// for assert
|
||||
#include <sstream>// for std::stringstream
|
||||
#include <cstring>// for memcpy
|
||||
#include <type_traits>// for std::void_t
|
||||
|
||||
#define checkPtr(ptr, msg) \
|
||||
{ \
|
||||
@@ -98,9 +99,57 @@ namespace nanovdb {
|
||||
template<typename BufferT>
|
||||
struct BufferTraits
|
||||
{
|
||||
static constexpr bool hasDeviceDual = false;
|
||||
static constexpr bool hasDeviceDual = false;
|
||||
static constexpr bool hasDeviceSingle = false;
|
||||
};
|
||||
|
||||
/// @brief Detects whether @c BufferTraits<BufferT> defines @c hasDeviceSingle,
|
||||
/// i.e. whether the buffer manages a single device-resident allocation.
|
||||
/// @details Defaults to false when the trait member is absent, so pre-existing
|
||||
/// BufferTraits specializations (in or out of tree) that only define
|
||||
/// hasDeviceDual keep compiling unchanged.
|
||||
template<typename BufferT, typename = void>
|
||||
struct BufferHasDeviceSingle { static constexpr bool value = false; };
|
||||
template<typename BufferT>
|
||||
struct BufferHasDeviceSingle<BufferT, std::void_t<decltype(BufferTraits<BufferT>::hasDeviceSingle)>>
|
||||
{ static constexpr bool value = BufferTraits<BufferT>::hasDeviceSingle; };
|
||||
|
||||
/// @brief Companion detection for BufferTraits<...>::hasHostSingle: a
|
||||
/// single-space buffer whose storage is host-accessible (e.g. a
|
||||
/// pinned-resource cuda::Buffer).
|
||||
template<typename BufferT, typename = void>
|
||||
struct BufferHasHostSingle { static constexpr bool value = false; };
|
||||
template<typename BufferT>
|
||||
struct BufferHasHostSingle<BufferT, std::void_t<decltype(BufferTraits<BufferT>::hasHostSingle)>>
|
||||
{ static constexpr bool value = BufferTraits<BufferT>::hasHostSingle; };
|
||||
|
||||
/// @brief Detects whether a buffer exposes a retained stream (a stream()
|
||||
/// member), i.e. whether its resource is stream-ordered. Used to pick
|
||||
/// the buffer's stream-taking constructor without naming CUDA types.
|
||||
template<typename BufferT, typename = void>
|
||||
struct BufferHasStream { static constexpr bool value = false; };
|
||||
template<typename BufferT>
|
||||
struct BufferHasStream<BufferT, std::void_t<decltype(std::declval<const BufferT&>().stream())>>
|
||||
{ static constexpr bool value = true; };
|
||||
|
||||
/// @brief Detects whether a buffer's elements are byte-sized. Buffers that
|
||||
/// expose no ElementType (e.g. HostBuffer) address raw bytes by
|
||||
/// definition, so the primary defaults to true.
|
||||
template<typename BufferT, typename = void>
|
||||
struct BufferHasByteElements { static constexpr bool value = true; };
|
||||
template<typename BufferT>
|
||||
struct BufferHasByteElements<BufferT, std::void_t<typename BufferT::ElementType>>
|
||||
{ static constexpr bool value = sizeof(typename BufferT::ElementType) == 1; };
|
||||
|
||||
/// @brief Detects whether a buffer provides destroy(), the cuda::Buffer
|
||||
/// spelling for releasing its storage. Handle reset() dispatches to it
|
||||
/// when present and falls back to the legacy clear() otherwise.
|
||||
template<typename BufferT, typename = void>
|
||||
struct BufferHasDestroy { static constexpr bool value = false; };
|
||||
template<typename BufferT>
|
||||
struct BufferHasDestroy<BufferT, std::void_t<decltype(std::declval<BufferT&>().destroy())>>
|
||||
{ static constexpr bool value = true; };
|
||||
|
||||
// ----------------------------> HostBuffer <--------------------------------------
|
||||
|
||||
/// @brief This is a buffer that contains a shared or private pool
|
||||
|
||||
@@ -1808,6 +1808,42 @@ struct ProbeValue;
|
||||
template<typename BuildT>
|
||||
struct GetNodeInfo;
|
||||
|
||||
/// @brief Result of a fused ReadAccessor::getDimAndActive traversal: the
|
||||
/// getDim(ijk, ray) step size and the policy-dependent active state of @a ijk.
|
||||
///
|
||||
/// The low 31 bits store @c dim and bit 31 stores @c active, saving one live
|
||||
/// return register versus {uint32_t, bool}. The default FloatTree's upper-node
|
||||
/// maximum is 4096. The constructor supports `return {dim, active}`; dim() and
|
||||
/// active() decode the packed value.
|
||||
struct DimAndActive
|
||||
{
|
||||
uint32_t packed{0u};
|
||||
|
||||
__hostdev__ DimAndActive() : packed(0u) {}
|
||||
__hostdev__ DimAndActive(uint32_t d, bool a)
|
||||
: packed((d & 0x7FFFFFFFu) | (uint32_t(a) << 31))
|
||||
{
|
||||
NANOVDB_ASSERT(d < (uint32_t(1) << 31)); // dim must fit in 31 bits (bit 31 = active)
|
||||
}
|
||||
|
||||
__hostdev__ uint32_t dim() const { return packed & 0x7FFFFFFFu; }
|
||||
__hostdev__ bool active() const { return (packed >> 31) != 0u; }
|
||||
};
|
||||
|
||||
/// @brief Policy that exactly matches separate getDim and isActive calls. A
|
||||
/// skip flag still controls @c dim but cannot stop the active-state descent:
|
||||
/// GridStats flags value range versus render delta, not activeness. This costs
|
||||
/// a full descent through skipped InternalNodes; use it when @c active is read
|
||||
/// unconditionally, such as by HDDA iterators observing upper-level activeness.
|
||||
struct ActiveExact {};
|
||||
|
||||
/// @brief Policy that preserves getDim's skip-flag fast path. If an
|
||||
/// InternalNode short-circuits, @c active is unspecified and must be gated on
|
||||
/// @c dim. Otherwise the leaf-level active read is fused into the descent. Use
|
||||
/// for logic such as `if (hdda.dim() > 1 || !active) continue;`, where the
|
||||
/// short-circuit ensures @c active is read only after a leaf-level descent.
|
||||
struct ActiveOnLeafOnly {};
|
||||
|
||||
// ----------------------------> CheckMode <----------------------------------
|
||||
|
||||
/// @brief List of different modes for computing for a checksum
|
||||
@@ -3136,6 +3172,23 @@ private:
|
||||
return ChildNodeType::dim(); // background
|
||||
}
|
||||
|
||||
/// @brief Returns dim and active state in one traversal, sharing cache and
|
||||
/// terminal tile/mask reads. Root has no skip flag, so @c Policy affects
|
||||
/// only the child descent; see ActiveExact and ActiveOnLeafOnly.
|
||||
template<typename Policy = ActiveExact, typename RayT, typename AccT>
|
||||
__hostdev__ DimAndActive getDimAndActiveAndCache(const CoordType& ijk, const RayT& ray, const AccT& acc) const
|
||||
{
|
||||
if (const Tile* tile = this->probeTile(ijk)) {
|
||||
if (tile->isChild()) {
|
||||
const auto* child = this->getChild(tile);
|
||||
acc.insert(ijk, child);
|
||||
return child->template getDimAndActiveAndCache<Policy>(ijk, ray, acc);
|
||||
}
|
||||
return {uint32_t(1) << ChildT::TOTAL, tile->state > 0u}; // tile value
|
||||
}
|
||||
return {ChildNodeType::dim(), false}; // background is never active
|
||||
}
|
||||
|
||||
template<typename OpT, typename AccT, typename... ArgsT>
|
||||
__hostdev__ typename OpT::Type getAndCache(const CoordType& ijk, const AccT& acc, ArgsT&&... args) const
|
||||
{
|
||||
@@ -3632,6 +3685,39 @@ private:
|
||||
return ChildNodeType::dim(); // tile value
|
||||
}
|
||||
|
||||
/// @brief Returns dim and active state in one traversal. On a skip flag,
|
||||
/// ActiveExact resolves the true active state but returns this node's dim;
|
||||
/// ActiveOnLeafOnly returns {dim, false} immediately. Without a skip flag
|
||||
/// both policies are identical. See GridStats::setFlag.
|
||||
template<typename Policy = ActiveExact, typename RayT, typename AccT>
|
||||
__hostdev__ DimAndActive getDimAndActiveAndCache(const CoordType& ijk, const RayT& ray, const AccT& acc) const
|
||||
{
|
||||
//if (!ray.intersects( this->bbox() )) return {1<<TOTAL, ...};
|
||||
// Separate paths keep `skip` out of ActiveOnLeafOnly's recursion,
|
||||
// saving ~2 registers/thread and avoiding an occupancy cliff.
|
||||
if constexpr (util::is_same<Policy, ActiveOnLeafOnly>::value) {
|
||||
if (DataType::mFlags & uint32_t(1u))
|
||||
return {this->dim(), false}; // active unspecified per policy
|
||||
const uint32_t n = CoordToOffset(ijk);
|
||||
if (DataType::mChildMask.isOn(n)) {
|
||||
const ChildT* child = this->getChild(n);
|
||||
acc.insert(ijk, child);
|
||||
return child->template getDimAndActiveAndCache<Policy>(ijk, ray, acc);
|
||||
}
|
||||
return {ChildNodeType::dim(), DataType::mValueMask.isOn(n)};
|
||||
} else {
|
||||
const bool skip = (DataType::mFlags & uint32_t(1u));
|
||||
const uint32_t n = CoordToOffset(ijk);
|
||||
if (DataType::mChildMask.isOn(n)) {
|
||||
const ChildT* child = this->getChild(n);
|
||||
acc.insert(ijk, child);
|
||||
const auto r = child->template getDimAndActiveAndCache<Policy>(ijk, ray, acc);
|
||||
return {skip ? this->dim() : r.dim(), r.active()};
|
||||
}
|
||||
return {skip ? this->dim() : ChildNodeType::dim(), DataType::mValueMask.isOn(n)};
|
||||
}
|
||||
}
|
||||
|
||||
template<typename OpT, typename AccT, typename... ArgsT>
|
||||
__hostdev__ typename OpT::Type getAndCache(const CoordType& ijk, const AccT& acc, ArgsT&&... args) const
|
||||
{
|
||||
@@ -4572,6 +4658,21 @@ private:
|
||||
return ChildNodeType::dim();
|
||||
}
|
||||
|
||||
/// @brief Returns dim and active state in one traversal. @c Policy is
|
||||
/// accepted for recursion but does not change leaf behavior: the skip flag
|
||||
/// controls dim while active always comes from mValueMask. Branching here
|
||||
/// raised parallelForKernel from 64 to 66 registers/thread and reduced
|
||||
/// occupancy from four to three blocks/SM, negating the descent savings;
|
||||
/// leaf skip flags are rare because upper-level flags usually short-circuit.
|
||||
template<typename Policy = ActiveExact, typename RayT, typename AccT>
|
||||
__hostdev__ DimAndActive getDimAndActiveAndCache(const CoordT& ijk, const RayT& /*ray*/, const AccT&) const
|
||||
{
|
||||
//if (!ray.intersects( this->bbox() )) return {1 << LOG2DIM, ...};
|
||||
const uint32_t n = CoordToOffset(ijk);
|
||||
const uint32_t dimOut = (DataType::mFlags & uint8_t(1u)) ? this->dim() : ChildNodeType::dim();
|
||||
return {dimOut, DataType::mValueMask.isOn(n)};
|
||||
}
|
||||
|
||||
template<typename OpT, typename AccT, typename... ArgsT>
|
||||
__hostdev__ auto
|
||||
//__hostdev__ decltype(OpT::get(util::declval<const LeafNode&>(), util::declval<uint32_t>(), util::declval<ArgsT>()...))
|
||||
@@ -4878,6 +4979,15 @@ public:
|
||||
{
|
||||
return mRoot->getDimAndCache(ijk, ray, *this);
|
||||
}
|
||||
/// @brief Fused getDim + isActive: one tree walk returns both the HDDA
|
||||
/// step size and the active state of @a ijk, shaving the second descent
|
||||
/// / cache-check / leaf ValueMask lookup. See @c ActiveExact /
|
||||
/// @c ActiveOnLeafOnly for the @c Policy contract.
|
||||
template<typename Policy = ActiveExact, typename RayT>
|
||||
__hostdev__ DimAndActive getDimAndActive(const CoordType& ijk, const RayT& ray) const
|
||||
{
|
||||
return mRoot->template getDimAndActiveAndCache<Policy>(ijk, ray, *this);
|
||||
}
|
||||
template<typename OpT, typename... ArgsT>
|
||||
__hostdev__ auto get(const CoordType& ijk, ArgsT&&... args) const
|
||||
{
|
||||
@@ -4993,6 +5103,17 @@ public:
|
||||
return mRoot->getDimAndCache(ijk, ray, *this);
|
||||
}
|
||||
|
||||
/// @brief Fused getDim + isActive: one tree walk returns both the HDDA
|
||||
/// step size and the active state of @a ijk, shaving the second descent
|
||||
/// / cache-check / leaf ValueMask lookup. See @c ActiveExact /
|
||||
/// @c ActiveOnLeafOnly for the @c Policy contract.
|
||||
template<typename Policy = ActiveExact, typename RayT>
|
||||
__hostdev__ DimAndActive getDimAndActive(const CoordType& ijk, const RayT& ray) const
|
||||
{
|
||||
if (this->isCached(ijk)) return mNode->template getDimAndActiveAndCache<Policy>(ijk, ray, *this);
|
||||
return mRoot->template getDimAndActiveAndCache<Policy>(ijk, ray, *this);
|
||||
}
|
||||
|
||||
template<typename OpT, typename... ArgsT>
|
||||
__hostdev__ typename OpT::Type get(const CoordType& ijk, ArgsT&&... args) const
|
||||
{
|
||||
@@ -5175,6 +5296,26 @@ public:
|
||||
return mRoot->getDimAndCache(ijk, ray, *this);
|
||||
}
|
||||
|
||||
/// @brief Fused getDim + isActive: one tree walk returns both the HDDA
|
||||
/// step size and the active state of @a ijk, shaving the second descent
|
||||
/// / cache-check / leaf ValueMask lookup. See @c ActiveExact /
|
||||
/// @c ActiveOnLeafOnly for the @c Policy contract.
|
||||
template<typename Policy = ActiveExact, typename RayT>
|
||||
__hostdev__ DimAndActive getDimAndActive(const CoordType& ijk, const RayT& ray) const
|
||||
{
|
||||
#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY
|
||||
const CoordValueType dirty = this->computeDirty(ijk);
|
||||
#else
|
||||
auto&& dirty = ijk;
|
||||
#endif
|
||||
if (this->isCached1(dirty)) {
|
||||
return mNode1->template getDimAndActiveAndCache<Policy>(ijk, ray, *this);
|
||||
} else if (this->isCached2(dirty)) {
|
||||
return mNode2->template getDimAndActiveAndCache<Policy>(ijk, ray, *this);
|
||||
}
|
||||
return mRoot->template getDimAndActiveAndCache<Policy>(ijk, ray, *this);
|
||||
}
|
||||
|
||||
template<typename OpT, typename... ArgsT>
|
||||
__hostdev__ typename OpT::Type get(const CoordType& ijk, ArgsT&&... args) const
|
||||
{
|
||||
@@ -5446,6 +5587,28 @@ public:
|
||||
return mRoot->getDimAndCache(ijk, ray, *this);
|
||||
}
|
||||
|
||||
/// @brief Fused getDim + isActive: one tree walk returns both the HDDA
|
||||
/// step size and the active state of @a ijk, shaving the second descent
|
||||
/// / cache-check / leaf ValueMask lookup. See @c ActiveExact /
|
||||
/// @c ActiveOnLeafOnly for the @c Policy contract.
|
||||
template<typename Policy = ActiveExact, typename RayT>
|
||||
__hostdev__ DimAndActive getDimAndActive(const CoordType& ijk, const RayT& ray) const
|
||||
{
|
||||
#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY
|
||||
const CoordValueType dirty = this->computeDirty(ijk);
|
||||
#else
|
||||
auto&& dirty = ijk;
|
||||
#endif
|
||||
if (this->isCached<LeafT>(dirty)) {
|
||||
return ((LeafT*)mNode[0])->template getDimAndActiveAndCache<Policy>(ijk, ray, *this);
|
||||
} else if (this->isCached<NodeT1>(dirty)) {
|
||||
return ((NodeT1*)mNode[1])->template getDimAndActiveAndCache<Policy>(ijk, ray, *this);
|
||||
} else if (this->isCached<NodeT2>(dirty)) {
|
||||
return ((NodeT2*)mNode[2])->template getDimAndActiveAndCache<Policy>(ijk, ray, *this);
|
||||
}
|
||||
return mRoot->template getDimAndActiveAndCache<Policy>(ijk, ray, *this);
|
||||
}
|
||||
|
||||
private:
|
||||
/// @brief Allow nodes to insert themselves into the cache.
|
||||
template<typename>
|
||||
|
||||
@@ -53,10 +53,14 @@ struct NodeManagerData
|
||||
template<typename BufferT>
|
||||
class NodeManagerHandle
|
||||
{
|
||||
static_assert(!(BufferHasDeviceSingle<BufferT>::value || BufferHasHostSingle<BufferT>::value)
|
||||
|| BufferHasByteElements<BufferT>::value,
|
||||
"NodeManagerHandle requires byte-addressed single-space storage, e.g. cuda::Buffer<std::byte, R>");
|
||||
|
||||
GridType mGridType{GridType::Unknown};
|
||||
BufferT mBuffer;
|
||||
|
||||
template<typename BuildT>
|
||||
template<typename BuildT, typename U = BufferT, typename util::disable_if<BufferHasDeviceSingle<U>::value, int>::type = 0>
|
||||
const NodeManager<BuildT>* getMgr() const {
|
||||
return mGridType == toGridType<BuildT>() ? (const NodeManager<BuildT>*)mBuffer.data() : nullptr;
|
||||
}
|
||||
@@ -72,7 +76,9 @@ class NodeManagerHandle
|
||||
|
||||
public:
|
||||
/// @brief Move constructor from a buffer
|
||||
NodeManagerHandle(GridType gridType, BufferT&& buffer) : mGridType(gridType) { mBuffer = std::move(buffer); }
|
||||
NodeManagerHandle(GridType gridType, BufferT&& buffer)
|
||||
: mGridType(gridType)
|
||||
, mBuffer(std::move(buffer)) {}// member-initialized, so a non-default-constructible buffer works
|
||||
/// @brief Empty ctor
|
||||
NodeManagerHandle() = default;
|
||||
/// @brief Disallow copy-construction
|
||||
@@ -87,15 +93,19 @@ public:
|
||||
return *this;
|
||||
}
|
||||
/// @brief Move copy-constructor
|
||||
NodeManagerHandle(NodeManagerHandle&& other) noexcept {
|
||||
mGridType = other.mGridType;
|
||||
mBuffer = std::move(other.mBuffer);
|
||||
NodeManagerHandle(NodeManagerHandle&& other) noexcept
|
||||
: mGridType(other.mGridType)
|
||||
, mBuffer(std::move(other.mBuffer))
|
||||
{
|
||||
other.mGridType = GridType::Unknown;
|
||||
}
|
||||
/// @brief Default destructor
|
||||
~NodeManagerHandle() { this->reset(); }
|
||||
/// @brief clear the buffer
|
||||
void reset() { mBuffer.clear(); }
|
||||
/// @brief Release the buffer's storage
|
||||
void reset() {
|
||||
if constexpr (BufferHasDestroy<BufferT>::value) mBuffer.destroy();
|
||||
else mBuffer.clear();
|
||||
}
|
||||
|
||||
/// @brief Return a reference to the buffer
|
||||
BufferT& buffer() { return mBuffer; }
|
||||
@@ -103,15 +113,15 @@ public:
|
||||
/// @brief Return a const reference to the buffer
|
||||
const BufferT& buffer() const { return mBuffer; }
|
||||
|
||||
/// @brief Returns a non-const pointer to the data.
|
||||
///
|
||||
//@{
|
||||
/// @brief Returns a pointer to the host data; not available for a
|
||||
/// single-space device buffer, which has no host-readable bytes.
|
||||
/// @warning Note that the return pointer can be NULL if the NodeManagerHandle was not initialized
|
||||
template<typename U = BufferT, typename util::disable_if<BufferHasDeviceSingle<U>::value, int>::type = 0>
|
||||
void* data() { return mBuffer.data(); }
|
||||
|
||||
/// @brief Returns a const pointer to the data.
|
||||
///
|
||||
/// @warning Note that the return pointer can be NULL if the NodeManagerHandle was not initialized
|
||||
template<typename U = BufferT, typename util::disable_if<BufferHasDeviceSingle<U>::value, int>::type = 0>
|
||||
const void* data() const { return mBuffer.data(); }
|
||||
//@}
|
||||
|
||||
/// @brief Returns the size in bytes of the raw memory buffer managed by this NodeManagerHandle's allocator.
|
||||
uint64_t size() const { return mBuffer.size(); }
|
||||
@@ -142,6 +152,19 @@ public:
|
||||
typename util::enable_if<BufferTraits<U>::hasDeviceDual, NodeManager<BuildT>*>::type
|
||||
deviceMgr() { return no_const(this->template getDeviceMgr<BuildT>()); }
|
||||
|
||||
//@{
|
||||
/// @brief Return a pointer to the NodeManager of a single-space device buffer
|
||||
/// @warning Note that the return pointer can be NULL if the template parameter does not match the specified grid!
|
||||
template<typename BuildT, typename U = BufferT>
|
||||
typename util::enable_if<BufferHasDeviceSingle<U>::value, const NodeManager<BuildT>*>::type
|
||||
deviceMgr() const {
|
||||
return mGridType == toGridType<BuildT>() ? (const NodeManager<BuildT>*)mBuffer.data() : nullptr;
|
||||
}
|
||||
template<typename BuildT, typename U = BufferT>
|
||||
typename util::enable_if<BufferHasDeviceSingle<U>::value, NodeManager<BuildT>*>::type
|
||||
deviceMgr() { return no_const(static_cast<const NodeManagerHandle*>(this)->template deviceMgr<BuildT>()); }
|
||||
//@}
|
||||
|
||||
/// @brief Upload the NodeManager to the device, e.g. from CPU to GPU
|
||||
///
|
||||
/// @note This method is only available if the buffer supports devices
|
||||
|
||||
@@ -7,6 +7,8 @@ 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)
|
||||
* [TEACHME — learn NanoVDB by prompting an LLM coding agent](../../doc/nanovdb/TEACHME/README.md)
|
||||
* [Python examples](python/examples/)
|
||||
|
||||
### Copyright Contributors to the OpenVDB Project
|
||||
### SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
+108
-11
@@ -50,7 +50,7 @@ struct StreamHolder<true> { cudaStream_t mStream = 0; };
|
||||
/// 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)
|
||||
/// the most recent allocation (or the one supplied via set_stream)
|
||||
/// 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>
|
||||
@@ -62,6 +62,18 @@ class Buffer : private detail::StreamHolder<is_async_resource<R>::value>
|
||||
|
||||
static constexpr bool IsAsync = is_async_resource<R>::value;
|
||||
|
||||
public:
|
||||
/// @brief Element and resource types, for generic code that rebinds one
|
||||
/// or constructs sibling buffers over the same resource.
|
||||
using ElementType = T;
|
||||
using ResourceType = R;
|
||||
|
||||
/// @brief Alias for a sibling buffer over the same resource with a
|
||||
/// different element type.
|
||||
template<typename U>
|
||||
using rebind = Buffer<U, R>;
|
||||
|
||||
private:
|
||||
R mResource;
|
||||
T* mData = nullptr;
|
||||
size_t mSize = 0; // element count
|
||||
@@ -133,7 +145,7 @@ public:
|
||||
Buffer& operator=(Buffer&& other) noexcept
|
||||
{
|
||||
if (this != &other) {
|
||||
this->clear();
|
||||
this->destroy();
|
||||
static_cast<detail::StreamHolder<IsAsync>&>(*this) = other;
|
||||
mResource = std::move(other.mResource);
|
||||
mData = other.mData;
|
||||
@@ -156,6 +168,11 @@ public:
|
||||
return out;
|
||||
}
|
||||
|
||||
/// @brief Returns a deep copy of this buffer ordered on the retained
|
||||
/// stream, i.e. copy(this->stream()).
|
||||
template<typename S = R, std::enable_if_t<is_async_resource<S>::value, int> = 0>
|
||||
Buffer copy() const { return this->copy(this->stream()); }
|
||||
|
||||
/// @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>
|
||||
@@ -168,7 +185,7 @@ public:
|
||||
|
||||
/// @brief D-tor. A stream-ordered resource frees on the retained stream;
|
||||
/// a synchronous resource frees immediately.
|
||||
~Buffer() { this->clear(); }
|
||||
~Buffer() { this->destroy(); }
|
||||
|
||||
/// @brief Returns the retained stream, i.e. the stream the buffer's memory
|
||||
/// will be freed on.
|
||||
@@ -179,9 +196,11 @@ public:
|
||||
/// 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.
|
||||
/// in-flight work that uses the buffer's memory. This deliberately
|
||||
/// does not synchronize, matching cuda::buffer's set_stream, which
|
||||
/// avoids implicit synchronization in fundamental primitives.
|
||||
template<typename S = R, std::enable_if_t<is_async_resource<S>::value, int> = 0>
|
||||
void setStream(cudaStream_t stream) { this->mStream = stream; }
|
||||
void set_stream(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
|
||||
@@ -219,7 +238,7 @@ public:
|
||||
mSize = count;
|
||||
}
|
||||
else {
|
||||
this->mStream = stream; // no reallocation: setStream semantics
|
||||
this->mStream = stream; // no reallocation: set_stream semantics
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,6 +272,13 @@ public:
|
||||
T* data() { return mData; }
|
||||
const T* data() const { return mData; }
|
||||
|
||||
/// @brief Returns a copy of the resource; for a ResourceRef this refers
|
||||
/// to the same underlying instance.
|
||||
/// @note Requires R to be copy-constructible (the cuda::mr convention:
|
||||
/// resources are cheap handles). A resource that owns its pool by
|
||||
/// value hands the caller an independent copy of that pool.
|
||||
R resource() const { return mResource; }
|
||||
|
||||
/// @brief Returns the number of elements.
|
||||
size_t size() const { return mSize; }
|
||||
|
||||
@@ -263,13 +289,45 @@ public:
|
||||
bool empty() const { return mSize == 0; }
|
||||
|
||||
/// @brief Frees the buffer memory (if any) and resets to the empty state.
|
||||
void clear()
|
||||
/// A stream-ordered resource frees on the retained stream.
|
||||
/// @note Spelled destroy to match cuda::buffer. This is the name to use.
|
||||
void destroy()
|
||||
{
|
||||
this->deallocate(mData, mSize);
|
||||
mData = nullptr;
|
||||
mSize = 0;
|
||||
}
|
||||
|
||||
/// @brief Frees the buffer memory (if any) and resets to the empty state.
|
||||
/// @deprecated Use destroy(): the handles now dispatch to it directly.
|
||||
[[deprecated("Use cuda::Buffer::destroy instead")]]
|
||||
void clear() { this->destroy(); }
|
||||
|
||||
/// @brief Frees the buffer memory (if any) on @c stream and resets to the
|
||||
/// empty state. @c stream becomes the retained stream.
|
||||
/// @param stream cuda stream the 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 destroy(cudaStream_t stream)
|
||||
{
|
||||
this->mStream = stream;
|
||||
this->destroy();
|
||||
}
|
||||
|
||||
/// @brief Exchanges the contents of this buffer with @c other. Neither
|
||||
/// buffer allocates, frees, or copies element data.
|
||||
/// @param other buffer to exchange contents with
|
||||
void swap(Buffer& other) noexcept
|
||||
{
|
||||
auto& lhs = static_cast<detail::StreamHolder<IsAsync>&>(*this);
|
||||
auto& rhs = static_cast<detail::StreamHolder<IsAsync>&>(other);
|
||||
std::swap(lhs, rhs);
|
||||
std::swap(mResource, other.mResource);
|
||||
std::swap(mData, other.mData);
|
||||
std::swap(mSize, other.mSize);
|
||||
}
|
||||
|
||||
private:
|
||||
/// @brief Returns @c count * sizeof(T), throwing std::runtime_error if the
|
||||
/// byte size would overflow size_t instead of silently wrapping into
|
||||
@@ -351,18 +409,57 @@ public:
|
||||
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()
|
||||
/// without touching the underlying storage -- the view is
|
||||
/// non-owning, so "destroying" it releases nothing. This is the
|
||||
/// one deliberate deviation from std::span, required by the buffer
|
||||
/// static interface the handles consume through reset().
|
||||
void destroy()
|
||||
{
|
||||
mData = nullptr;
|
||||
mSize = 0;
|
||||
}
|
||||
|
||||
/// @brief Detaches the view.
|
||||
/// @deprecated Use destroy(): the handles now dispatch to it directly.
|
||||
[[deprecated("Use cuda::BufferView::destroy instead")]]
|
||||
void clear() { this->destroy(); }
|
||||
}; // BufferView<T> class
|
||||
|
||||
} // namespace cuda
|
||||
|
||||
// Primary template defined in HostBuffer.h; declared here so this header
|
||||
// stays self-contained without pulling in the host-buffer machinery.
|
||||
template<typename BufferT>
|
||||
struct BufferTraits;
|
||||
|
||||
/// @brief GridHandle support for the single-space cuda::Buffer: the buffer
|
||||
/// owns exactly one allocation, resident on the device, so the handle
|
||||
/// parses metadata through a device read and exposes only the device
|
||||
/// accessors. Requires byte-addressed storage.
|
||||
/// @note This trait doubles as the definition of the single-space
|
||||
/// device-buffer concept: a buffer whose BufferTraits specialization
|
||||
/// sets hasDeviceSingle guarantees ElementType and ResourceType
|
||||
/// typedefs, data(), size() and size_bytes() (byte-addressed elements,
|
||||
/// enforced by the consumer), resource(), copy(), destroy(), and stream()
|
||||
/// when the resource is stream-ordered. Any consumer of hasDeviceSingle
|
||||
/// may rely on exactly this interface and nothing more; in particular,
|
||||
/// scratch allocates through resource() as a cuda::Buffer, so a
|
||||
/// conforming buffer never needs to be constructible by a consumer.
|
||||
template<typename T, typename R>
|
||||
struct BufferTraits<cuda::Buffer<T, R>>
|
||||
{
|
||||
static constexpr bool hasDeviceDual = false;
|
||||
// Device-resident storage; the byte-addressed requirement is enforced by
|
||||
// the single-space GridHandle constructor, so trait queries stay
|
||||
// answerable for any element type.
|
||||
static constexpr bool hasDeviceSingle = !cuda::is_host_accessible_resource<R>::value;
|
||||
// A buffer over a host-accessible resource (e.g. PinnedResource) is
|
||||
// host-readable single-space storage: GridHandle parses its metadata on
|
||||
// the host, exposes the host accessors, and allocates reads and copies
|
||||
// through the buffer's resource.
|
||||
static constexpr bool hasHostSingle = cuda::is_host_accessible_resource<R>::value;
|
||||
};
|
||||
|
||||
} // namespace nanovdb
|
||||
|
||||
#endif // end of NANOVDB_CUDA_BUFFER_H_HAS_BEEN_INCLUDED
|
||||
|
||||
@@ -38,6 +38,7 @@ class DeviceBuffer
|
||||
uint64_t mSize; // total number of bytes managed by this buffer (assumed to be identical for host and device)
|
||||
void *mCpuData, **mGpuData; // raw pointers to the host and device buffers
|
||||
int mDeviceCount, mManaged;// if mManaged is non-zero this class is responsible for allocating and freeing memory buffers. Otherwise this is assumed to be handled externally
|
||||
cudaEvent_t *mEvents = nullptr;// per-device event marking the last use of each managed device buffer (parallel to mGpuData, length mDeviceCount). Every use waits on this event before issuing work and re-records it afterwards, so the single event transitively covers EVERY stream the buffer has been used on. Frees then wait on it, which orders them after all outstanding work: freeing on the default stream alone is only safe for blocking streams, and freeing on the last-used stream alone is only safe when just one stream was used.
|
||||
|
||||
/// @brief Initialize buffer
|
||||
/// @param size byte size of buffer to be initialized
|
||||
@@ -46,6 +47,33 @@ class DeviceBuffer
|
||||
/// @warning size is expected to be non-zero. Use clear() clear buffer!
|
||||
void init(uint64_t size, int device, cudaStream_t stream);
|
||||
|
||||
/// @brief Free every managed device allocation, each ordered after all tracked uses of the
|
||||
/// buffer, and destroy the tracking events.
|
||||
/// @param stream Stream the frees are issued on for allocations owned by the CURRENT device.
|
||||
/// A stream belongs to a single device, so it cannot carry frees for other devices'
|
||||
/// memory pools; allocations on other devices are freed on their own device's default
|
||||
/// stream, after switching to that device.
|
||||
/// @note Destroying an event with a pending wait is safe: CUDA releases it once the device
|
||||
/// has completed it.
|
||||
void freeDeviceBuffers(cudaStream_t stream)
|
||||
{
|
||||
int current = 0;
|
||||
cudaCheck(cudaGetDevice(¤t));
|
||||
for (int i = 0; i < mDeviceCount; ++i) {
|
||||
if (mGpuData[i]) {
|
||||
const cudaStream_t freeStream = (i == current) ? stream : cudaStream_t{0};
|
||||
if (i != current) cudaCheck(cudaSetDevice(i));
|
||||
this->orderAfterPriorUses(i, freeStream);
|
||||
cudaCheck(util::cuda::freeAsync(mGpuData[i], freeStream));
|
||||
if (i != current) cudaCheck(cudaSetDevice(current));
|
||||
}
|
||||
if (mEvents && mEvents[i]) {
|
||||
cudaCheck(cudaEventDestroy(mEvents[i]));
|
||||
mEvents[i] = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
using PtrT = std::shared_ptr<DeviceBuffer>;
|
||||
@@ -122,8 +150,10 @@ public:
|
||||
, mGpuData(other.mGpuData)
|
||||
, mDeviceCount(other.mDeviceCount)
|
||||
, mManaged(other.mManaged)
|
||||
, mEvents(other.mEvents)
|
||||
{
|
||||
other.mCpuData = other.mGpuData = nullptr;
|
||||
other.mEvents = nullptr;
|
||||
other.mSize = other.mDeviceCount = other.mManaged = 0;
|
||||
}
|
||||
|
||||
@@ -142,6 +172,8 @@ public:
|
||||
}
|
||||
|
||||
/// @brief Destructor frees memory on both the host and device
|
||||
/// @note Each managed device free waits on that device's tracking event first, so it is
|
||||
/// ordered after every stream the buffer was used on, not just the most recent one.
|
||||
~DeviceBuffer() { this->clear(); };
|
||||
|
||||
/// @brief Static factory method that return an instance of this buffer
|
||||
@@ -226,8 +258,59 @@ public:
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
|
||||
/// @brief Order work subsequently issued on @a stream after every prior use of this
|
||||
/// device buffer, whichever stream those uses were issued on. The consume-side
|
||||
/// companion of recordUse: an external consumer (e.g. a zero-copy array-interface
|
||||
/// export) calls this with its own stream before reading, so it cannot observe a
|
||||
/// partially-written buffer after asynchronous uploads or recorded kernels.
|
||||
/// @param device Device whose buffer is about to be read
|
||||
/// @param stream Stream the consumer's work will be issued on
|
||||
void orderAfterPriorUses(int device, cudaStream_t stream) const
|
||||
{
|
||||
if (mEvents && mEvents[device]) cudaCheck(cudaStreamWaitEvent(stream, mEvents[device], 0));
|
||||
}
|
||||
|
||||
/// @brief Record that this buffer's device data was just used on @a stream, so that the
|
||||
/// buffer's device frees (destructor, move-assignment, clear) are ordered after that
|
||||
/// work. Uses issued through deviceUpload/deviceDownload are recorded automatically;
|
||||
/// callers that enqueue their own kernels or copies against the raw pointer returned
|
||||
/// by deviceData() should call this afterwards. Without it, such work is only safe if
|
||||
/// it is on a blocking stream (which the free, issued on the default stream, waits on
|
||||
/// implicitly) or if the caller synchronizes before the buffer is cleared/destroyed.
|
||||
/// @param device Device whose buffer was used
|
||||
/// @param stream Stream the work was issued on
|
||||
/// @note Recording chains across streams: @a stream is first ordered after the previously
|
||||
/// recorded use (if any) so the single per-device event transitively covers every
|
||||
/// recorded use, not just the last one. Without this, concurrent uses on streams A
|
||||
/// then B would leave only B's event, and the device free could run while A's work
|
||||
/// is still in flight. The side effect is that work subsequently issued on @a stream
|
||||
/// also waits on the previously recorded use -- acceptable for a shared buffer, where
|
||||
/// later-recorded consumers observing earlier writes is the expected ordering. Note
|
||||
/// this also serializes CONCURRENT READERS that record uses (the single event cannot
|
||||
/// distinguish read-read from write-read); if that ever matters in a profile, the
|
||||
/// upgrade path is a read/write-separated or per-record event scheme, not a revert.
|
||||
void recordUse(int device, cudaStream_t stream)
|
||||
{
|
||||
if (!mEvents) return;
|
||||
if (mEvents[device] == nullptr) {// events are per-device, so create it on the right one
|
||||
int current = 0;
|
||||
cudaCheck(cudaGetDevice(¤t));
|
||||
if (current != device) cudaCheck(cudaSetDevice(device));
|
||||
cudaCheck(cudaEventCreateWithFlags(&mEvents[device], cudaEventDisableTiming));
|
||||
if (current != device) cudaCheck(cudaSetDevice(current));
|
||||
} else {
|
||||
// Re-recording MOVES the event; chain first so the new capture also covers the
|
||||
// prior recorded use (waiting on a never-recorded or completed event is a no-op).
|
||||
cudaCheck(cudaStreamWaitEvent(stream, mEvents[device], 0));
|
||||
}
|
||||
cudaCheck(cudaEventRecord(mEvents[device], stream));
|
||||
}
|
||||
|
||||
/// @brief Retuns a raw pointer to the specified device/GPU buffer managed by this allocator.
|
||||
/// @warning Note that the pointer can be NULL!
|
||||
/// @note Work enqueued against this raw pointer is invisible to the buffer's lifetime
|
||||
/// tracking: on a non-blocking stream, call recordUse afterwards (or synchronize
|
||||
/// before the buffer is cleared/destroyed) so the device free is ordered after it.
|
||||
void* deviceData(int device) const {
|
||||
NANOVDB_ASSERT(device >= 0 && device < mDeviceCount);
|
||||
return mGpuData[device];
|
||||
@@ -301,6 +384,10 @@ public:
|
||||
/// @}
|
||||
|
||||
/// @brief De-allocate all memory managed by this allocator and set all pointers to NULL
|
||||
/// @param stream Stream the device frees are issued on. The frees are additionally ordered
|
||||
/// after every stream the buffer was used on (via the per-device tracking event), so
|
||||
/// @a stream selects where the free is enqueued, not what it is ordered against - any
|
||||
/// stream is safe to pass here regardless of where the buffer was used.
|
||||
void clear(cudaStream_t stream = 0);
|
||||
void clear(void* stream){this->clear(cudaStream_t(stream));}
|
||||
|
||||
@@ -310,18 +397,22 @@ public:
|
||||
|
||||
inline DeviceBuffer& DeviceBuffer::operator=(DeviceBuffer&& other) noexcept
|
||||
{
|
||||
if (mManaged) {// first free all the managed data buffers
|
||||
if (this == &other) return *this;// self-move would free our buffers and then read them back
|
||||
if (mManaged) {// first free all the managed data buffers, ordered after every use of each
|
||||
cudaCheck(cudaFreeHost(mCpuData));
|
||||
for (int i=0; i<mDeviceCount; ++i) cudaCheck(util::cuda::freeAsync(mGpuData[i], 0));
|
||||
this->freeDeviceBuffers(cudaStream_t{0});
|
||||
}
|
||||
delete [] mGpuData;
|
||||
delete [] mEvents;
|
||||
mSize = other.mSize;
|
||||
mCpuData = other.mCpuData;
|
||||
mGpuData = other.mGpuData;
|
||||
mDeviceCount = other.mDeviceCount;
|
||||
mManaged = other.mManaged;
|
||||
mEvents = other.mEvents;
|
||||
other.mCpuData = nullptr;
|
||||
other.mGpuData = nullptr;
|
||||
other.mEvents = nullptr;
|
||||
other.mSize = 0;
|
||||
other.mDeviceCount = 0;
|
||||
other.mManaged = 0;
|
||||
@@ -333,6 +424,7 @@ inline void DeviceBuffer::init(uint64_t size, int device, cudaStream_t stream)
|
||||
if (size==0) return;
|
||||
cudaCheck(cudaGetDeviceCount(&mDeviceCount));
|
||||
mGpuData = new void*[mDeviceCount]();// NULL initialization
|
||||
mEvents = new cudaEvent_t[mDeviceCount]();// NULL initialization; created lazily on first use
|
||||
NANOVDB_ASSERT(device >= cudaCpuDeviceId && device < mDeviceCount);
|
||||
if (device == cudaCpuDeviceId) {
|
||||
cudaCheck(cudaMallocHost((void**)&mCpuData, size)); // un-managed pinned memory on the host (can be slow to access!). Always 32B aligned
|
||||
@@ -340,6 +432,7 @@ inline void DeviceBuffer::init(uint64_t size, int device, cudaStream_t stream)
|
||||
} else {
|
||||
cudaCheck(util::cuda::mallocAsync(mGpuData+device, size, stream)); // un-managed memory on the device, always 32B aligned!
|
||||
checkPtr(mGpuData[device], "cuda::DeviceBuffer::init: failed to allocate device buffer");
|
||||
this->recordUse(device, stream);// the free must be ordered after this allocation
|
||||
}
|
||||
mSize = size;
|
||||
mManaged = 1;// i.e. this instance is responsible for allocating and delete memory
|
||||
@@ -354,7 +447,11 @@ inline void DeviceBuffer::deviceUpload(int device, cudaStream_t stream, bool syn
|
||||
cudaCheck(util::cuda::mallocAsync(mGpuData+device, mSize, stream)); // un-managed memory on the device, always 32B aligned!
|
||||
}
|
||||
checkPtr(mGpuData[device], "uninitialized gpu destination data");
|
||||
// Order this transfer after any use of the buffer on another stream, then mark it as the
|
||||
// latest use, so the tracking event keeps covering every stream the buffer has seen.
|
||||
this->orderAfterPriorUses(device, stream);
|
||||
cudaCheck(cudaMemcpyAsync(mGpuData[device], mCpuData, mSize, cudaMemcpyHostToDevice, stream));
|
||||
this->recordUse(device, stream);
|
||||
if (sync) cudaCheck(cudaStreamSynchronize(stream));
|
||||
} // DeviceBuffer::deviceUpload
|
||||
|
||||
@@ -374,7 +471,9 @@ inline void DeviceBuffer::deviceDownload(int device, cudaStream_t stream, bool s
|
||||
cudaCheck(cudaMallocHost((void**)&mCpuData, mSize)); // un-managed pinned memory on the host (can be slow to access!). Always 32B aligned
|
||||
}
|
||||
checkPtr(mCpuData, "uninitialized cpu destination data");
|
||||
this->orderAfterPriorUses(device, stream);
|
||||
cudaCheck(cudaMemcpyAsync(mCpuData, mGpuData[device], mSize, cudaMemcpyDeviceToHost, stream));
|
||||
this->recordUse(device, stream);
|
||||
if (sync) cudaCheck(cudaStreamSynchronize(stream));
|
||||
} // DeviceBuffer::deviceDownload
|
||||
|
||||
@@ -387,13 +486,15 @@ inline void DeviceBuffer::deviceDownload(void* stream, bool sync)
|
||||
|
||||
inline void DeviceBuffer::clear(cudaStream_t stream)
|
||||
{
|
||||
if (mManaged) {// free all the managed data buffers
|
||||
if (mManaged) {// free all the managed data buffers, ordered after every use of each
|
||||
cudaCheck(cudaFreeHost(mCpuData));
|
||||
for (int i=0; i<mDeviceCount; ++i) cudaCheck(util::cuda::freeAsync(mGpuData[i], stream));
|
||||
this->freeDeviceBuffers(stream);
|
||||
}
|
||||
delete [] mGpuData;
|
||||
delete [] mEvents;
|
||||
mCpuData = nullptr;
|
||||
mGpuData = nullptr;
|
||||
mEvents = nullptr;
|
||||
mSize = 0;
|
||||
mDeviceCount = 0;
|
||||
mManaged = 0;
|
||||
|
||||
@@ -123,6 +123,17 @@ struct is_async_resource<R, std::void_t<
|
||||
decltype(std::declval<R&>().deallocate(std::declval<void*>(), size_t{0}, size_t{0}))>>
|
||||
: std::true_type {};
|
||||
|
||||
/// @brief Detection trait: @c is_host_accessible_resource<R>::value is true
|
||||
/// iff @c R declares `static constexpr bool HOST_ACCESSIBLE = true`,
|
||||
/// i.e. its allocations are mapped into the host address space (e.g.
|
||||
/// PinnedResource). Defaults to false: allocations are device-resident.
|
||||
/// @note Unlike the void_t detections above, this checks the member's VALUE:
|
||||
/// a resource declaring HOST_ACCESSIBLE = false stays device-resident.
|
||||
template<typename R, typename = void>
|
||||
struct is_host_accessible_resource : std::false_type {};
|
||||
template<typename R>
|
||||
struct is_host_accessible_resource<R, typename std::enable_if<bool(R::HOST_ACCESSIBLE)>::type> : 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).
|
||||
@@ -145,6 +156,185 @@ struct is_resource<R, std::void_t<
|
||||
decltype(std::declval<R&>().deallocate(std::declval<void*>(), size_t{0}, size_t{0}))>>
|
||||
: std::true_type {};
|
||||
|
||||
/// @brief CRTP base supplying the synchronous half of the resource concept in
|
||||
/// terms of the stream-ordered half, so a custom stream-ordered resource
|
||||
/// only has to write allocate_async and deallocate_async.
|
||||
/// @tparam Derived the resource deriving from this base
|
||||
/// @details A stream-ordered resource must also model the synchronous concept
|
||||
/// (is_async_resource implies is_resource), which means writing four
|
||||
/// methods where two would do. The synchronous pair is not a bare
|
||||
/// delegate: memory from allocate must be usable immediately on any
|
||||
/// stream, so the null-stream allocation has to be synchronized before
|
||||
/// it is returned. Omitting that synchronization yields memory that
|
||||
/// satisfies the concept but is not actually synchronous -- a race
|
||||
/// rather than a compile error -- so it lives here rather than being
|
||||
/// rewritten per resource.
|
||||
/// @code
|
||||
/// struct MyResource : nanovdb::cuda::SyncFromAsync<MyResource> {
|
||||
/// static constexpr size_t DEFAULT_ALIGNMENT = 256;
|
||||
/// void* allocate_async(size_t bytes, size_t alignment, cudaStream_t stream);
|
||||
/// void deallocate_async(void* p, size_t bytes, size_t alignment, cudaStream_t stream);
|
||||
/// };
|
||||
/// @endcode
|
||||
template <class Derived>
|
||||
struct SyncFromAsync
|
||||
{
|
||||
/// @brief Allocates @c bytes usable on any stream when this returns.
|
||||
/// @param bytes number of bytes to allocate
|
||||
/// @param alignment requested alignment
|
||||
/// @note Every call synchronizes the null stream; on hot paths prefer the
|
||||
/// stream-ordered pair.
|
||||
void* allocate(size_t bytes, size_t alignment)
|
||||
{
|
||||
void* p = static_cast<Derived&>(*this).allocate_async(bytes, alignment, cudaStream_t{0});
|
||||
cudaCheck(cudaStreamSynchronize(cudaStream_t{0}));
|
||||
return p;
|
||||
}
|
||||
|
||||
/// @brief Frees @c p on the null stream.
|
||||
/// @param p pointer previously returned by allocate
|
||||
/// @param bytes size passed to the matching allocate
|
||||
/// @param alignment alignment passed to the matching allocate
|
||||
/// @note No synchronization here: the synchronous concept's contract is
|
||||
/// that the memory is already quiescent when deallocate is called.
|
||||
void deallocate(void* p, size_t bytes, size_t alignment)
|
||||
{
|
||||
static_cast<Derived&>(*this).deallocate_async(p, bytes, alignment, cudaStream_t{0});
|
||||
}
|
||||
};
|
||||
|
||||
/// @brief Synchronous device memory resource backed by cudaMalloc/cudaFree.
|
||||
/// Models only the Resource concept: it never touches stream-ordered
|
||||
/// allocation, so it works on devices without memory-pool support
|
||||
/// (cudaDevAttrMemoryPoolsSupported == 0), where DeviceResource's
|
||||
/// cudaMallocAsync path fails by design. Pair with AsyncFromSync to
|
||||
/// drive the stream-ordered builders on such a device.
|
||||
class MallocResource
|
||||
{
|
||||
public:
|
||||
// cudaMalloc aligns memory to 256 bytes by default
|
||||
static constexpr size_t DEFAULT_ALIGNMENT = 256;
|
||||
|
||||
/// @brief Allocates @c bytes with cudaMalloc; valid on every stream when
|
||||
/// this returns. A zero request returns nullptr.
|
||||
void* allocate(size_t bytes, size_t)
|
||||
{
|
||||
if (bytes == 0) return nullptr;
|
||||
void* p = nullptr;
|
||||
cudaCheck(cudaMalloc(&p, bytes));
|
||||
return p;
|
||||
}
|
||||
|
||||
/// @brief Frees @c p with cudaFree; the caller guarantees that device work
|
||||
/// touching the memory has completed.
|
||||
void deallocate(void* p, size_t, size_t) { cudaCheck(cudaFree(p)); }
|
||||
};// MallocResource
|
||||
|
||||
/// @brief Wrapper presenting a synchronous resource as a stream-ordered one,
|
||||
/// so it can drive components that require the AsyncResource concept
|
||||
/// (TempPool and the GPU builders).
|
||||
/// @tparam R the wrapped synchronous resource, held by value; wrap a
|
||||
/// ResourceRef<R> to borrow a stateful instance instead.
|
||||
/// @details The mirror of SyncFromAsync, and the analog of cuda::mr's
|
||||
/// synchronous_resource_adapter. allocate_async forwards to
|
||||
/// R::allocate, whose memory is immediately valid on every stream --
|
||||
/// a stronger guarantee than stream-ordering requires.
|
||||
/// deallocate_async synchronizes @c stream before R::deallocate,
|
||||
/// establishing the quiescence the synchronous contract demands.
|
||||
/// @warning Every deallocation synchronizes its stream, so expect
|
||||
/// serialization relative to a genuinely stream-ordered resource.
|
||||
/// That is the unavoidable cost of a synchronous backend under a
|
||||
/// stream-ordered algorithm; this wrapper exists so the cost is
|
||||
/// explicit and chosen by the caller -- e.g. on a device without
|
||||
/// memory-pool support -- rather than silently substituted.
|
||||
template<class R>
|
||||
struct AsyncFromSync
|
||||
{
|
||||
static_assert(is_resource<R>::value,
|
||||
"AsyncFromSync requires R to model the synchronous Resource concept");
|
||||
|
||||
static constexpr size_t DEFAULT_ALIGNMENT = R::DEFAULT_ALIGNMENT;
|
||||
|
||||
/// @brief The adapter is host-accessible iff the adapted resource is.
|
||||
static constexpr bool HOST_ACCESSIBLE = is_host_accessible_resource<R>::value;
|
||||
|
||||
R resource;
|
||||
|
||||
/// @brief Allocates through the synchronous resource; the result is valid
|
||||
/// on every stream, hence trivially valid on @c stream.
|
||||
void* allocate_async(size_t bytes, size_t alignment, cudaStream_t) { return resource.allocate(bytes, alignment); }
|
||||
|
||||
/// @brief Synchronizes @c stream, then frees through the synchronous
|
||||
/// resource -- the synchronize makes the quiescence contract hold.
|
||||
/// Null is a no-op and skips the synchronize.
|
||||
void deallocate_async(void* p, size_t bytes, size_t alignment, cudaStream_t stream)
|
||||
{
|
||||
if (p == nullptr) return;
|
||||
cudaCheck(cudaStreamSynchronize(stream));
|
||||
resource.deallocate(p, bytes, alignment);
|
||||
}
|
||||
|
||||
/// @brief Synchronous pair, forwarding to the wrapped resource.
|
||||
void* allocate(size_t bytes, size_t alignment) { return resource.allocate(bytes, alignment); }
|
||||
void deallocate(void* p, size_t bytes, size_t alignment) { resource.deallocate(p, bytes, alignment); }
|
||||
};// AsyncFromSync<R>
|
||||
|
||||
/// @brief Non-owning reference to a memory resource that is itself a resource:
|
||||
/// copying the ref shares the underlying resource rather than copying it.
|
||||
/// @tparam R the referenced resource type
|
||||
/// @details Types that hold their resource by value -- cuda::Buffer, matching
|
||||
/// cuda::buffer -- select their ownership semantics by what is placed
|
||||
/// in that slot: a concrete resource is owned as a copy, while a
|
||||
/// ResourceRef borrows. This is the same division cuda::mr draws
|
||||
/// between any_resource (owning) and resource_ref (borrowing), and the
|
||||
/// same shape as std::pmr::polymorphic_allocator over memory_resource*.
|
||||
/// Use it when a resource is stateful or long-lived and a container
|
||||
/// must allocate through *that* instance rather than a copy of it.
|
||||
/// @warning The referenced resource must outlive every use of this ref and of
|
||||
/// all copies of it, including any container holding one.
|
||||
template <class R>
|
||||
struct ResourceRef
|
||||
{
|
||||
static_assert(is_async_resource<R>::value || is_resource<R>::value,
|
||||
"ResourceRef requires R to model the AsyncResource or the Resource concept");
|
||||
|
||||
static constexpr size_t DEFAULT_ALIGNMENT = R::DEFAULT_ALIGNMENT;
|
||||
|
||||
/// @brief A reference is host-accessible iff the referenced resource is.
|
||||
static constexpr bool HOST_ACCESSIBLE = is_host_accessible_resource<R>::value;
|
||||
|
||||
/// @brief Constructs a ref borrowing @c resource.
|
||||
/// @param resource resource to allocate from; must outlive this ref
|
||||
ResourceRef(R& resource) : mResource(&resource) {}
|
||||
|
||||
/// @{
|
||||
/// @brief Stream-ordered pair, present only when @c R models AsyncResource,
|
||||
/// so a ref over a synchronous resource does not misreport its tier.
|
||||
template<class S = R, std::enable_if_t<is_async_resource<S>::value, int> = 0>
|
||||
void* allocate_async(size_t bytes, size_t alignment, cudaStream_t stream)
|
||||
{
|
||||
return mResource->allocate_async(bytes, alignment, stream);
|
||||
}
|
||||
template<class S = R, std::enable_if_t<is_async_resource<S>::value, int> = 0>
|
||||
void deallocate_async(void* p, size_t bytes, size_t alignment, cudaStream_t stream)
|
||||
{
|
||||
mResource->deallocate_async(p, bytes, alignment, stream);
|
||||
}
|
||||
/// @}
|
||||
|
||||
/// @brief Synchronous pair, forwarding to the referenced resource.
|
||||
void* allocate(size_t bytes, size_t alignment) { return mResource->allocate(bytes, alignment); }
|
||||
void deallocate(void* p, size_t bytes, size_t alignment) { mResource->deallocate(p, bytes, alignment); }
|
||||
|
||||
/// @brief Two refs compare equal iff they reference the same resource, i.e.
|
||||
/// memory allocated through one may be deallocated through the other.
|
||||
friend bool operator==(ResourceRef lhs, ResourceRef rhs) { return lhs.mResource == rhs.mResource; }
|
||||
friend bool operator!=(ResourceRef lhs, ResourceRef rhs) { return lhs.mResource != rhs.mResource; }
|
||||
|
||||
private:
|
||||
R* mResource;
|
||||
};// ResourceRef<R>
|
||||
|
||||
}
|
||||
|
||||
} // namespace nanovdb::cuda
|
||||
|
||||
@@ -17,21 +17,20 @@
|
||||
#ifndef NANOVDB_CUDA_GRIDHANDLE_CUH_HAS_BEEN_INCLUDED
|
||||
#define NANOVDB_CUDA_GRIDHANDLE_CUH_HAS_BEEN_INCLUDED
|
||||
|
||||
#include <nanovdb/cuda/Buffer.h>// for the resource-aware scratch buffers below
|
||||
#include <nanovdb/cuda/DeviceBuffer.h>// required for instantiation of move c-tor of GridHandle
|
||||
#include <nanovdb/tools/cuda/GridChecksum.cuh>// for cuda::updateChecksum
|
||||
#include <nanovdb/GridHandle.h>
|
||||
|
||||
#include <string>// for the grid index in chain-validation error messages
|
||||
#include <type_traits>// for std::is_default_constructible in cuda::copyTo
|
||||
|
||||
namespace nanovdb {
|
||||
|
||||
namespace cuda {
|
||||
|
||||
namespace detail {
|
||||
|
||||
static __global__ void cpyGridHandleMeta(const GridData *d_data, GridHandleMetaData *d_meta)
|
||||
{
|
||||
nanovdb::cpyGridHandleMeta(d_data, d_meta);
|
||||
}
|
||||
|
||||
static __global__ void updateGridCount(GridData *d_data, uint32_t gridIndex, uint32_t gridCount, bool *d_dirty)
|
||||
{
|
||||
NANOVDB_ASSERT(gridIndex < gridCount);
|
||||
@@ -43,6 +42,145 @@ static __global__ void updateGridCount(GridData *d_data, uint32_t gridIndex, uin
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief Defect classes the device-side chain walk can report, mapped to
|
||||
/// exceptions on the host by parseGridChain.
|
||||
enum class ChainError : uint32_t { Ok = 0, Truncated, Invalid, Inconsistent, BadSize };
|
||||
|
||||
/// @brief Result of the device-side chain walk: the defect class and the
|
||||
/// index of the grid the walk failed at.
|
||||
struct ChainStatus { ChainError error; uint32_t gridIndex; };
|
||||
|
||||
/// @brief Validates every header of the grid chain and fills the metadata
|
||||
/// scratch in a single pass. Bounds are checked before every header
|
||||
/// read, so a truncated buffer or a forged header can never cause an
|
||||
/// out-of-bounds access.
|
||||
static __global__ void parseGridChainKernel(const GridData *d_head, uint64_t bytes, uint32_t count,
|
||||
GridHandleMetaData *d_meta, ChainStatus *d_status)
|
||||
{
|
||||
*d_status = ChainStatus{ChainError::Ok, 0u};
|
||||
uint64_t offset = 0;
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
if (offset + sizeof(GridData) > bytes) { *d_status = {ChainError::Truncated, i}; return; }
|
||||
const GridData *data = util::PtrAdd<GridData>(d_head, offset);
|
||||
if (!data->isValid()) { *d_status = {ChainError::Invalid, i}; return; }
|
||||
if (data->mGridIndex != i || data->mGridCount != count) { *d_status = {ChainError::Inconsistent, i}; return; }
|
||||
if (data->mGridSize < sizeof(GridData) || data->mGridSize > bytes - offset) { *d_status = {ChainError::BadSize, i}; return; }
|
||||
d_meta[i] = GridHandleMetaData{offset, data->mGridSize, data->mGridType};
|
||||
offset += data->mGridSize;
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief Reads and validates the head of the grid chain, returning the grid
|
||||
/// count the chain claims. This is the one synchronizing header read
|
||||
/// that sizing the metadata scratch requires; the rest of the chain is
|
||||
/// validated on the device by parseGridChain.
|
||||
inline uint32_t validGridChainHead(const GridData *d_head, uint64_t bytes, cudaStream_t stream)
|
||||
{
|
||||
if (bytes < sizeof(GridData))
|
||||
throw std::runtime_error("GridHandle: grid chain exceeds the device buffer (truncated or corrupt grid data)");
|
||||
GridData tmp;
|
||||
cudaCheck(cudaMemcpyAsync(&tmp, d_head, sizeof(GridData), cudaMemcpyDeviceToHost, stream));
|
||||
cudaCheck(cudaStreamSynchronize(stream));
|
||||
if (!tmp.isValid()) throw std::runtime_error("GridHandle was constructed with an invalid device buffer");
|
||||
if (tmp.mGridCount == 0) throw std::runtime_error("GridHandle: device buffer contains no grids");
|
||||
if (tmp.mGridIndex != 0)
|
||||
throw std::runtime_error("GridHandle: inconsistent grid index/count in the device buffer's grid chain");
|
||||
if (uint64_t(tmp.mGridCount) > bytes / sizeof(GridData))// every grid is at least one full header
|
||||
throw std::runtime_error("GridHandle: grid chain exceeds the device buffer (truncated or corrupt grid data)");
|
||||
return tmp.mGridCount;
|
||||
}
|
||||
|
||||
/// @brief Bytes of device scratch parseGridChain needs for @c count grids:
|
||||
/// the metadata array followed by the status word.
|
||||
inline constexpr uint64_t chainScratchSize(uint32_t count)
|
||||
{
|
||||
return count * sizeof(GridHandleMetaData) + sizeof(ChainStatus);
|
||||
}
|
||||
|
||||
/// @brief Launches the combined validate-and-parse walk over the grid chain
|
||||
/// and fills @c meta from its scratch: one kernel, one readback and
|
||||
/// one synchronization regardless of the grid count.
|
||||
/// @param scratch device scratch of at least chainScratchSize(count) bytes
|
||||
/// @throw std::runtime_error naming the defect when any header fails validation
|
||||
template<typename ScratchBufferT>
|
||||
inline void parseGridChain(const GridData *d_head, uint64_t bytes, uint32_t count,
|
||||
ScratchBufferT &scratch, std::vector<GridHandleMetaData> &meta, cudaStream_t stream)
|
||||
{
|
||||
auto *d_meta = reinterpret_cast<GridHandleMetaData*>(scratch.data());
|
||||
auto *d_status = reinterpret_cast<ChainStatus*>(scratch.data() + count * sizeof(GridHandleMetaData));
|
||||
parseGridChainKernel<<<1, 1, 0, stream>>>(d_head, bytes, count, d_meta, d_status);
|
||||
cudaCheckError();
|
||||
meta.resize(count);
|
||||
ChainStatus status;
|
||||
cudaCheck(cudaMemcpyAsync(meta.data(), d_meta, count * sizeof(GridHandleMetaData), cudaMemcpyDeviceToHost, stream));
|
||||
cudaCheck(cudaMemcpyAsync(&status, d_status, sizeof(ChainStatus), cudaMemcpyDeviceToHost, stream));
|
||||
cudaCheck(cudaStreamSynchronize(stream));
|
||||
if (status.error == ChainError::Ok) return;
|
||||
const std::string where = " (grid " + std::to_string(status.gridIndex) + " of " + std::to_string(count) + ")";
|
||||
switch (status.error) {
|
||||
case ChainError::Truncated:
|
||||
throw std::runtime_error("GridHandle: grid chain exceeds the device buffer (truncated or corrupt grid data)" + where);
|
||||
case ChainError::Inconsistent:
|
||||
throw std::runtime_error("GridHandle: inconsistent grid index/count in the device buffer's grid chain" + where);
|
||||
case ChainError::BadSize:
|
||||
throw std::runtime_error("GridHandle: grid size field exceeds the device buffer (truncated or corrupt grid data)" + where);
|
||||
default:
|
||||
throw std::runtime_error("GridHandle was constructed with an invalid device buffer" + where);
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief The buffer's retained stream when its resource is stream-ordered,
|
||||
/// the default stream otherwise.
|
||||
template<typename BufferT>
|
||||
inline cudaStream_t retainedStreamOrDefault(const BufferT& buf)
|
||||
{
|
||||
if constexpr (is_async_resource<typename BufferT::ResourceType>::value) return buf.stream();
|
||||
else { (void)buf; return cudaStream_t(0); }
|
||||
}
|
||||
|
||||
/// @brief Constructs the metadata scratch through @c buf's resource, on
|
||||
/// @c stream for a stream-ordered resource.
|
||||
template<typename ScratchT, typename BufferT>
|
||||
inline ScratchT makeMetaScratch(const BufferT& buf, uint64_t count, cudaStream_t stream)
|
||||
{
|
||||
if constexpr (is_async_resource<typename BufferT::ResourceType>::value)
|
||||
return ScratchT(stream, buf.resource(), count, noInit);
|
||||
else { (void)stream; return ScratchT(buf.resource(), count, noInit); }
|
||||
}
|
||||
|
||||
/// @brief Allocates @c bytes of destination storage for a cross-space
|
||||
/// transfer: single-space buffers allocate through @c proto's resource
|
||||
/// (or a default-constructed resource without one), on @c stream when
|
||||
/// the resource is stream-ordered; buffers providing create() go
|
||||
/// through it.
|
||||
template<typename DstBufferT>
|
||||
inline DstBufferT makeTransferStorage(uint64_t bytes, cudaStream_t stream, const DstBufferT* proto)
|
||||
{
|
||||
if constexpr (BufferHasDeviceSingle<DstBufferT>::value || BufferHasHostSingle<DstBufferT>::value) {
|
||||
using ResourceT = typename DstBufferT::ResourceType;
|
||||
if (!proto) {
|
||||
// both branches of a plain conditional would instantiate the
|
||||
// default-resource constructor, breaking non-default-constructible
|
||||
// resources (e.g. ResourceRef) even for callers that pass a proto
|
||||
if constexpr (std::is_default_constructible<ResourceT>::value) {
|
||||
if constexpr (is_async_resource<ResourceT>::value) return DstBufferT(stream, bytes, noInit);
|
||||
else return DstBufferT(bytes, noInit);
|
||||
} else {
|
||||
throw std::runtime_error("cuda::copyTo: a destination buffer over a non-default-constructible "
|
||||
"resource requires a prototype buffer");
|
||||
}
|
||||
}
|
||||
if constexpr (is_async_resource<ResourceT>::value) {
|
||||
return DstBufferT(stream, proto->resource(), bytes, noInit);
|
||||
} else {
|
||||
(void)stream;
|
||||
return DstBufferT(proto->resource(), bytes, noInit);
|
||||
}
|
||||
} else {
|
||||
return DstBufferT::create(bytes, proto);
|
||||
}
|
||||
}
|
||||
|
||||
}// namespace detail
|
||||
|
||||
template<typename BufferT, template <class, class...> class VectorT = std::vector>
|
||||
@@ -52,10 +190,11 @@ splitGridHandles(const GridHandle<BufferT> &handle, const BufferT* other = nullp
|
||||
const void *ptr = handle.deviceData();
|
||||
if (ptr == nullptr) return VectorT<GridHandle<BufferT>>();
|
||||
VectorT<GridHandle<BufferT>> handles(handle.gridCount());
|
||||
bool dirty, *d_dirty;// use this to check if the checksum needs to be recomputed
|
||||
cudaCheck(util::cuda::mallocAsync((void**)&d_dirty, sizeof(bool), stream));
|
||||
Buffer<bool, DeviceResource> dirtyBuf(stream, 1, noInit);
|
||||
bool *d_dirty = dirtyBuf.data();
|
||||
int device = util::cuda::currentDevice();
|
||||
for (uint32_t n=0; n<handle.gridCount(); ++n) {
|
||||
bool dirty = false;// set when the checksum needs to be recomputed
|
||||
auto buffer = BufferT::create(handle.gridSize(n), other, device, stream);
|
||||
GridData *dst = reinterpret_cast<GridData*>(buffer.deviceData());
|
||||
const GridData *src = reinterpret_cast<const GridData*>(ptr);
|
||||
@@ -68,7 +207,6 @@ splitGridHandles(const GridHandle<BufferT> &handle, const BufferT* other = nullp
|
||||
handles[n] = nanovdb::GridHandle<BufferT>(std::move(buffer));
|
||||
ptr = util::PtrAdd(ptr, handle.gridSize(n));
|
||||
}
|
||||
cudaCheck(util::cuda::freeAsync(d_dirty, stream));
|
||||
return handles;
|
||||
}// cuda::splitGridHandles
|
||||
|
||||
@@ -85,11 +223,12 @@ mergeGridHandles(const VectorT<GridHandle<BufferT>> &handles, const BufferT* oth
|
||||
int device = util::cuda::currentDevice();
|
||||
auto buffer = BufferT::create(size, other, device, stream);
|
||||
void *dst = buffer.deviceData();
|
||||
bool dirty, *d_dirty;// use this to check if the checksum needs to be recomputed
|
||||
cudaCheck(util::cuda::mallocAsync((void**)&d_dirty, sizeof(bool), stream));
|
||||
Buffer<bool, DeviceResource> dirtyBuf(stream, 1, noInit);
|
||||
bool *d_dirty = dirtyBuf.data();
|
||||
for (auto &h : handles) {
|
||||
const void *src = h.deviceData();
|
||||
for (uint32_t n=0; n<h.gridCount(); ++n) {
|
||||
bool dirty = false;// set when the checksum needs to be recomputed
|
||||
cudaCheck(cudaMemcpyAsync(dst, src, h.gridSize(n), cudaMemcpyDeviceToDevice, stream));
|
||||
GridData *data = reinterpret_cast<GridData*>(dst);
|
||||
detail::updateGridCount<<<1, 1, 0, stream>>>(data, counter++, gridCount, d_dirty);
|
||||
@@ -101,10 +240,82 @@ mergeGridHandles(const VectorT<GridHandle<BufferT>> &handles, const BufferT* oth
|
||||
src = util::PtrAdd(src, h.gridSize(n));
|
||||
}
|
||||
}
|
||||
cudaCheck(util::cuda::freeAsync(d_dirty, stream));
|
||||
return GridHandle<BufferT>(std::move(buffer));
|
||||
}// cuda::mergeGridHandles
|
||||
|
||||
/// @brief Deep-copies a grid handle into a different address space: the
|
||||
/// explicit, stream-carrying transfer between single-space device
|
||||
/// handles and host-readable handles (HostBuffer or a host-accessible
|
||||
/// single-space buffer such as a pinned-resource cuda::Buffer).
|
||||
/// @tparam DstBufferT destination buffer type (specify explicitly)
|
||||
/// @param src the handle to copy; must not be dual-space (use
|
||||
/// deviceUpload/deviceDownload on those)
|
||||
/// @param stream stream the copy is issued on; a device destination buffer
|
||||
/// with a stream-ordered resource retains it
|
||||
/// @warning Passing a stream other than the source buffer's retained stream
|
||||
/// makes the caller responsible for ordering: prior work on the
|
||||
/// source (and the source's later destruction, which frees on its
|
||||
/// own stream) must be ordered against @a stream by the caller,
|
||||
/// e.g. with cudaStreamWaitEvent or a synchronization. The
|
||||
/// stream-less overload below has no such requirement.
|
||||
/// @param proto optional buffer whose resource (or pool, for buffers
|
||||
/// providing create()) allocates the destination storage; without it
|
||||
/// the destination resource is default-constructed
|
||||
/// @return a handle of the destination buffer type with equal contents
|
||||
/// @details The returned handle is immediately usable: a host-readable
|
||||
/// destination synchronizes @c stream before returning, and a device
|
||||
/// destination parses (and validates) its metadata on the
|
||||
/// transferred bytes, which synchronizes internally. A pageable host
|
||||
/// source or destination (HostBuffer) degrades the copy to
|
||||
/// synchronous behavior; pinned single-space handles keep it
|
||||
/// asynchronous.
|
||||
template<typename DstBufferT, typename SrcBufferT>
|
||||
inline GridHandle<DstBufferT> copyTo(const GridHandle<SrcBufferT>& src, cudaStream_t stream, const DstBufferT* proto = nullptr)
|
||||
{
|
||||
constexpr bool srcDev = BufferHasDeviceSingle<SrcBufferT>::value;
|
||||
constexpr bool dstDev = BufferHasDeviceSingle<DstBufferT>::value;
|
||||
static_assert(!BufferTraits<SrcBufferT>::hasDeviceDual && !BufferTraits<DstBufferT>::hasDeviceDual,
|
||||
"cuda::copyTo does not support dual-space buffers: use deviceUpload/deviceDownload on the handle");
|
||||
static_assert(srcDev || dstDev,
|
||||
"cuda::copyTo is for cross-space transfers involving a device buffer: use GridHandle::copy for host-to-host");
|
||||
const uint64_t bytes = src.bufferSize();
|
||||
if (bytes == 0u) {
|
||||
if constexpr (std::is_default_constructible<DstBufferT>::value) {
|
||||
return GridHandle<DstBufferT>();
|
||||
} else {
|
||||
throw std::runtime_error("cuda::copyTo: an empty handle cannot be copied to a buffer type "
|
||||
"that is not default-constructible");
|
||||
}
|
||||
}
|
||||
DstBufferT dst = detail::makeTransferStorage<DstBufferT>(bytes, stream, proto);
|
||||
const void* srcPtr;
|
||||
if constexpr (srcDev) srcPtr = src.deviceData();
|
||||
else srcPtr = src.data();
|
||||
constexpr cudaMemcpyKind kind = srcDev ? (dstDev ? cudaMemcpyDeviceToDevice : cudaMemcpyDeviceToHost)
|
||||
: cudaMemcpyHostToDevice;
|
||||
cudaCheck(cudaMemcpyAsync(dst.data(), srcPtr, bytes, kind, stream));
|
||||
if constexpr (dstDev) {
|
||||
// A synchronous destination resource retains no stream: order the
|
||||
// metadata parse (which runs on the default stream) after the copy.
|
||||
if constexpr (!is_async_resource<typename DstBufferT::ResourceType>::value)
|
||||
cudaCheck(cudaStreamSynchronize(stream));
|
||||
} else {
|
||||
cudaCheck(cudaStreamSynchronize(stream));// the host-readable result is the postcondition
|
||||
}
|
||||
return GridHandle<DstBufferT>(std::move(dst));// the constructor parses (and validates) the metadata
|
||||
}// cuda::copyTo
|
||||
|
||||
/// @brief Convenience overload issuing the copy on the source buffer's
|
||||
/// retained stream when it has one (any single-space source over a
|
||||
/// stream-ordered resource), the default stream otherwise.
|
||||
template<typename DstBufferT, typename SrcBufferT>
|
||||
inline GridHandle<DstBufferT> copyTo(const GridHandle<SrcBufferT>& src, const DstBufferT* proto = nullptr)
|
||||
{
|
||||
cudaStream_t stream = 0;
|
||||
if constexpr (BufferHasStream<SrcBufferT>::value) stream = src.buffer().stream();
|
||||
return copyTo<DstBufferT>(src, stream, proto);
|
||||
}// cuda::copyTo (retained stream)
|
||||
|
||||
}// namespace cuda
|
||||
|
||||
template<typename BufferT, template <class, class...> class VectorT = std::vector>
|
||||
@@ -122,30 +333,49 @@ mergeDeviceGrids(const VectorT<GridHandle<BufferT>> &handles, const BufferT* oth
|
||||
template<typename BufferT>
|
||||
template<typename T, typename util::enable_if<BufferTraits<T>::hasDeviceDual, int>::type>
|
||||
GridHandle<BufferT>::GridHandle(T&& buffer)
|
||||
: mBuffer(std::move(buffer))
|
||||
{
|
||||
static_assert(util::is_same<T,BufferT>::value, "Expected U==BufferT");
|
||||
mBuffer = std::move(buffer);
|
||||
if (auto *data = reinterpret_cast<const GridData*>(mBuffer.data())) {
|
||||
if (!data->isValid()) throw std::runtime_error("GridHandle was constructed with an invalid host buffer");
|
||||
mMetaData.resize(data->mGridCount);
|
||||
cpyGridHandleMeta(data, mMetaData.data());
|
||||
} else {
|
||||
if (auto *d_data = reinterpret_cast<const GridData*>(mBuffer.deviceData())) {
|
||||
GridData tmp;
|
||||
cudaCheck(cudaMemcpy(&tmp, d_data, sizeof(GridData), cudaMemcpyDeviceToHost));
|
||||
if (!tmp.isValid()) throw std::runtime_error("GridHandle was constructed with an invalid device buffer");
|
||||
GridHandleMetaData *d_metaData;
|
||||
cudaMalloc((void**)&d_metaData, tmp.mGridCount*sizeof(GridHandleMetaData));
|
||||
cuda::detail::cpyGridHandleMeta<<<1,1>>>(d_data, d_metaData);
|
||||
mMetaData.resize(tmp.mGridCount);
|
||||
cudaCheck(cudaMemcpy(mMetaData.data(), d_metaData,tmp.mGridCount*sizeof(GridHandleMetaData), cudaMemcpyDeviceToHost));
|
||||
cudaCheck(cudaFree(d_metaData));
|
||||
const uint32_t count = cuda::detail::validGridChainHead(d_data, mBuffer.size(), cudaStream_t(0));
|
||||
// MallocResource: plain cudaMalloc, so this long-standing parse path
|
||||
// keeps working on devices without memory-pool support.
|
||||
cuda::Buffer<std::byte, cuda::MallocResource> scratch(cuda::detail::chainScratchSize(count), cuda::noInit);
|
||||
cuda::detail::parseGridChain(d_data, mBuffer.size(), count, scratch, mMetaData, cudaStream_t(0));
|
||||
}
|
||||
}
|
||||
}// GridHandle(T&& buffer)
|
||||
|
||||
// Dummy function that ensures instantiation of the move-constructor above when BufferT=cuda::DeviceBuffer
|
||||
namespace {auto __dummy(){return GridHandle<cuda::DeviceBuffer>(std::move(cuda::DeviceBuffer()));}}
|
||||
// move constructor from a single-space device buffer: all device work runs on
|
||||
// the buffer's retained stream (or the default stream for a synchronous
|
||||
// resource), and the metadata scratch allocates through the buffer's resource.
|
||||
template<typename BufferT>
|
||||
template<typename T, typename util::enable_if<BufferHasDeviceSingle<T>::value, int>::type, typename>
|
||||
GridHandle<BufferT>::GridHandle(T&& buffer)
|
||||
: mBuffer(std::move(buffer))
|
||||
{
|
||||
static_assert(util::is_same<T,BufferT>::value, "Expected U==BufferT");
|
||||
using ResourceT = typename BufferT::ResourceType;
|
||||
if (const GridData *d_data = reinterpret_cast<const GridData*>(mBuffer.data())) {
|
||||
const cudaStream_t stream = cuda::detail::retainedStreamOrDefault(mBuffer);
|
||||
const uint32_t count = cuda::detail::validGridChainHead(d_data, mBuffer.size_bytes(), stream);
|
||||
using ScratchT = cuda::Buffer<std::byte, ResourceT>;
|
||||
ScratchT scratch = cuda::detail::makeMetaScratch<ScratchT>(mBuffer, cuda::detail::chainScratchSize(count), stream);
|
||||
cuda::detail::parseGridChain(d_data, mBuffer.size_bytes(), count, scratch, mMetaData, stream);
|
||||
}
|
||||
}// GridHandle(T&& buffer) for single-space device buffers
|
||||
|
||||
// Emit the dual-buffer move constructor from every CUDA translation unit, so
|
||||
// host-only translation units that construct GridHandle<cuda::DeviceBuffer>
|
||||
// (they see only the declaration) always find the symbol at link time. An
|
||||
// unused private function is not enough: the optimizer may drop the
|
||||
// complete-object constructor it instantiates.
|
||||
template GridHandle<cuda::DeviceBuffer>::GridHandle(cuda::DeviceBuffer&&);
|
||||
|
||||
} // namespace nanovdb
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#define NANOVDB_CUDA_NODE_MANAGER_CUH_HAS_BEEN_INCLUDED
|
||||
|
||||
#include <nanovdb/util/cuda/Util.h>// for cuda::lambdaKernel
|
||||
#include <nanovdb/cuda/Buffer.h>// for the resource-aware buffers below
|
||||
#include <nanovdb/cuda/DeviceBuffer.h>
|
||||
#include <nanovdb/NodeManager.h>
|
||||
|
||||
@@ -25,6 +26,23 @@ namespace nanovdb {
|
||||
|
||||
namespace cuda {
|
||||
|
||||
namespace detail {
|
||||
|
||||
/// @brief Constructs a single-space buffer of @c count elements through
|
||||
/// @c ref, on @c stream when the resource is stream-ordered.
|
||||
template<typename BufferT, typename RefT>
|
||||
inline BufferT makeNodeManagerStorage(cudaStream_t stream, const RefT& ref, uint64_t count)
|
||||
{
|
||||
if constexpr (is_async_resource<typename BufferT::ResourceType>::value) {
|
||||
return BufferT(stream, ref, count, noInit);
|
||||
} else {
|
||||
(void)stream;
|
||||
return BufferT(ref, count, noInit);
|
||||
}
|
||||
}
|
||||
|
||||
}// namespace detail
|
||||
|
||||
/// @brief Construct a NodeManager from a device grid pointer
|
||||
///
|
||||
/// @param d_grid device grid pointer whose nodes will be accessed sequentially
|
||||
@@ -41,8 +59,8 @@ createNodeManager(const NanoGrid<BuildT> *d_grid,
|
||||
cudaCheck(cudaGetDevice(&device));
|
||||
auto buffer = BufferT::create(sizeof(NodeManagerData), &pool, device, stream);
|
||||
auto *d_data = (NodeManagerData*)buffer.deviceData();
|
||||
size_t size = 0u, *d_size;
|
||||
cudaCheck(util::cuda::mallocAsync((void**)&d_size, sizeof(size_t), stream));
|
||||
Buffer<size_t, DeviceResource> sizeBuf(stream, 1, noInit);
|
||||
size_t size = 0u, *d_size = sizeBuf.data();
|
||||
util::cuda::lambdaKernel<<<1, 1, 0, stream>>>(1, [=] __device__(size_t) {
|
||||
*d_data = NodeManagerData((void*)d_grid);
|
||||
*d_size = sizeof(NodeManagerData);
|
||||
@@ -58,7 +76,7 @@ createNodeManager(const NanoGrid<BuildT> *d_grid,
|
||||
});
|
||||
cudaCheckError();
|
||||
cudaCheck(cudaMemcpyAsync(&size, d_size, sizeof(size_t), cudaMemcpyDeviceToHost, stream));
|
||||
cudaCheck(util::cuda::freeAsync(d_size, stream));
|
||||
cudaCheck(cudaStreamSynchronize(stream));// the branch below reads the copied size on the host
|
||||
if (size > sizeof(NodeManagerData)) {
|
||||
auto tmp = BufferT::create(size, &pool, device, stream);// only allocate buffer on the device
|
||||
cudaCheck(cudaMemcpyAsync(tmp.deviceData(), buffer.deviceData(), sizeof(NodeManagerData), cudaMemcpyDeviceToDevice, stream));
|
||||
@@ -85,6 +103,76 @@ createNodeManager(const NanoGrid<BuildT> *d_grid,
|
||||
return NodeManagerHandle<BufferT>(toGridType<BuildT>(), std::move(buffer));
|
||||
}// cuda::createNodeManager
|
||||
|
||||
/// @brief Construct a NodeManager over a single-space device buffer whose
|
||||
/// storage allocates through @a resource, which the returned handle
|
||||
/// borrows (it must outlive the handle) -- the same ownership contract
|
||||
/// as the builders.
|
||||
///
|
||||
/// @param d_grid device grid pointer whose nodes will be accessed sequentially
|
||||
/// @param resource device-resident memory resource the handle's storage
|
||||
/// allocates through; a host-accessible resource (e.g. PinnedResource)
|
||||
/// is excluded, since the result would carry no device NodeManager
|
||||
/// @param stream cuda stream
|
||||
/// @return Handle over cuda::Buffer<std::byte, ResourceRef<ResourceT>> that
|
||||
/// contains a device NodeManager
|
||||
template <typename BuildT, typename ResourceT>
|
||||
inline typename util::enable_if<(is_resource<ResourceT>::value || is_async_resource<ResourceT>::value)
|
||||
&& !is_host_accessible_resource<ResourceT>::value,
|
||||
NodeManagerHandle<Buffer<std::byte, ResourceRef<ResourceT>>>>::type
|
||||
createNodeManager(const NanoGrid<BuildT> *d_grid,
|
||||
ResourceT& resource,
|
||||
cudaStream_t stream = 0)
|
||||
{
|
||||
using RefT = ResourceRef<ResourceT>;
|
||||
using BufferT = Buffer<std::byte, RefT>;
|
||||
RefT ref(resource);
|
||||
auto buffer = detail::makeNodeManagerStorage<BufferT>(stream, ref, sizeof(NodeManagerData));
|
||||
auto *d_data = reinterpret_cast<NodeManagerData*>(buffer.data());
|
||||
auto sizeBuf = detail::makeNodeManagerStorage<Buffer<size_t, RefT>>(stream, ref, 1);
|
||||
size_t size = 0u, *d_size = sizeBuf.data();
|
||||
util::cuda::lambdaKernel<<<1, 1, 0, stream>>>(1, [=] __device__(size_t) {
|
||||
*d_data = NodeManagerData((void*)d_grid);
|
||||
*d_size = sizeof(NodeManagerData);
|
||||
auto &tree = d_grid->tree();
|
||||
if (NodeManager<BuildT>::FIXED_SIZE && d_grid->isBreadthFirst()) {
|
||||
d_data->mLinear = uint8_t(1u);
|
||||
d_data->mOff[0] = util::PtrDiff(tree.template getFirstNode<0>(), d_grid);
|
||||
d_data->mOff[1] = util::PtrDiff(tree.template getFirstNode<1>(), d_grid);
|
||||
d_data->mOff[2] = util::PtrDiff(tree.template getFirstNode<2>(), d_grid);
|
||||
} else {
|
||||
*d_size += sizeof(uint64_t)*tree.totalNodeCount();
|
||||
}
|
||||
});
|
||||
cudaCheckError();
|
||||
cudaCheck(cudaMemcpyAsync(&size, d_size, sizeof(size_t), cudaMemcpyDeviceToHost, stream));
|
||||
cudaCheck(cudaStreamSynchronize(stream));// the branch below reads the copied size on the host
|
||||
if (size > sizeof(NodeManagerData)) {
|
||||
auto tmp = detail::makeNodeManagerStorage<BufferT>(stream, ref, size);
|
||||
cudaCheck(cudaMemcpyAsync(tmp.data(), buffer.data(), sizeof(NodeManagerData), cudaMemcpyDeviceToDevice, stream));
|
||||
buffer = std::move(tmp);
|
||||
d_data = reinterpret_cast<NodeManagerData*>(buffer.data());
|
||||
util::cuda::lambdaKernel<<<1, 1, 0, stream>>>(1, [=] __device__ (size_t) {
|
||||
auto &tree = d_grid->tree();
|
||||
int64_t *ptr0 = d_data->mPtr[0] = reinterpret_cast<int64_t*>(d_data + 1);
|
||||
int64_t *ptr1 = d_data->mPtr[1] = d_data->mPtr[0] + tree.nodeCount(0);
|
||||
int64_t *ptr2 = d_data->mPtr[2] = d_data->mPtr[1] + tree.nodeCount(1);
|
||||
// Performs depth first traversal but breadth first insertion
|
||||
for (auto it2 = tree.root().cbeginChild(); it2; ++it2) {
|
||||
*ptr2++ = util::PtrDiff(&*it2, d_grid);
|
||||
for (auto it1 = it2->cbeginChild(); it1; ++it1) {
|
||||
*ptr1++ = util::PtrDiff(&*it1, d_grid);
|
||||
for (auto it0 = it1->cbeginChild(); it0; ++it0) {
|
||||
*ptr0++ = util::PtrDiff(&*it0, d_grid);
|
||||
}// loop over child nodes of the lower internal node
|
||||
}// loop over child nodes of the upper internal node
|
||||
}// loop over child nodes of the root node
|
||||
});
|
||||
cudaCheckError();
|
||||
}
|
||||
|
||||
return NodeManagerHandle<BufferT>(toGridType<BuildT>(), std::move(buffer));
|
||||
}// cuda::createNodeManager (single-space)
|
||||
|
||||
}// namespace cuda
|
||||
|
||||
template <typename BuildT, typename BufferT = cuda::DeviceBuffer>
|
||||
|
||||
@@ -30,6 +30,11 @@ public:
|
||||
// alignment is always satisfied; 256 is the nominal default advertised here.
|
||||
static constexpr size_t DEFAULT_ALIGNMENT = 256;
|
||||
|
||||
/// @brief Pinned allocations are mapped into the host address space, so
|
||||
/// containers over this resource remain host-readable (detected by
|
||||
/// nanovdb::cuda::is_host_accessible_resource).
|
||||
static constexpr bool HOST_ACCESSIBLE = true;
|
||||
|
||||
/// @brief Synchronous allocation of page-locked host memory.
|
||||
/// @param bytes number of bytes to allocate
|
||||
/// @param alignment requested alignment (ignored; cudaMallocHost is page-aligned)
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#ifndef NANOVDB_CUDA_TEMPPOOL_H_HAS_BEEN_INCLUDED
|
||||
#define NANOVDB_CUDA_TEMPPOOL_H_HAS_BEEN_INCLUDED
|
||||
|
||||
#include <nanovdb/cuda/Buffer.h>
|
||||
#include <nanovdb/cuda/DeviceResource.h>
|
||||
|
||||
#include <cstddef>
|
||||
@@ -21,31 +22,33 @@ namespace cuda {
|
||||
|
||||
template <class Resource>
|
||||
class TempPool {
|
||||
static_assert(is_async_resource<Resource>::value,
|
||||
"TempPool allocates stream-ordered scratch and requires an AsyncResource");
|
||||
// The buffer borrows the pool's resource through a ResourceRef rather than
|
||||
// copying it, preserving the pool's contract that all traffic reaches the
|
||||
// caller's resource instance (which may be stateful).
|
||||
using BufferT = Buffer<std::byte, ResourceRef<Resource>>;
|
||||
public:
|
||||
|
||||
/// @brief Default c-tor of an empty memory pool that uses the default
|
||||
/// instance of @c Resource for all allocations.
|
||||
TempPool() : mResource(&default_resource<Resource>()), mData(nullptr), mSize(0), mRequestedSize(0), mStream(nullptr) {}
|
||||
TempPool() : TempPool(default_resource<Resource>()) {}
|
||||
|
||||
/// @brief C-tor of an empty memory pool that routes all allocations through
|
||||
/// the supplied @c Resource instance.
|
||||
/// @param resource resource instance to allocate from; must outlive this pool.
|
||||
explicit TempPool(Resource& resource) : mResource(&resource), mData(nullptr), mSize(0), mRequestedSize(0), mStream(nullptr) {}
|
||||
|
||||
/// @brief Destructor. Frees the managed memory on the stream of the most
|
||||
/// recent reallocate(), so the stream-ordered free is ordered after
|
||||
/// the work that used the memory (rather than on the null stream).
|
||||
~TempPool() {
|
||||
mRequestedSize = 0;
|
||||
mResource->deallocate_async(mData, mSize, Resource::DEFAULT_ALIGNMENT, mStream);
|
||||
mData = nullptr;
|
||||
mSize = 0;
|
||||
explicit TempPool(Resource& resource)
|
||||
: mResource(&resource)
|
||||
, mBuffer(cudaStream_t{0}, ResourceRef<Resource>(resource), 0, noInit)
|
||||
{
|
||||
}
|
||||
|
||||
/// @brief Returns a non-const void pointer to the data managed by this instance.
|
||||
void* data() {return mData;}
|
||||
void* data() {return mBuffer.data();}
|
||||
|
||||
/// @brief Returns a non-const reference to the actual size of the data managed by this instance.
|
||||
/// @note Returned by reference because cub's two-pass API takes the storage
|
||||
/// size as a size_t&, so this cannot forward Buffer::size() by value.
|
||||
size_t& size() {return mSize;}
|
||||
|
||||
/// @brief Returns a non-const reference to the requested size of the data managed by this instance.
|
||||
@@ -54,25 +57,27 @@ public:
|
||||
|
||||
/// @brief Returns the stream that the managed memory was last (re)allocated on,
|
||||
/// i.e. the stream this pool will free on at destruction.
|
||||
cudaStream_t stream() const {return mStream;}
|
||||
cudaStream_t stream() const {return mBuffer.stream();}
|
||||
|
||||
/// @brief Re-allocation of the data managed by this instance. Only has affect if the pool in empty or
|
||||
/// the requested memory is larger than the existing size.
|
||||
/// @param stream cuda stream used for asynchronous de-allocation and allocation.
|
||||
/// @note Scratch is discarded, never resized: preserving a prefix of
|
||||
/// temporary storage would be a wasted copy.
|
||||
void reallocate(cudaStream_t stream) {
|
||||
if (!mData || mRequestedSize > mSize) {
|
||||
mResource->deallocate_async(mData, mSize, Resource::DEFAULT_ALIGNMENT, stream);
|
||||
mData = mResource->allocate_async(mRequestedSize, Resource::DEFAULT_ALIGNMENT, stream);
|
||||
mSize = mRequestedSize;
|
||||
if (mBuffer.empty() || mRequestedSize > mSize) {
|
||||
mBuffer.destroy(stream);// free the outgrown block on this stream
|
||||
mBuffer = BufferT(stream, ResourceRef<Resource>(*mResource), mRequestedSize, noInit);
|
||||
mSize = mBuffer.size();
|
||||
} else {
|
||||
mBuffer.set_stream(stream);// retained so the d-tor frees on the most-recently-used stream
|
||||
}
|
||||
mStream = stream;// retained so the destructor frees on the most-recently-used stream
|
||||
}
|
||||
private:
|
||||
Resource *mResource;
|
||||
void *mData;
|
||||
size_t mSize;
|
||||
size_t mRequestedSize;
|
||||
cudaStream_t mStream;
|
||||
Resource *mResource;// non-owning; must outlive this pool and its buffer
|
||||
BufferT mBuffer;
|
||||
size_t mSize{0};
|
||||
size_t mRequestedSize{0};
|
||||
};// TempPool<Resource> class
|
||||
|
||||
using TempDevicePool = TempPool<DeviceResource>;
|
||||
|
||||
@@ -117,8 +117,28 @@ nanovdb_example(NAME "ex_coarsen_nanovdb_cuda" OPENVDB)
|
||||
nanovdb_example(NAME "ex_mesh_to_grid_cuda" OPENVDB)
|
||||
|
||||
if(CUDAToolkit_FOUND)
|
||||
nanovdb_example(NAME "ex_make_mgpu_nanovdb") # requires cuRAND
|
||||
target_link_libraries(ex_make_mgpu_nanovdb PRIVATE CUDA::curand)
|
||||
# ex_make_mgpu_nanovdb uses TF32 WMMA which requires SM_80+ (Ampere or newer).
|
||||
# Gate on CMAKE_CUDA_ARCHITECTURES (what the build actually targets) rather than
|
||||
# probing the configure-time machine's GPU: the two can differ (cross-compiling,
|
||||
# building on one machine to run on another, CI runners without a GPU), and
|
||||
# probing via "bash -c nvidia-smi ..." isn't portable to Windows either way.
|
||||
set(_nanovdb_has_sm80 OFF)
|
||||
foreach(_cuda_arch IN LISTS CMAKE_CUDA_ARCHITECTURES)
|
||||
string(REGEX MATCH "^[0-9]+" _cuda_arch_number "${_cuda_arch}")
|
||||
if(_cuda_arch_number GREATER_EQUAL 80)
|
||||
set(_nanovdb_has_sm80 ON)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if(_nanovdb_has_sm80)
|
||||
nanovdb_example(NAME "ex_make_mgpu_nanovdb") # requires cuRAND and SM_80+ (TF32 WMMA)
|
||||
target_link_libraries(ex_make_mgpu_nanovdb PRIVATE CUDA::curand)
|
||||
else()
|
||||
message(STATUS "Skipping ex_make_mgpu_nanovdb: requires CUDA architecture 80 or newer (CMAKE_CUDA_ARCHITECTURES=${CMAKE_CUDA_ARCHITECTURES})")
|
||||
endif()
|
||||
unset(_nanovdb_has_sm80)
|
||||
unset(_cuda_arch_number)
|
||||
unset(_cuda_arch)
|
||||
endif()
|
||||
|
||||
if(NANOVDB_USE_MAGICAVOXEL)
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifndef _USE_MATH_DEFINES
|
||||
#define _USE_MATH_DEFINES
|
||||
#endif
|
||||
#include <cmath>
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#ifndef _USE_MATH_DEFINES
|
||||
#define _USE_MATH_DEFINES
|
||||
#endif
|
||||
#include <cmath>
|
||||
#include <chrono>
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
#if defined(NANOVDB_USE_OPENVDB)
|
||||
|
||||
#ifndef _USE_MATH_DEFINES
|
||||
#define _USE_MATH_DEFINES
|
||||
#endif
|
||||
#include <cmath>
|
||||
#include <chrono>
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ extern "C" void launch_kernels(const nanovdb::NodeManager<float>* deviceMgr,
|
||||
|
||||
// Simple wrapper that makes sure nanovdb::cuda::createNodeManager is initiated
|
||||
extern "C" void cudaCreateNodeManager(const nanovdb::NanoGrid<float> *d_grid,
|
||||
nanovdb::NodeManagerHandle<nanovdb::CudaDeviceBuffer> *handle)
|
||||
nanovdb::NodeManagerHandle<nanovdb::cuda::DeviceBuffer> *handle)
|
||||
{
|
||||
*handle = std::move(nanovdb::cuda::createNodeManager<float>(d_grid));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifndef _USE_MATH_DEFINES
|
||||
#define _USE_MATH_DEFINES
|
||||
#endif
|
||||
#include <cmath>
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#ifndef _USE_MATH_DEFINES
|
||||
#define _USE_MATH_DEFINES
|
||||
#endif
|
||||
#include <cmath>
|
||||
#include <chrono>
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
#if defined(NANOVDB_USE_OPENVDB)
|
||||
|
||||
#ifndef _USE_MATH_DEFINES
|
||||
#define _USE_MATH_DEFINES
|
||||
#endif
|
||||
#include <cmath>
|
||||
#include <chrono>
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifndef _USE_MATH_DEFINES
|
||||
#define _USE_MATH_DEFINES
|
||||
#endif
|
||||
#include <cmath>
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#ifndef _USE_MATH_DEFINES
|
||||
#define _USE_MATH_DEFINES
|
||||
#endif
|
||||
#include <cmath>
|
||||
#include <chrono>
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifndef _USE_MATH_DEFINES
|
||||
#define _USE_MATH_DEFINES
|
||||
#endif
|
||||
#include <cmath>
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
@@ -44,11 +46,41 @@ inline __hostdev__ void mortonEncode(uint32_t& code, uint32_t x, uint32_t y)
|
||||
template<typename RenderFn, typename GridT>
|
||||
inline float renderImage(bool useCuda, const RenderFn renderOp, int width, int height, float* image, const GridT* grid)
|
||||
{
|
||||
#if defined(__CUDACC__)
|
||||
// Kernel-only timing on the GPU path. cudaEventElapsedTime measures only
|
||||
// the work between the two record points on the default stream, so it
|
||||
// excludes host-side launch latency and the cudaDeviceSynchronize wakeup
|
||||
// that std::chrono would otherwise pick up. Events are created/destroyed
|
||||
// per call; the ~10us creation cost is dwarfed by the kernel itself.
|
||||
if (useCuda) {
|
||||
cudaEvent_t startEv, stopEv;
|
||||
NANOVDB_CUDA_CHECK_ERROR(cudaEventCreate(&startEv), __FILE__, __LINE__);
|
||||
NANOVDB_CUDA_CHECK_ERROR(cudaEventCreate(&stopEv), __FILE__, __LINE__);
|
||||
|
||||
NANOVDB_CUDA_CHECK_ERROR(cudaEventRecord(startEv, 0), __FILE__, __LINE__);
|
||||
|
||||
computeForEach(
|
||||
true, width * height, 256, __FILE__, __LINE__, [renderOp, image, grid] __hostdev__(int start, int end) {
|
||||
renderOp(start, end, image, grid);
|
||||
});
|
||||
|
||||
NANOVDB_CUDA_CHECK_ERROR(cudaEventRecord(stopEv, 0), __FILE__, __LINE__);
|
||||
NANOVDB_CUDA_CHECK_ERROR(cudaEventSynchronize(stopEv), __FILE__, __LINE__);
|
||||
|
||||
float ms = 0.f;
|
||||
NANOVDB_CUDA_CHECK_ERROR(cudaEventElapsedTime(&ms, startEv, stopEv), __FILE__, __LINE__);
|
||||
|
||||
NANOVDB_CUDA_CHECK_ERROR(cudaEventDestroy(startEv), __FILE__, __LINE__);
|
||||
NANOVDB_CUDA_CHECK_ERROR(cudaEventDestroy(stopEv), __FILE__, __LINE__);
|
||||
return ms;
|
||||
}
|
||||
#endif
|
||||
|
||||
using ClockT = std::chrono::high_resolution_clock;
|
||||
auto t0 = ClockT::now();
|
||||
|
||||
computeForEach(
|
||||
useCuda, width * height, 512, __FILE__, __LINE__, [renderOp, image, grid] __hostdev__(int start, int end) {
|
||||
useCuda, width * height, 256, __FILE__, __LINE__, [renderOp, image, grid] __hostdev__(int start, int end) {
|
||||
renderOp(start, end, image, grid);
|
||||
});
|
||||
computeSync(useCuda, __FILE__, __LINE__);
|
||||
|
||||
@@ -33,7 +33,7 @@ int main(int ac, char** av)
|
||||
throw std::runtime_error("Grid must be a level set");
|
||||
}
|
||||
|
||||
const int numIterations = 50;
|
||||
const int numIterations = 200;
|
||||
|
||||
const int width = 1024;
|
||||
const int height = 1024;
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#ifndef _USE_MATH_DEFINES
|
||||
#define _USE_MATH_DEFINES
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <chrono>
|
||||
#include <vector>
|
||||
|
||||
#if defined(NANOVDB_USE_CUDA)
|
||||
#include <nanovdb/cuda/DeviceBuffer.h>
|
||||
@@ -18,6 +22,34 @@ using BufferT = nanovdb::HostBuffer;
|
||||
|
||||
#include "common.h"
|
||||
|
||||
// Number of untimed warmup iterations to run before the timed loop. These
|
||||
// pay the kernel cold-start, lazy module load and GPU clock ramp-up costs
|
||||
// that would otherwise contaminate the first measured iteration.
|
||||
static constexpr int NUM_WARMUP_ITERATIONS = 2;
|
||||
|
||||
// Print min / median / mean / max instead of just mean. A single outlier
|
||||
// (context switch, OS jitter, thermal blip) pulls the mean noticeably but
|
||||
// leaves the median alone, so the median is the more trustworthy stat.
|
||||
static void
|
||||
reportStats(const char *label, const std::vector<float> &samples)
|
||||
{
|
||||
if (samples.empty()) return;
|
||||
std::vector<float> sorted(samples); // copy so we don't reorder the caller's data
|
||||
std::sort(sorted.begin(), sorted.end());
|
||||
const float minMs = sorted.front();
|
||||
const float maxMs = sorted.back();
|
||||
const float medianMs = sorted[sorted.size() / 2];
|
||||
float sum = 0;
|
||||
for (float s : sorted) sum += s;
|
||||
const float meanMs = sum / float(sorted.size());
|
||||
std::cout << label
|
||||
<< " min=" << minMs << " ms"
|
||||
<< " median=" << medianMs << " ms"
|
||||
<< " mean=" << meanMs << " ms"
|
||||
<< " max=" << maxMs << " ms"
|
||||
<< " (n=" << sorted.size() << ")" << std::endl;
|
||||
}
|
||||
|
||||
void runNanoVDB(nanovdb::GridHandle<BufferT>& handle, int numIterations, int width, int height, BufferT& imageBuffer)
|
||||
{
|
||||
using GridT = nanovdb::FloatGrid;
|
||||
@@ -69,14 +101,16 @@ void runNanoVDB(nanovdb::GridHandle<BufferT>& handle, int numIterations, int wid
|
||||
};
|
||||
|
||||
{
|
||||
float durationAvg = 0;
|
||||
for (int i = 0; i < numIterations; ++i) {
|
||||
float duration = renderImage(false, renderOp, width, height, h_outImage, h_grid);
|
||||
//std::cout << "Duration(NanoVDB-Host) = " << duration << " ms" << std::endl;
|
||||
durationAvg += duration;
|
||||
for (int i = 0; i < NUM_WARMUP_ITERATIONS; ++i) {
|
||||
(void)renderImage(false, renderOp, width, height, h_outImage, h_grid);
|
||||
}
|
||||
durationAvg /= numIterations;
|
||||
std::cout << "Average Duration(NanoVDB-Host) = " << durationAvg << " ms" << std::endl;
|
||||
|
||||
std::vector<float> samples;
|
||||
samples.reserve(numIterations);
|
||||
for (int i = 0; i < numIterations; ++i) {
|
||||
samples.push_back(renderImage(false, renderOp, width, height, h_outImage, h_grid));
|
||||
}
|
||||
reportStats("Duration(NanoVDB-Host):", samples);
|
||||
|
||||
saveImage("raytrace_level_set-nanovdb-host.pfm", width, height, (float*)imageBuffer.data());
|
||||
}
|
||||
@@ -92,14 +126,16 @@ void runNanoVDB(nanovdb::GridHandle<BufferT>& handle, int numIterations, int wid
|
||||
float* d_outImage = reinterpret_cast<float*>(imageBuffer.deviceData());
|
||||
|
||||
{
|
||||
float durationAvg = 0;
|
||||
for (int i = 0; i < numIterations; ++i) {
|
||||
float duration = renderImage(true, renderOp, width, height, d_outImage, d_grid);
|
||||
//std::cout << "Duration(NanoVDB-Cuda) = " << duration << " ms" << std::endl;
|
||||
durationAvg += duration;
|
||||
for (int i = 0; i < NUM_WARMUP_ITERATIONS; ++i) {
|
||||
(void)renderImage(true, renderOp, width, height, d_outImage, d_grid);
|
||||
}
|
||||
durationAvg /= numIterations;
|
||||
std::cout << "Average Duration(NanoVDB-Cuda) = " << durationAvg << " ms" << std::endl;
|
||||
|
||||
std::vector<float> samples;
|
||||
samples.reserve(numIterations);
|
||||
for (int i = 0; i < numIterations; ++i) {
|
||||
samples.push_back(renderImage(true, renderOp, width, height, d_outImage, d_grid));
|
||||
}
|
||||
reportStats("Duration(NanoVDB-Cuda):", samples);
|
||||
|
||||
imageBuffer.deviceDownload();
|
||||
saveImage("raytrace_level_set-nanovdb-cuda.pfm", width, height, (float*)imageBuffer.data());
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
#if defined(NANOVDB_USE_OPENVDB)
|
||||
|
||||
#ifndef _USE_MATH_DEFINES
|
||||
#define _USE_MATH_DEFINES
|
||||
#endif
|
||||
#include <cmath>
|
||||
#include <chrono>
|
||||
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ int main(int, char**)
|
||||
{
|
||||
try {
|
||||
// returns a GridHandle using CUDA for memory management.
|
||||
auto handle = nanovdb::io::readGrid<nanovdb::CudaDeviceBuffer>("data/sphere.nvdb");
|
||||
auto handle = nanovdb::io::readGrid<nanovdb::cuda::DeviceBuffer>("data/sphere.nvdb");
|
||||
|
||||
cudaStream_t stream; // Create a CUDA stream to allow for asynchronous copy of pinned CUDA memory.
|
||||
cudaStreamCreate(&stream);
|
||||
|
||||
@@ -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
|
||||
// .toNanoVDB() 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 toNanoVDB() "
|
||||
"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("toNanoVDB",
|
||||
[](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 .toNanoVDB() 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,9 @@
|
||||
#include <nanovdb/tools/CreateNanoGrid.h>
|
||||
#include <nanovdb/tools/GridBuilder.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace nb = nanobind;
|
||||
using namespace nb::literals;
|
||||
using namespace nanovdb;
|
||||
@@ -35,20 +38,561 @@ 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));
|
||||
}
|
||||
// Invalid (not None) object signals "SrcBuildT didn't match"; the caller
|
||||
// tests is_valid() and falls through to the next SrcBuildT.
|
||||
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));
|
||||
}
|
||||
// Invalid (not None) object signals "SrcBuildT didn't match"; the caller
|
||||
// tests is_valid() and falls through to the next SrcBuildT.
|
||||
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));
|
||||
}
|
||||
// Invalid (not None) object signals "SrcBuildT didn't match"; the caller
|
||||
// tests is_valid() and falls through to the next SrcBuildT.
|
||||
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));
|
||||
// Invalid (not None) object signals "SrcBuildT didn't match"; the caller
|
||||
// tests is_valid() and falls through to the next SrcBuildT.
|
||||
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,189 @@
|
||||
#include <nanobind/stl/string.h>
|
||||
|
||||
#include <nanovdb/GridHandle.h>
|
||||
#include <nanovdb/NanoVDB.h>
|
||||
|
||||
#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: collect non-owning pointers to each handle and forward to
|
||||
// the borrowing nanovdb::mergeGrids overload. The vector-of-handles
|
||||
// overload is unusable from a Python list because GridHandle is move-only
|
||||
// — building the vector would move from (and silently empty) the caller's
|
||||
// Python objects.
|
||||
m.def("mergeGrids", [](nb::sequence handles) {
|
||||
std::vector<const HandleT*> sources;
|
||||
sources.reserve(nb::len(handles));
|
||||
for (nb::handle item : handles) {
|
||||
sources.push_back(&nb::cast<const HandleT&>(item));
|
||||
}
|
||||
return nanovdb::mergeGrids<BufferT>(sources.data(), sources.size());
|
||||
}, 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 `.toNanoVDB()` 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.toNanoVDB(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.toNanoVDB()
|
||||
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.toNanoVDB())
|
||||
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
|
||||
@@ -142,7 +142,16 @@ public:
|
||||
|
||||
~VoxelBlockManagerHandle() { this->reset(); }
|
||||
/// @brief clear the buffer
|
||||
void reset() { mFirstLeafID.clear(); mJumpMap.clear(); mBlockCount = 0; }
|
||||
void reset() {
|
||||
if constexpr (BufferHasDestroy<BufferT>::value) {
|
||||
mFirstLeafID.destroy();
|
||||
mJumpMap.destroy();
|
||||
} else {
|
||||
mFirstLeafID.clear();
|
||||
mJumpMap.clear();
|
||||
}
|
||||
mBlockCount = 0;
|
||||
}
|
||||
|
||||
/// @brief Returns a non-const pointer to the firstLeafID device-hosted data
|
||||
///
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#define NANOVDB_TOOLS_CUDA_ADDBLINDDATA_CUH_HAS_BEEN_INCLUDED
|
||||
|
||||
#include <nanovdb/NanoVDB.h>
|
||||
#include <nanovdb/cuda/Buffer.h>
|
||||
#include <nanovdb/cuda/DeviceBuffer.h>
|
||||
#include <nanovdb/GridHandle.h>
|
||||
#include <nanovdb/util/cuda/Util.h>
|
||||
@@ -34,6 +35,7 @@ namespace tools::cuda {// ============================================
|
||||
/// @tparam BuildT Build type of the grid
|
||||
/// @tparam BlindDataT Type of the blind data
|
||||
/// @tparam BufferT Type of the buffer used for allocation
|
||||
/// @tparam ResourceT Template type of optional resource used for internal temporary memory
|
||||
/// @param d_grid Pointer to device grid
|
||||
/// @param d_blindData Pointer to device blind data
|
||||
/// @param valueCount number of values in the blind data
|
||||
@@ -43,7 +45,7 @@ namespace tools::cuda {// ============================================
|
||||
/// @param pool optional pool used for allocation
|
||||
/// @param stream optional CUDA stream (defaults to CUDA stream 0)
|
||||
/// @return GridHandle with blind data appended
|
||||
template<typename BuildT, typename BlindDataT, typename BufferT = nanovdb::cuda::DeviceBuffer>
|
||||
template<typename BuildT, typename BlindDataT, typename BufferT = nanovdb::cuda::DeviceBuffer, typename ResourceT = nanovdb::cuda::DeviceResource>
|
||||
GridHandle<BufferT>
|
||||
addBlindData(const NanoGrid<BuildT> *d_grid,
|
||||
const BlindDataT *d_blindData,
|
||||
@@ -60,11 +62,16 @@ addBlindData(const NanoGrid<BuildT> *d_grid,
|
||||
// old grid old meta new meta old data new data
|
||||
|
||||
static_assert(BufferTraits<BufferT>::hasDeviceDual, "Expected BufferT to support device allocation");
|
||||
static_assert(nanovdb::cuda::is_async_resource<ResourceT>::value,
|
||||
"addBlindData allocates stream-ordered scratch and requires an AsyncResource");
|
||||
|
||||
// extract byte sizes of the grid, blind meta data and blind data
|
||||
enum {GRID=0, META=1, DATA=2, CHECKSUM=3};
|
||||
uint64_t tmp[4], *d_tmp;
|
||||
cudaCheck(util::cuda::mallocAsync((void**)&d_tmp, 4*sizeof(uint64_t), stream));
|
||||
using ScratchT = nanovdb::cuda::Buffer<uint64_t, nanovdb::cuda::ResourceRef<ResourceT>>;
|
||||
auto &resource = nanovdb::cuda::default_resource<ResourceT>();
|
||||
uint64_t tmp[4];
|
||||
ScratchT tmpBuf(stream, nanovdb::cuda::ResourceRef<ResourceT>(resource), 4, nanovdb::cuda::noInit);
|
||||
uint64_t *d_tmp = tmpBuf.data();
|
||||
util::cuda::lambdaKernel<<<1, 1, 0, stream>>>(1, [=] __device__(size_t) {
|
||||
if (auto count = d_grid->blindDataCount()) {
|
||||
d_tmp[GRID] = util::PtrDiff(&d_grid->blindMetaData(0), d_grid);
|
||||
@@ -110,13 +117,15 @@ addBlindData(const NanoGrid<BuildT> *d_grid,
|
||||
// increment grid size and blind data counter in output grid
|
||||
util::cuda::lambdaKernel<<<1, 1, 0, stream>>>(1, [=] __device__(size_t) {
|
||||
auto &grid = *reinterpret_cast<NanoGrid<BuildT>*>(d_data);
|
||||
grid.mGridIndex = 0u; // Possibly overwriting input; returned grid has batch size 1
|
||||
grid.mGridCount = 1u;
|
||||
grid.mBlindMetadataCount += 1;
|
||||
grid.mBlindMetadataOffset = d_tmp[GRID];
|
||||
auto *meta = util::PtrAdd<GridBlindMetaData>(d_data, grid.mBlindMetadataOffset);// points to first blind meta data
|
||||
for (uint32_t i=0, n=grid.mBlindMetadataCount-1; i<n; ++i, ++meta) meta->mDataOffset += sizeof(GridBlindMetaData);
|
||||
grid.mGridSize += sizeof(GridBlindMetaData) + meta->blindDataSize();// expansion with 32 byte alignment
|
||||
}); cudaCheckError();
|
||||
cudaCheck(util::cuda::freeAsync(d_tmp, stream));
|
||||
tmpBuf.destroy(stream);
|
||||
|
||||
Checksum cs(tmp[CHECKSUM]);
|
||||
cuda::updateChecksum(reinterpret_cast<GridData*>(d_data), cs.mode(), stream);
|
||||
|
||||
@@ -30,9 +30,12 @@ namespace nanovdb {
|
||||
|
||||
namespace tools::cuda {
|
||||
|
||||
template <typename BuildT>
|
||||
template <typename BuildT, typename ResourceT = nanovdb::cuda::DeviceResource>
|
||||
class CoarsenGrid
|
||||
{
|
||||
static_assert(nanovdb::cuda::is_async_resource<ResourceT>::value,
|
||||
"CoarsenGrid allocates stream-ordered scratch and requires an AsyncResource");
|
||||
|
||||
using GridT = NanoGrid<BuildT>;
|
||||
using TreeT = NanoTree<BuildT>;
|
||||
using RootT = NanoRoot<BuildT>;
|
||||
@@ -43,8 +46,11 @@ public:
|
||||
/// @brief Constructor
|
||||
/// @param deviceGrid source device grid to be coarsened
|
||||
/// @param stream optional CUDA stream (defaults to CUDA stream 0)
|
||||
CoarsenGrid(const GridT* d_srcGrid, cudaStream_t stream = 0)
|
||||
: mBuilder(stream), mStream(stream), mTimer(stream), mDeviceSrcGrid(d_srcGrid) {}
|
||||
/// @param resource resource instance all device scratch is allocated from;
|
||||
/// must outlive this operator (defaults to the per-type default resource)
|
||||
CoarsenGrid(const GridT* d_srcGrid, cudaStream_t stream = 0,
|
||||
ResourceT& resource = nanovdb::cuda::default_resource<ResourceT>())
|
||||
: mBuilder(stream, resource), mStream(stream), mTimer(stream), mDeviceSrcGrid(d_srcGrid) {}
|
||||
|
||||
/// @brief Toggle on and off verbose mode
|
||||
/// @param level Verbose level: 0=quiet, 1=timing, 2=benchmarking
|
||||
@@ -74,20 +80,20 @@ private:
|
||||
static constexpr unsigned int mNumThreads = 128;// for kernels spawned via lambdaKernel (others may specialize)
|
||||
static unsigned int numBlocks(unsigned int n) {return (n + mNumThreads - 1) / mNumThreads;}
|
||||
|
||||
TopologyBuilder<BuildT> mBuilder;
|
||||
TopologyBuilder<BuildT, ResourceT> mBuilder;
|
||||
cudaStream_t mStream{0};
|
||||
util::cuda::Timer mTimer;
|
||||
int mVerbose{0};
|
||||
const GridT *mDeviceSrcGrid;
|
||||
TreeData mSrcTreeData;
|
||||
};// tools::cuda::CoarsenGrid<BuildT>
|
||||
};// tools::cuda::CoarsenGrid<BuildT, ResourceT>
|
||||
|
||||
//-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
template<typename BuildT>
|
||||
template<typename BuildT, typename ResourceT>
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT>
|
||||
CoarsenGrid<BuildT>::getHandle(const BufferT &pool)
|
||||
CoarsenGrid<BuildT, ResourceT>::getHandle(const BufferT &pool)
|
||||
{
|
||||
// Copy TreeData from GPU -> CPU
|
||||
cudaStreamSynchronize(mStream);
|
||||
@@ -147,12 +153,12 @@ CoarsenGrid<BuildT>::getHandle(const BufferT &pool)
|
||||
cudaStreamSynchronize(mStream);
|
||||
|
||||
return GridHandle<BufferT>(std::move(buffer));
|
||||
}// CoarsenGrid<BuildT>::getHandle
|
||||
}// CoarsenGrid<BuildT, ResourceT>::getHandle
|
||||
|
||||
//-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
template<typename BuildT>
|
||||
void CoarsenGrid<BuildT>::coarsenRoot()
|
||||
template<typename BuildT, typename ResourceT>
|
||||
void CoarsenGrid<BuildT, ResourceT>::coarsenRoot()
|
||||
{
|
||||
// This method coarsens the root tiles, to accommodate for the overall downsamping operation.
|
||||
|
||||
@@ -197,12 +203,12 @@ void CoarsenGrid<BuildT>::coarsenRoot()
|
||||
for (const auto& [key, tile] : coarsenedTiles)
|
||||
*coarsenedRootPtr->tile(t++) = tile;
|
||||
mBuilder.mProcessedRoot.deviceUpload(device, mStream, false);
|
||||
}// CoarsenGrid<BuildT>::coarsenRoot
|
||||
}// CoarsenGrid<BuildT, ResourceT>::coarsenRoot
|
||||
|
||||
//-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
template<typename BuildT>
|
||||
void CoarsenGrid<BuildT>::coarsenInternalNodes()
|
||||
template<typename BuildT, typename ResourceT>
|
||||
void CoarsenGrid<BuildT, ResourceT>::coarsenInternalNodes()
|
||||
{
|
||||
// Computes the masks of upper and (densified) lower internal nodes, as a result of the coarsening operation
|
||||
// Masks of lower internal nodes are densified in the sense that a serialized array of them is allocated,
|
||||
@@ -210,26 +216,26 @@ void CoarsenGrid<BuildT>::coarsenInternalNodes()
|
||||
if (auto srcLeafCount = mSrcTreeData.mNodeCount[0]) { // Unless it's an empty grid
|
||||
util::cuda::lambdaKernel<<<numBlocks(srcLeafCount), mNumThreads, 0, mStream>>>(
|
||||
srcLeafCount, util::morphology::cuda::CoarsenInternalNodesFunctor<BuildT>(),
|
||||
mDeviceSrcGrid, mBuilder.deviceProcessedRoot(), mBuilder.mUpperMasks.deviceData(), mBuilder.mLowerMasks.deviceData() );
|
||||
mDeviceSrcGrid, mBuilder.deviceProcessedRoot(), mBuilder.mUpperMasks.data(), mBuilder.mLowerMasks.data() );
|
||||
}
|
||||
}// CoarsenGrid<BuildT>::coarsenInternalNodes
|
||||
}// CoarsenGrid<BuildT, ResourceT>::coarsenInternalNodes
|
||||
|
||||
//-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
template <typename BuildT>
|
||||
void CoarsenGrid<BuildT>::processGridTreeRoot()
|
||||
template <typename BuildT, typename ResourceT>
|
||||
void CoarsenGrid<BuildT, ResourceT>::processGridTreeRoot()
|
||||
{
|
||||
// Copy GridData from source grid
|
||||
// By convention: this will duplicate grid name and map. Others will be reset later
|
||||
cudaCheck(cudaMemcpyAsync(&mBuilder.data()->getGrid(), mDeviceSrcGrid->data(), GridT::memUsage(), cudaMemcpyDeviceToDevice, mStream));
|
||||
util::cuda::lambdaKernel<<<1, 1, 0, mStream>>>(1, topology::detail::BuildGridTreeRootFunctor<BuildT>(), mBuilder.deviceData());
|
||||
cudaCheckError();
|
||||
}// CoarsenGrid<BuildT>::processGridTreeRoot
|
||||
}// CoarsenGrid<BuildT, ResourceT>::processGridTreeRoot
|
||||
|
||||
//-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
template<typename BuildT>
|
||||
void CoarsenGrid<BuildT>::coarsenLeafNodes()
|
||||
template<typename BuildT, typename ResourceT>
|
||||
void CoarsenGrid<BuildT, ResourceT>::coarsenLeafNodes()
|
||||
{
|
||||
// Coarsens the active masks of the source grid (as indicated at the leaf level), into a new grid that
|
||||
// has been already topologically coarsened to include all necessary leaf nodes.
|
||||
@@ -241,7 +247,7 @@ void CoarsenGrid<BuildT>::coarsenLeafNodes()
|
||||
|
||||
// Update leaf offsets and prefix sums
|
||||
mBuilder.processLeafOffsets(mStream);
|
||||
}// CoarsenGrid<BuildT>::coarsenLeafNodes
|
||||
}// CoarsenGrid<BuildT, ResourceT>::coarsenLeafNodes
|
||||
|
||||
//-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -30,9 +30,12 @@ namespace nanovdb {
|
||||
|
||||
namespace tools::cuda {
|
||||
|
||||
template <typename BuildT>
|
||||
template <typename BuildT, typename ResourceT = nanovdb::cuda::DeviceResource>
|
||||
class DilateGrid
|
||||
{
|
||||
static_assert(nanovdb::cuda::is_async_resource<ResourceT>::value,
|
||||
"DilateGrid allocates stream-ordered scratch and requires an AsyncResource");
|
||||
|
||||
using GridT = NanoGrid<BuildT>;
|
||||
using TreeT = NanoTree<BuildT>;
|
||||
using RootT = NanoRoot<BuildT>;
|
||||
@@ -43,8 +46,11 @@ public:
|
||||
/// @brief Constructor
|
||||
/// @param deviceGrid source device grid to be dilated
|
||||
/// @param stream optional CUDA stream (defaults to CUDA stream 0)
|
||||
DilateGrid(const GridT* d_srcGrid, cudaStream_t stream = 0)
|
||||
: mBuilder(stream), mStream(stream), mTimer(stream), mDeviceSrcGrid(d_srcGrid) {}
|
||||
/// @param resource resource instance all device scratch is allocated from;
|
||||
/// must outlive this operator (defaults to the per-type default resource)
|
||||
DilateGrid(const GridT* d_srcGrid, cudaStream_t stream = 0,
|
||||
ResourceT& resource = nanovdb::cuda::default_resource<ResourceT>())
|
||||
: mBuilder(stream, resource), mStream(stream), mTimer(stream), mDeviceSrcGrid(d_srcGrid) {}
|
||||
|
||||
/// @brief Toggle on and off verbose mode
|
||||
/// @param level Verbose level: 0=quiet, 1=timing, 2=benchmarking
|
||||
@@ -78,21 +84,21 @@ private:
|
||||
static constexpr unsigned int mNumThreads = 128;// for kernels spawned via lambdaKernel (others may specialize)
|
||||
static unsigned int numBlocks(unsigned int n) {return (n + mNumThreads - 1) / mNumThreads;}
|
||||
|
||||
TopologyBuilder<BuildT> mBuilder;
|
||||
TopologyBuilder<BuildT, ResourceT> mBuilder;
|
||||
cudaStream_t mStream{0};
|
||||
util::cuda::Timer mTimer;
|
||||
int mVerbose{0};
|
||||
const GridT *mDeviceSrcGrid;
|
||||
morphology::NearestNeighbors mOp{morphology::NN_FACE_EDGE_VERTEX};
|
||||
TreeData mSrcTreeData;
|
||||
};// tools::cuda::DilateGrid<BuildT>
|
||||
};// tools::cuda::DilateGrid<BuildT, ResourceT>
|
||||
|
||||
//-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
template<typename BuildT>
|
||||
template<typename BuildT, typename ResourceT>
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT>
|
||||
DilateGrid<BuildT>::getHandle(const BufferT &pool)
|
||||
DilateGrid<BuildT, ResourceT>::getHandle(const BufferT &pool)
|
||||
{
|
||||
// Copy TreeData from GPU -> CPU
|
||||
cudaStreamSynchronize(mStream);
|
||||
@@ -152,12 +158,12 @@ DilateGrid<BuildT>::getHandle(const BufferT &pool)
|
||||
cudaStreamSynchronize(mStream);
|
||||
|
||||
return GridHandle<BufferT>(std::move(buffer));
|
||||
}// DilateGrid<BuildT>::getHandle
|
||||
}// DilateGrid<BuildT, ResourceT>::getHandle
|
||||
|
||||
//-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
template<typename BuildT>
|
||||
void DilateGrid<BuildT>::dilateRoot()
|
||||
template<typename BuildT, typename ResourceT>
|
||||
void DilateGrid<BuildT, ResourceT>::dilateRoot()
|
||||
{
|
||||
// This method conservatively and speculatively dilates the root tiles, to accommodate
|
||||
// any new root nodes that might be introduced by the dilation operation.
|
||||
@@ -220,12 +226,12 @@ void DilateGrid<BuildT>::dilateRoot()
|
||||
for (const auto& [key, tile] : dilatedTiles)
|
||||
*dilatedRootPtr->tile(t++) = tile;
|
||||
mBuilder.mProcessedRoot.deviceUpload(device, mStream, false);
|
||||
}// DilateGrid<BuildT>::dilateRoot
|
||||
}// DilateGrid<BuildT, ResourceT>::dilateRoot
|
||||
|
||||
//-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
template<typename BuildT>
|
||||
void DilateGrid<BuildT>::dilateInternalNodes()
|
||||
template<typename BuildT, typename ResourceT>
|
||||
void DilateGrid<BuildT, ResourceT>::dilateInternalNodes()
|
||||
{
|
||||
// Computes the masks of upper and (densified) lower internal nodes, as a result of the dilation operation
|
||||
// Masks of lower internal nodes are densified in the sense that a serialized array of them is allocated,
|
||||
@@ -247,24 +253,24 @@ void DilateGrid<BuildT>::dilateInternalNodes()
|
||||
<<<dim3(mSrcTreeData.mNodeCount[1],Op::SlicesPerLowerNode,1), Op::MaxThreadsPerBlock, 0, mStream>>>
|
||||
(mDeviceSrcGrid, mBuilder.deviceProcessedRoot(), mBuilder.deviceUpperMasks(), mBuilder.deviceLowerMasks()); }
|
||||
}
|
||||
}// DilateGrid<BuildT>::dilateInternalNodes
|
||||
}// DilateGrid<BuildT, ResourceT>::dilateInternalNodes
|
||||
|
||||
//-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
template <typename BuildT>
|
||||
void DilateGrid<BuildT>::processGridTreeRoot()
|
||||
template <typename BuildT, typename ResourceT>
|
||||
void DilateGrid<BuildT, ResourceT>::processGridTreeRoot()
|
||||
{
|
||||
// Copy GridData from source grid
|
||||
// By convention: this will duplicate grid name and map. Others will be reset later
|
||||
cudaCheck(cudaMemcpyAsync(&mBuilder.data()->getGrid(), mDeviceSrcGrid->data(), GridT::memUsage(), cudaMemcpyDeviceToDevice, mStream));
|
||||
util::cuda::lambdaKernel<<<1, 1, 0, mStream>>>(1, topology::detail::BuildGridTreeRootFunctor<BuildT>(), mBuilder.deviceData());
|
||||
cudaCheckError();
|
||||
}// DilateGrid<BuildT>::processGridTreeRoot
|
||||
}// DilateGrid<BuildT, ResourceT>::processGridTreeRoot
|
||||
|
||||
//-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
template<typename BuildT>
|
||||
void DilateGrid<BuildT>::dilateLeafNodes()
|
||||
template<typename BuildT, typename ResourceT>
|
||||
void DilateGrid<BuildT, ResourceT>::dilateLeafNodes()
|
||||
{
|
||||
// Dilates the active masks of the source grid (as indicated at the leaf level), into a new grid that
|
||||
// has been already topologically dilated to include all necessary leaf nodes.
|
||||
@@ -279,13 +285,13 @@ void DilateGrid<BuildT>::dilateLeafNodes()
|
||||
else if (mOp == morphology::NN_FACE_EDGE_VERTEX) {
|
||||
using Op = util::morphology::cuda::DilateLeafNodesFunctor<BuildT, morphology::NN_FACE_EDGE_VERTEX>;
|
||||
util::cuda::operatorKernel<Op>
|
||||
<<<dim3(mBuilder.data()->nodeCount[1],Op::SlicesPerLowerNode,1), Op::MaxThreadsPerBlock>>>
|
||||
<<<dim3(mBuilder.data()->nodeCount[1],Op::SlicesPerLowerNode,1), Op::MaxThreadsPerBlock, 0, mStream>>>
|
||||
(mDeviceSrcGrid, static_cast<GridT*>(mBuilder.data()->d_bufferPtr)); }
|
||||
}
|
||||
|
||||
// Update leaf offsets and prefix sums
|
||||
mBuilder.processLeafOffsets(mStream);
|
||||
}// DilateGrid<BuildT>::dilateLeafNodes
|
||||
}// DilateGrid<BuildT, ResourceT>::dilateLeafNodes
|
||||
|
||||
//-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
#include <nanovdb/cuda/UnifiedBuffer.h>
|
||||
#include <nanovdb/tools/cuda/PointsToGrid.cuh>
|
||||
#include <nanovdb/util/cuda/Util.h>
|
||||
#include <algorithm>
|
||||
|
||||
#include <cuda/cmath>
|
||||
|
||||
namespace nanovdb {
|
||||
|
||||
@@ -37,70 +40,85 @@ private:
|
||||
uint32_t* mNodeCounts;
|
||||
};
|
||||
|
||||
/// @brief Implements the merge path binary search algorithm in order to find the median across two sorted input key arrays
|
||||
/// @brief Find a partition at an arbitrary diagonal in the conceptual merge of two sorted input arrays.
|
||||
template<typename KeyIteratorIn>
|
||||
__device__
|
||||
void mergePath(KeyIteratorIn keys1, size_t keys1Count, KeyIteratorIn keys2, size_t keys2Count, ptrdiff_t* key1Intervals, ptrdiff_t* key2Intervals, int intervalIndex)
|
||||
void mergePath(KeyIteratorIn keys1, size_t keys1Count, KeyIteratorIn keys2, size_t keys2Count, ptrdiff_t* key1Intervals, ptrdiff_t* key2Intervals, size_t combinedIndex)
|
||||
{
|
||||
using key_type = typename ::cuda::std::iterator_traits<KeyIteratorIn>::value_type;
|
||||
|
||||
const size_t combinedIndex = intervalIndex * (keys1Count + keys2Count) / 2;
|
||||
size_t leftTop = combinedIndex > keys1Count ? keys1Count : combinedIndex;
|
||||
size_t rightTop = combinedIndex > keys1Count ? combinedIndex - keys1Count : 0;
|
||||
size_t leftBottom = rightTop;
|
||||
|
||||
key_type leftKey;
|
||||
key_type rightKey;
|
||||
while(true)
|
||||
{
|
||||
ptrdiff_t offset = (leftTop - leftBottom) / 2;
|
||||
ptrdiff_t leftMid = leftTop - offset;
|
||||
ptrdiff_t rightMid = rightTop + offset;
|
||||
|
||||
if (leftMid > keys1Count - 1 || rightMid < 1) {
|
||||
leftKey = 1;
|
||||
rightKey = 0;
|
||||
}
|
||||
else {
|
||||
leftKey = *(keys1 + leftMid);
|
||||
rightKey = *(keys2 + rightMid - 1);
|
||||
}
|
||||
|
||||
if (leftKey > rightKey) {
|
||||
if (rightMid > keys2Count - 1 || leftMid < 1) {
|
||||
leftKey = 0;
|
||||
rightKey = 1;
|
||||
}
|
||||
else {
|
||||
leftKey = *(keys1 + leftMid - 1);
|
||||
rightKey = *(keys2 + rightMid);
|
||||
}
|
||||
|
||||
if (leftKey <= rightKey) {
|
||||
*key1Intervals = leftMid;
|
||||
*key2Intervals = rightMid;
|
||||
break;
|
||||
}
|
||||
else {
|
||||
leftTop = leftMid - 1;
|
||||
rightTop = rightMid + 1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
leftBottom = leftMid + 1;
|
||||
size_t begin = combinedIndex > keys2Count ? combinedIndex - keys2Count : 0;
|
||||
size_t end = combinedIndex < keys1Count ? combinedIndex : keys1Count;
|
||||
while (begin < end) {
|
||||
const size_t key1Index = (begin + end) / 2;
|
||||
const size_t key2Index = combinedIndex - 1 - key1Index;
|
||||
if (!(keys2[key2Index] < keys1[key1Index])) {
|
||||
begin = key1Index + 1;
|
||||
} else {
|
||||
end = key1Index;
|
||||
}
|
||||
}
|
||||
|
||||
*key1Intervals = begin;
|
||||
*key2Intervals = combinedIndex - begin;
|
||||
}
|
||||
|
||||
namespace kernels {
|
||||
|
||||
/// @brief Kernel wrapper for the merge path algorithm
|
||||
/// @brief Kernel wrapper for the merge path algorithm.
|
||||
template<typename KeyIteratorIn>
|
||||
__global__
|
||||
void mergePathKernel(KeyIteratorIn keys1, size_t keys1Count, KeyIteratorIn keys2, size_t keys2Count, ptrdiff_t* key1Intervals, ptrdiff_t* key2Intervals, size_t intervalOffset)
|
||||
void mergePathKernel(KeyIteratorIn keys1, size_t keys1Count, KeyIteratorIn keys2, size_t keys2Count, ptrdiff_t* key1Intervals, ptrdiff_t* key2Intervals, size_t combinedIndex)
|
||||
{
|
||||
const unsigned int intervalIndex = threadIdx.x + blockIdx.x * blockDim.x + intervalOffset;
|
||||
mergePath(keys1, keys1Count, keys2, keys2Count, key1Intervals, key2Intervals, intervalIndex);
|
||||
mergePath(keys1, keys1Count, keys2, keys2Count, key1Intervals, key2Intervals, combinedIndex);
|
||||
}
|
||||
|
||||
/// @brief Snap each device boundary in stripeOffsets to the nearest edge of the run of equal
|
||||
/// keys containing it, so that no run (i.e. upper-node tile) straddles two devices. The keys
|
||||
/// must be globally sorted. The boundaries are adjusted monotonically from left to right, so a
|
||||
/// run spanning several stripes is consolidated onto one device and the fully-interior devices
|
||||
/// are left with empty stripes. Runs sequentially on a single thread: the boundary chain is a
|
||||
/// sequential dependence over deviceCount entries and each run extent is found by binary search.
|
||||
template<typename KeyT>
|
||||
__global__
|
||||
void snapBoundariesToRunsKernel(const KeyT* keys, ptrdiff_t keyCount, int deviceCount, ptrdiff_t* stripeOffsets, size_t* stripeCounts)
|
||||
{
|
||||
ptrdiff_t previousBoundary = stripeOffsets[0]; // device 0 always starts at 0
|
||||
for (int deviceId = 1; deviceId < deviceCount; ++deviceId) {
|
||||
ptrdiff_t boundary = stripeOffsets[deviceId];
|
||||
if (boundary >= keyCount) {
|
||||
boundary = keyCount;
|
||||
} else if (boundary > previousBoundary && keys[boundary] == keys[boundary - 1]) {
|
||||
// The even-split boundary falls inside a run; binary search for the run's extent
|
||||
// and snap to whichever end keeps the boundary closest to the even split without
|
||||
// crossing the previous boundary.
|
||||
const KeyT key = keys[boundary];
|
||||
ptrdiff_t lo = previousBoundary, hi = boundary;
|
||||
while (lo < hi) {
|
||||
const ptrdiff_t mid = lo + (hi - lo) / 2;
|
||||
if (keys[mid] < key) lo = mid + 1; else hi = mid;
|
||||
}
|
||||
const ptrdiff_t runStart = lo;
|
||||
lo = boundary; hi = keyCount;
|
||||
while (lo < hi) {
|
||||
const ptrdiff_t mid = lo + (hi - lo) / 2;
|
||||
if (keys[mid] <= key) lo = mid + 1; else hi = mid;
|
||||
}
|
||||
const ptrdiff_t runEnd = lo;
|
||||
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;
|
||||
stripeOffsets[deviceId] = boundary;
|
||||
previousBoundary = boundary;
|
||||
}
|
||||
|
||||
// Recompute the per-device counts from the adjusted, monotonic offsets.
|
||||
for (int deviceId = 0; deviceId < deviceCount; ++deviceId) {
|
||||
const ptrdiff_t nextOffset = (deviceId + 1 < deviceCount) ? stripeOffsets[deviceId + 1] : keyCount;
|
||||
stripeCounts[deviceId] = static_cast<size_t>(nextOffset - stripeOffsets[deviceId]);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace kernels
|
||||
@@ -120,135 +138,183 @@ void mergePathKernel(KeyIteratorIn keys1, size_t keys1Count, KeyIteratorIn keys2
|
||||
#endif// ifdef _WIN32
|
||||
#endif// ifndef CUB_LAUNCH
|
||||
|
||||
/// @brief Make every device stream wait for work submitted to all device streams.
|
||||
inline void mergeStreams(const nanovdb::cuda::DeviceMesh& deviceMesh, cudaEvent_t* events)
|
||||
{
|
||||
static constexpr int mergeDeviceId = 0;
|
||||
|
||||
// Record an event for each device in its respective stream.
|
||||
for (const auto& [deviceId, stream] : deviceMesh) {
|
||||
cudaCheck(cudaSetDevice(deviceId));
|
||||
cudaCheck(cudaEventRecord(events[deviceId], stream));
|
||||
}
|
||||
|
||||
// Fan in the per-device events on the merge stream.
|
||||
cudaCheck(cudaSetDevice(mergeDeviceId));
|
||||
const auto mergeStream = deviceMesh[mergeDeviceId].stream;
|
||||
for (const auto& deviceNode : deviceMesh) {
|
||||
cudaCheck(cudaStreamWaitEvent(mergeStream, events[deviceNode.id]));
|
||||
}
|
||||
cudaCheck(cudaEventRecord(events[mergeDeviceId], mergeStream));
|
||||
|
||||
// Fan the merged dependency back out to every device stream.
|
||||
for (const auto& [deviceId, stream] : deviceMesh) {
|
||||
cudaCheck(cudaSetDevice(deviceId));
|
||||
cudaCheck(cudaStreamWaitEvent(stream, events[mergeDeviceId]));
|
||||
}
|
||||
}
|
||||
|
||||
template<typename KeyT, typename ValueT, typename NumItemsT, typename OffsetT, typename CountT>
|
||||
void radixSortAsync(const nanovdb::cuda::DeviceMesh& deviceMesh, nanovdb::cuda::TempDevicePool* pools, KeyT* keysIn, KeyT* keysOut, ValueT* valuesIn, ValueT* valuesOut, NumItemsT numItems, OffsetT* mergeIntervals, const OffsetT* offsets, const CountT* counts, cudaEvent_t* preEvents, cudaEvent_t* postEvents)
|
||||
{
|
||||
// Radix sort the subset of keys assigned to each device in parallel
|
||||
if (!numItems) return;
|
||||
|
||||
const int deviceCount = static_cast<int>(deviceMesh.deviceCount());
|
||||
OffsetT* leftIntervals = mergeIntervals;
|
||||
OffsetT* rightIntervals = mergeIntervals + deviceCount + 1;
|
||||
const auto offset = [&](int deviceIndex) {
|
||||
return deviceIndex == deviceCount ? static_cast<OffsetT>(numItems) : offsets[deviceIndex];
|
||||
};
|
||||
|
||||
// Radix sort the subset of key/value pairs assigned to each GPU.
|
||||
for (const auto& [deviceId, stream] : deviceMesh) {
|
||||
cudaCheck(cudaSetDevice(deviceId));
|
||||
cudaCheck(cudaEventSynchronize(preEvents[deviceId]));
|
||||
|
||||
if (!counts[deviceId]) {
|
||||
cudaCheck(cudaEventRecord(postEvents[deviceId], stream));
|
||||
continue;
|
||||
}
|
||||
|
||||
const KeyT* deviceKeysIn = keysIn + offsets[deviceId];
|
||||
const ValueT* deviceValuesIn = valuesIn + offsets[deviceId];
|
||||
KeyT* deviceKeysOut = keysOut + offsets[deviceId];
|
||||
ValueT* deviceValuesOut = valuesOut + offsets[deviceId];
|
||||
|
||||
cudaCheck(util::cuda::memPrefetchAsync(deviceKeysIn, counts[deviceId] * sizeof(KeyT), deviceId, stream));
|
||||
cudaCheck(util::cuda::memPrefetchAsync(deviceValuesIn, counts[deviceId] * sizeof(ValueT), deviceId, stream));
|
||||
cudaCheck(util::cuda::memPrefetchAsync(deviceKeysOut, counts[deviceId] * sizeof(KeyT), deviceId, stream));
|
||||
cudaCheck(util::cuda::memPrefetchAsync(deviceValuesOut, counts[deviceId] * sizeof(ValueT), deviceId, stream));
|
||||
|
||||
// TODO: Add begin and end bit support
|
||||
CUB_LAUNCH(DeviceRadixSort::SortPairs, pools[deviceId], stream, deviceKeysIn, deviceKeysOut, deviceValuesIn, deviceValuesOut, counts[deviceId], 0, sizeof(KeyT) * 8);
|
||||
cudaCheck(cudaEventRecord(postEvents[deviceId], stream));
|
||||
}
|
||||
mergeStreams(deviceMesh, postEvents);
|
||||
|
||||
// TODO: Generalize to numbers of GPUs that aren't powers of two
|
||||
// For each pair of devices, merge the local sorts by first computing the median across the two devices followed by merging
|
||||
// the elements less than and greater than/equal to the median onto the first and second device of the pair respectively.
|
||||
// This avoids the allocating memory for and gathering the values from both devices onto a single device.
|
||||
const int log2DeviceCount = log2(deviceMesh.deviceCount());
|
||||
OffsetT* leftIntervals = mergeIntervals;
|
||||
OffsetT* rightIntervals = mergeIntervals + deviceMesh.deviceCount();
|
||||
for (int deviceExponent = 0; deviceExponent < log2DeviceCount; ++deviceExponent) {
|
||||
std::swap(keysIn, keysOut);
|
||||
std::swap(valuesIn, valuesOut);
|
||||
const int deviceIncrement = 1 << deviceExponent;
|
||||
KeyT* currentKeys = keysOut;
|
||||
KeyT* nextKeys = keysIn;
|
||||
ValueT* currentValues = valuesOut;
|
||||
ValueT* nextValues = valuesIn;
|
||||
|
||||
std::vector<std::thread> threads;
|
||||
for (int leftDeviceId = 0; leftDeviceId < static_cast<int>(deviceMesh.deviceCount()); leftDeviceId += 2 * deviceIncrement) {
|
||||
threads.emplace_back([&, leftDeviceId]() {
|
||||
const int rightDeviceId = leftDeviceId + deviceIncrement;
|
||||
// At each level, split every pair of sorted runs at all of the group's output-shard boundaries.
|
||||
// Every GPU in the group merges one balanced output shard, including at the upper merge-tree levels.
|
||||
for (int runDeviceCount = 1; runDeviceCount < deviceCount; runDeviceCount *= 2) {
|
||||
for (int groupStart = 0; groupStart < deviceCount; groupStart += 2 * runDeviceCount) {
|
||||
const int groupMiddle = std::min(groupStart + runDeviceCount, deviceCount);
|
||||
const int groupEnd = std::min(groupStart + 2 * runDeviceCount, deviceCount);
|
||||
if (groupMiddle == groupEnd) continue;
|
||||
|
||||
CountT leftDeviceItemCount = 0;
|
||||
for (int deviceId = leftDeviceId; deviceId < rightDeviceId; ++deviceId)
|
||||
leftDeviceItemCount += counts[deviceId];
|
||||
const CountT leftCount = offset(groupMiddle) - offset(groupStart);
|
||||
const CountT rightCount = offset(groupEnd) - offset(groupMiddle);
|
||||
const KeyT* leftKeys = currentKeys + offset(groupStart);
|
||||
const KeyT* rightKeys = currentKeys + offset(groupMiddle);
|
||||
|
||||
CountT rightDeviceItemCount = 0;
|
||||
for (int deviceId = rightDeviceId; deviceId < rightDeviceId + deviceIncrement; ++deviceId)
|
||||
rightDeviceItemCount += counts[deviceId];
|
||||
|
||||
const KeyT* leftDeviceKeysIn = keysIn + offsets[leftDeviceId];
|
||||
const ValueT* leftDeviceValuesIn = valuesIn + offsets[leftDeviceId];
|
||||
const KeyT* rightDeviceKeysIn = keysIn + offsets[leftDeviceId] + leftDeviceItemCount;
|
||||
const ValueT* rightDeviceValuesIn = valuesIn + offsets[leftDeviceId] + leftDeviceItemCount;
|
||||
|
||||
// Wait on the prior sort to finish on both devices before computing the median across both devices
|
||||
auto mergePathSubfunc = [&](int deviceId, int otherDeviceId, int intervalIndex) {
|
||||
cudaCheck(cudaSetDevice(deviceId));
|
||||
|
||||
cudaCheck(cudaStreamWaitEvent(deviceMesh[deviceId].stream, postEvents[otherDeviceId]));
|
||||
kernels::mergePathKernel<<<1, 1, 0, deviceMesh[deviceId].stream>>>(leftDeviceKeysIn, leftDeviceItemCount, rightDeviceKeysIn, rightDeviceItemCount, leftIntervals + deviceId, rightIntervals + deviceId, intervalIndex);
|
||||
cudaCheck(cudaEventRecord(postEvents[deviceId], deviceMesh[deviceId].stream));
|
||||
};
|
||||
mergePathSubfunc(leftDeviceId, rightDeviceId, 0);
|
||||
mergePathSubfunc(rightDeviceId, leftDeviceId, 1);
|
||||
|
||||
cudaCheck(cudaEventSynchronize(postEvents[leftDeviceId]));
|
||||
cudaCheck(cudaEventSynchronize(postEvents[rightDeviceId]));
|
||||
|
||||
// Merge the pairs less than the median to the left device
|
||||
{
|
||||
cudaCheck(cudaSetDevice(leftDeviceId));
|
||||
|
||||
const KeyT* leftKeysIn = leftDeviceKeysIn + leftIntervals[leftDeviceId];
|
||||
const ValueT* leftValuesIn = leftDeviceValuesIn + leftIntervals[leftDeviceId];
|
||||
CountT leftCount = leftIntervals[rightDeviceId] - leftIntervals[leftDeviceId];
|
||||
|
||||
const KeyT* rightKeysIn = rightDeviceKeysIn + rightIntervals[leftDeviceId];
|
||||
const ValueT* rightValuesIn = rightDeviceValuesIn + rightIntervals[leftDeviceId];
|
||||
CountT rightCount = rightIntervals[rightDeviceId] - rightIntervals[leftDeviceId];
|
||||
|
||||
OffsetT outputOffset = offsets[leftDeviceId] + leftIntervals[leftDeviceId] + rightIntervals[leftDeviceId];
|
||||
|
||||
CUB_LAUNCH(DeviceMerge::MergePairs, pools[leftDeviceId], deviceMesh[leftDeviceId].stream, leftKeysIn, leftValuesIn, leftCount, rightKeysIn, rightValuesIn, rightCount, keysOut + outputOffset, valuesOut + outputOffset, {});
|
||||
cudaCheck(cudaEventRecord(postEvents[leftDeviceId], deviceMesh[leftDeviceId].stream));
|
||||
};
|
||||
|
||||
// Merge the pairs greater than/equal to the median to the right device
|
||||
{
|
||||
cudaCheck(cudaSetDevice(rightDeviceId));
|
||||
|
||||
const KeyT* leftKeysIn = leftDeviceKeysIn + leftIntervals[rightDeviceId];
|
||||
const ValueT* leftValuesIn = leftDeviceValuesIn + leftIntervals[rightDeviceId];
|
||||
CountT leftCount = leftDeviceItemCount - leftIntervals[rightDeviceId];
|
||||
|
||||
const KeyT* rightKeysIn = rightDeviceKeysIn + rightIntervals[rightDeviceId];
|
||||
const ValueT* rightValuesIn = rightDeviceValuesIn + rightIntervals[rightDeviceId];
|
||||
CountT rightCount = rightDeviceItemCount - rightIntervals[rightDeviceId];
|
||||
|
||||
OffsetT outputOffset = offsets[leftDeviceId] + leftIntervals[rightDeviceId] + rightIntervals[rightDeviceId];
|
||||
|
||||
CUB_LAUNCH(DeviceMerge::MergePairs, pools[rightDeviceId], deviceMesh[rightDeviceId].stream, leftKeysIn, leftValuesIn, leftCount, rightKeysIn, rightValuesIn, rightCount, keysOut + outputOffset, valuesOut + outputOffset, {});
|
||||
cudaCheck(cudaEventRecord(postEvents[rightDeviceId], deviceMesh[rightDeviceId].stream));
|
||||
};
|
||||
|
||||
cudaCheck(cudaEventSynchronize(postEvents[leftDeviceId]));
|
||||
cudaCheck(cudaEventSynchronize(postEvents[rightDeviceId]));
|
||||
});
|
||||
for (int boundaryDevice = groupStart + 1; boundaryDevice < groupEnd; ++boundaryDevice) {
|
||||
cudaCheck(cudaSetDevice(boundaryDevice));
|
||||
const auto stream = deviceMesh[boundaryDevice].stream;
|
||||
const CountT diagonal = offset(boundaryDevice) - offset(groupStart);
|
||||
kernels::mergePathKernel<<<1, 1, 0, stream>>>(leftKeys, leftCount, rightKeys, rightCount, leftIntervals + boundaryDevice, rightIntervals + boundaryDevice, diagonal);
|
||||
cudaCheckError();
|
||||
cudaCheck(cudaEventRecord(postEvents[boundaryDevice], stream));
|
||||
}
|
||||
}
|
||||
std::for_each(threads.begin(), threads.end(), [](std::thread& t) { t.join(); });
|
||||
|
||||
// CUB needs the partition sizes on the host. Synchronize only the tiny merge-path kernels.
|
||||
for (int groupStart = 0; groupStart < deviceCount; groupStart += 2 * runDeviceCount) {
|
||||
const int groupMiddle = std::min(groupStart + runDeviceCount, deviceCount);
|
||||
const int groupEnd = std::min(groupStart + 2 * runDeviceCount, deviceCount);
|
||||
if (groupMiddle == groupEnd) continue;
|
||||
|
||||
for (int boundaryDevice = groupStart + 1; boundaryDevice < groupEnd; ++boundaryDevice) {
|
||||
cudaCheck(cudaEventSynchronize(postEvents[boundaryDevice]));
|
||||
}
|
||||
}
|
||||
|
||||
for (int groupStart = 0; groupStart < deviceCount; groupStart += 2 * runDeviceCount) {
|
||||
const int groupMiddle = std::min(groupStart + runDeviceCount, deviceCount);
|
||||
const int groupEnd = std::min(groupStart + 2 * runDeviceCount, deviceCount);
|
||||
|
||||
// A trailing run without a merge partner must still be copied because the
|
||||
// current and next buffers are swapped after every merge-tree level.
|
||||
if (groupMiddle == groupEnd) {
|
||||
for (int outputDevice = groupStart; outputDevice < groupEnd; ++outputDevice) {
|
||||
cudaCheck(cudaSetDevice(outputDevice));
|
||||
const auto stream = deviceMesh[outputDevice].stream;
|
||||
if (counts[outputDevice]) {
|
||||
cudaCheck(cudaMemcpyAsync(nextKeys + offset(outputDevice), currentKeys + offset(outputDevice), counts[outputDevice] * sizeof(KeyT), cudaMemcpyDefault, stream));
|
||||
cudaCheck(cudaMemcpyAsync(nextValues + offset(outputDevice), currentValues + offset(outputDevice), counts[outputDevice] * sizeof(ValueT), cudaMemcpyDefault, stream));
|
||||
}
|
||||
cudaCheck(cudaEventRecord(postEvents[outputDevice], stream));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const CountT leftCount = offset(groupMiddle) - offset(groupStart);
|
||||
const CountT rightCount = offset(groupEnd) - offset(groupMiddle);
|
||||
const KeyT* leftKeys = currentKeys + offset(groupStart);
|
||||
const KeyT* rightKeys = currentKeys + offset(groupMiddle);
|
||||
const ValueT* leftValues = currentValues + offset(groupStart);
|
||||
const ValueT* rightValues = currentValues + offset(groupMiddle);
|
||||
|
||||
for (int outputDevice = groupStart; outputDevice < groupEnd; ++outputDevice) {
|
||||
cudaCheck(cudaSetDevice(outputDevice));
|
||||
const auto stream = deviceMesh[outputDevice].stream;
|
||||
const CountT leftBegin = outputDevice == groupStart ? 0 : leftIntervals[outputDevice];
|
||||
const CountT leftEnd = outputDevice + 1 == groupEnd ? leftCount : leftIntervals[outputDevice + 1];
|
||||
const CountT rightBegin = outputDevice == groupStart ? 0 : rightIntervals[outputDevice];
|
||||
const CountT rightEnd = outputDevice + 1 == groupEnd ? rightCount : rightIntervals[outputDevice + 1];
|
||||
const CountT leftSegmentCount = leftEnd - leftBegin;
|
||||
const CountT rightSegmentCount = rightEnd - rightBegin;
|
||||
const CountT outputCount = leftSegmentCount + rightSegmentCount;
|
||||
NANOVDB_ASSERT(outputCount == counts[outputDevice]);
|
||||
|
||||
if (outputCount) {
|
||||
CUB_LAUNCH(DeviceMerge::MergePairs, pools[outputDevice], stream,
|
||||
leftKeys + leftBegin, leftValues + leftBegin, leftSegmentCount,
|
||||
rightKeys + rightBegin, rightValues + rightBegin, rightSegmentCount,
|
||||
nextKeys + offset(outputDevice), nextValues + offset(outputDevice), {});
|
||||
}
|
||||
cudaCheck(cudaEventRecord(postEvents[outputDevice], stream));
|
||||
}
|
||||
}
|
||||
|
||||
mergeStreams(deviceMesh, postEvents);
|
||||
std::swap(currentKeys, nextKeys);
|
||||
std::swap(currentValues, nextValues);
|
||||
}
|
||||
|
||||
// There is no merging required for a single device so we simply copy the sorted result to the destination array (where the sort would have been merged to).
|
||||
if (log2DeviceCount % 2) {
|
||||
std::swap(keysIn, keysOut);
|
||||
std::swap(valuesIn, valuesOut);
|
||||
// Keep the existing contract: callers always receive the final data in the output buffers.
|
||||
if (currentKeys != keysOut) {
|
||||
for (const auto& [deviceId, stream] : deviceMesh) {
|
||||
cudaCheck(cudaSetDevice(deviceId));
|
||||
|
||||
cudaMemcpyAsync(keysOut + offsets[deviceId], keysIn + offsets[deviceId], counts[deviceId] * sizeof(KeyT), cudaMemcpyDefault, stream);
|
||||
cudaMemcpyAsync(valuesOut + offsets[deviceId], valuesIn + offsets[deviceId], counts[deviceId] * sizeof(ValueT), cudaMemcpyDefault, stream);
|
||||
|
||||
cudaEventRecord(postEvents[deviceId], stream);
|
||||
if (counts[deviceId]) {
|
||||
cudaCheck(cudaMemcpyAsync(keysOut + offsets[deviceId], currentKeys + offsets[deviceId], counts[deviceId] * sizeof(KeyT), cudaMemcpyDefault, stream));
|
||||
cudaCheck(cudaMemcpyAsync(valuesOut + offsets[deviceId], currentValues + offsets[deviceId], counts[deviceId] * sizeof(ValueT), cudaMemcpyDefault, stream));
|
||||
}
|
||||
cudaCheck(cudaEventRecord(postEvents[deviceId], stream));
|
||||
}
|
||||
}
|
||||
mergeStreams(deviceMesh, postEvents);
|
||||
}
|
||||
|
||||
template<typename KeyT, typename ValueT, typename NumItemsT, typename OffsetT, typename CountT>
|
||||
void radixSortAsync(const nanovdb::cuda::DeviceMesh& deviceMesh, nanovdb::cuda::TempDevicePool* pools, KeyT* keysIn, KeyT* keysOut, ValueT* valuesIn, ValueT* valuesOut, NumItemsT numItems, const OffsetT* offsets, const CountT* counts, cudaEvent_t* preEvents, cudaEvent_t* postEvents)
|
||||
{
|
||||
ptrdiff_t* mergeIntervals = nullptr;
|
||||
cudaCheck(cudaMallocManaged(&mergeIntervals, 2 * deviceMesh.deviceCount() * sizeof(ptrdiff_t)));
|
||||
cudaCheck(cudaMallocHost(&mergeIntervals, 2 * (deviceMesh.deviceCount() + 1) * sizeof(ptrdiff_t)));
|
||||
radixSortAsync(deviceMesh, pools, keysIn, keysOut, valuesIn, valuesOut, numItems, mergeIntervals, offsets, counts, preEvents, postEvents);
|
||||
cudaCheck(cudaFree(mergeIntervals));
|
||||
cudaCheck(cudaFreeHost(mergeIntervals));
|
||||
}
|
||||
|
||||
/// @brief Launches an async exclusive sum operation across multiple devices. The operator waits on the per-device preEvents[deviceId] before summing over that device's contributions and records postEvents[deviceId] when the device's contribution is summed.
|
||||
@@ -263,10 +329,12 @@ void exclusiveSumAsync(const nanovdb::cuda::DeviceMesh& deviceMesh, nanovdb::cud
|
||||
// Required for the host to pass the correct value of counts[deviceId]
|
||||
cudaCheck(cudaEventSynchronize(preEvents[deviceId]));
|
||||
uint32_t deviceNumItems = counts[deviceId];
|
||||
CUB_LAUNCH(DeviceScan::ExclusiveSum, pools[deviceId], stream, deviceIn, deviceOut, deviceNumItems);
|
||||
if (deviceNumItems) {
|
||||
CUB_LAUNCH(DeviceScan::ExclusiveSum, pools[deviceId], stream, deviceIn, deviceOut, deviceNumItems);
|
||||
}
|
||||
cudaCheck(cudaEventRecord(preEvents[deviceId], stream));
|
||||
deviceIn += counts[deviceId];
|
||||
deviceOut += counts[deviceId];
|
||||
deviceIn += deviceNumItems;
|
||||
deviceOut += deviceNumItems;
|
||||
}
|
||||
|
||||
deviceIn = in;
|
||||
@@ -280,7 +348,7 @@ void exclusiveSumAsync(const nanovdb::cuda::DeviceMesh& deviceMesh, nanovdb::cud
|
||||
if (counts[deviceId]) {
|
||||
auto segmentExclusiveSum = deviceOut[counts[deviceId] - 1] + deviceIn[counts[deviceId] - 1];
|
||||
|
||||
unsigned int numBlocks = (counts[deviceId] + NumThreads - 1) / NumThreads;
|
||||
unsigned int numBlocks = ::cuda::ceil_div<int>(counts[deviceId], NumThreads);
|
||||
util::cuda::lambdaKernel<<<numBlocks, NumThreads, 0, stream>>>(counts[deviceId], [=] __device__ (size_t tid) { deviceOut[tid] += partialExclusiveSum; });
|
||||
cudaCheckError();
|
||||
|
||||
@@ -304,10 +372,12 @@ void inclusiveSumAsync(const nanovdb::cuda::DeviceMesh& deviceMesh, nanovdb::cud
|
||||
// Required for the host to pass the correct value of counts[deviceId]
|
||||
cudaCheck(cudaEventSynchronize(preEvents[deviceId]));
|
||||
uint32_t deviceNumItems = counts[deviceId];
|
||||
CUB_LAUNCH(DeviceScan::InclusiveSum, pools[deviceId], stream, deviceIn, deviceOut, deviceNumItems);
|
||||
if (deviceNumItems) {
|
||||
CUB_LAUNCH(DeviceScan::InclusiveSum, pools[deviceId], stream, deviceIn, deviceOut, deviceNumItems);
|
||||
}
|
||||
cudaCheck(cudaEventRecord(preEvents[deviceId], stream));
|
||||
deviceIn += counts[deviceId];
|
||||
deviceOut += counts[deviceId];
|
||||
deviceIn += deviceNumItems;
|
||||
deviceOut += deviceNumItems;
|
||||
}
|
||||
|
||||
deviceIn = in;
|
||||
@@ -321,7 +391,7 @@ void inclusiveSumAsync(const nanovdb::cuda::DeviceMesh& deviceMesh, nanovdb::cud
|
||||
if (counts[deviceId]) {
|
||||
auto segmentInclusiveSum = deviceOut[counts[deviceId] - 1];
|
||||
|
||||
unsigned int numBlocks = (counts[deviceId] + NumThreads - 1) / NumThreads;
|
||||
unsigned int numBlocks = ::cuda::ceil_div<int>(counts[deviceId], NumThreads);
|
||||
util::cuda::lambdaKernel<<<numBlocks, NumThreads, 0, stream>>>(counts[deviceId], [=] __device__ (size_t tid) { deviceOut[tid] += partialInclusiveSum; });
|
||||
cudaCheckError();
|
||||
|
||||
@@ -382,7 +452,7 @@ public:
|
||||
|
||||
private:
|
||||
static constexpr unsigned int mNumThreads = 128;
|
||||
static unsigned int numBlocks(unsigned int n) {return (n + mNumThreads - 1) / mNumThreads;}
|
||||
static unsigned int numBlocks(unsigned int n) {return ::cuda::ceil_div(n, mNumThreads);}
|
||||
|
||||
uint32_t* deviceNodeCount(int deviceId) const { return mNodeCounts + 3 * deviceId; }
|
||||
|
||||
@@ -433,7 +503,7 @@ DistributedPointsToGrid<BuildT>::DistributedPointsToGrid(const nanovdb::cuda::De
|
||||
mVoxelOffsets = nullptr;
|
||||
cudaCheck(cudaMallocManaged(&mVoxelOffsets, mDeviceMesh.deviceCount() * sizeof(uint32_t)));
|
||||
mIntervals = nullptr;
|
||||
cudaCheck(cudaMallocManaged(&mIntervals, 2 * mDeviceMesh.deviceCount() * sizeof(ptrdiff_t)));
|
||||
cudaCheck(cudaMallocHost(&mIntervals, 2 * (mDeviceMesh.deviceCount() + 1) * sizeof(ptrdiff_t)));
|
||||
}
|
||||
|
||||
template <typename BuildT>
|
||||
@@ -445,7 +515,7 @@ DistributedPointsToGrid<BuildT>::DistributedPointsToGrid(const nanovdb::cuda::De
|
||||
template <typename BuildT>
|
||||
DistributedPointsToGrid<BuildT>::~DistributedPointsToGrid()
|
||||
{
|
||||
cudaCheck(cudaFree(mIntervals));
|
||||
cudaCheck(cudaFreeHost(mIntervals));
|
||||
cudaCheck(cudaFree(mVoxelOffsets));
|
||||
cudaCheck(cudaFree(mVoxelCounts));
|
||||
cudaCheck(cudaFree(mNodeOffsets));
|
||||
@@ -535,33 +605,32 @@ void DistributedPointsToGrid<BuildT>::countNodes(const PtrT coords, size_t coord
|
||||
cudaEventCreateWithFlags(&leafPrefixSumEvents[deviceId], cudaEventDisableTiming);
|
||||
}
|
||||
|
||||
// Advise per-coord quantities to be split even across devices
|
||||
// Advise per-coord quantities to be split evenly across devices. Clamp each stripe to
|
||||
// the input range so that inputs smaller than the device count produce valid trailing empty stripes.
|
||||
const size_t deviceStripeSize = ::cuda::ceil_div(coordCount, mDeviceMesh.deviceCount());
|
||||
for (const auto& [deviceId, stream] : mDeviceMesh) {
|
||||
cudaCheck(cudaSetDevice(deviceId));
|
||||
|
||||
size_t deviceStripeCount = (coordCount + mDeviceMesh.deviceCount() - 1) / mDeviceMesh.deviceCount();
|
||||
const ptrdiff_t deviceStripeOffset = deviceStripeCount * deviceId;
|
||||
deviceStripeCount = std::min(deviceStripeCount, coordCount - deviceStripeOffset);
|
||||
const ptrdiff_t deviceStripeOffset = std::min(deviceStripeSize * deviceId, coordCount);
|
||||
const size_t deviceStripeCount = std::min(deviceStripeSize, coordCount - deviceStripeOffset);
|
||||
|
||||
mStripeCounts[deviceId] = deviceStripeCount;
|
||||
mStripeOffsets[deviceId] = deviceStripeOffset;
|
||||
|
||||
nanovdb::Coord* deviceCoords = coords + deviceStripeOffset;
|
||||
uint64_t* deviceInputKeys = mKeys + deviceStripeOffset;
|
||||
uint32_t* deviceInputIndices = mIndices + deviceStripeOffset;
|
||||
uint64_t* deviceOutputKeys = mData->d_keys + deviceStripeOffset;
|
||||
uint32_t* deviceOutputIndices = mData->d_indx + deviceStripeOffset;
|
||||
if (deviceStripeCount) {
|
||||
uint64_t* deviceInputKeys = mKeys + deviceStripeOffset;
|
||||
uint32_t* deviceInputIndices = mIndices + deviceStripeOffset;
|
||||
uint64_t* deviceOutputKeys = mData->d_keys + deviceStripeOffset;
|
||||
uint32_t* deviceOutputIndices = mData->d_indx + deviceStripeOffset;
|
||||
|
||||
util::cuda::memAdvise(deviceCoords, deviceStripeCount * sizeof(nanovdb::Coord), cudaMemAdviseSetPreferredLocation, deviceId);
|
||||
util::cuda::memAdvise(deviceCoords, deviceStripeCount * sizeof(nanovdb::Coord), cudaMemAdviseSetReadMostly, deviceId);
|
||||
util::cuda::memAdvise(deviceInputKeys, deviceStripeCount * sizeof(uint64_t), cudaMemAdviseSetPreferredLocation, deviceId);
|
||||
util::cuda::memAdvise(deviceInputIndices, deviceStripeCount * sizeof(uint32_t), cudaMemAdviseSetPreferredLocation, deviceId);
|
||||
util::cuda::memAdvise(deviceOutputKeys, deviceStripeCount * sizeof(uint64_t), cudaMemAdviseSetPreferredLocation, deviceId);
|
||||
util::cuda::memAdvise(deviceOutputIndices, deviceStripeCount * sizeof(uint32_t), cudaMemAdviseSetPreferredLocation, deviceId);
|
||||
|
||||
util::cuda::memAdvise(deviceInputKeys, deviceStripeCount * sizeof(uint64_t), cudaMemAdviseSetPreferredLocation, deviceId);
|
||||
util::cuda::memAdvise(deviceInputIndices, deviceStripeCount * sizeof(uint32_t), cudaMemAdviseSetPreferredLocation, deviceId);
|
||||
util::cuda::memAdvise(deviceOutputKeys, deviceStripeCount * sizeof(uint64_t), cudaMemAdviseSetPreferredLocation, deviceId);
|
||||
util::cuda::memAdvise(deviceOutputIndices, deviceStripeCount * sizeof(uint32_t), cudaMemAdviseSetPreferredLocation, deviceId);
|
||||
|
||||
uint32_t* devicePointsPerTile = mPointsPerTile + deviceStripeOffset;
|
||||
util::cuda::memAdvise(devicePointsPerTile, deviceStripeCount * sizeof(uint32_t), cudaMemAdviseSetPreferredLocation, deviceId);
|
||||
uint32_t* devicePointsPerTile = mPointsPerTile + deviceStripeOffset;
|
||||
util::cuda::memAdvise(devicePointsPerTile, deviceStripeCount * sizeof(uint32_t), cudaMemAdviseSetPreferredLocation, deviceId);
|
||||
}
|
||||
util::cuda::memAdvise(deviceNodeCount(deviceId), 3 * sizeof(uint32_t), cudaMemAdviseSetPreferredLocation, deviceId);
|
||||
}
|
||||
|
||||
@@ -572,9 +641,11 @@ void DistributedPointsToGrid<BuildT>::countNodes(const PtrT coords, size_t coord
|
||||
auto deviceStripeCount = mStripeCounts[deviceId];
|
||||
auto deviceStripeOffset = mStripeOffsets[deviceId];
|
||||
|
||||
util::cuda::memPrefetchAsync(coords, coordCount * sizeof(nanovdb::Coord), deviceId, stream);
|
||||
|
||||
util::cuda::offsetLambdaKernel<<<numBlocks(deviceStripeCount), mNumThreads, 0, stream>>>(deviceStripeCount, deviceStripeOffset, TileKeyFunctor<BuildT, PtrT>(), mData, coords, mData->d_keys, mData->d_indx);
|
||||
if (deviceStripeCount) {
|
||||
util::cuda::memPrefetchAsync(coords, coordCount * sizeof(nanovdb::Coord), deviceId, stream);
|
||||
nanovdb::util::cuda::offsetLambdaKernel<<<numBlocks(deviceStripeCount), mNumThreads, 0, stream>>>(deviceStripeCount, deviceStripeOffset, TileKeyFunctor<BuildT, PtrT>(), mData, coords, mData->d_keys, mData->d_indx);
|
||||
cudaCheckError();
|
||||
}
|
||||
}
|
||||
|
||||
radixSortAsync(mDeviceMesh, mTempDevicePools, mData->d_keys, mKeys, mData->d_indx, mIndices, coordCount, mIntervals, mStripeOffsets, mStripeCounts, sortEvents.data(), sortEvents.data());
|
||||
@@ -592,52 +663,26 @@ void DistributedPointsToGrid<BuildT>::countNodes(const PtrT coords, size_t coord
|
||||
// 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 (const auto& [deviceId, stream] : mDeviceMesh) {
|
||||
cudaCheck(cudaSetDevice(deviceId));
|
||||
cudaCheck(cudaStreamSynchronize(stream));
|
||||
}
|
||||
|
||||
// is simply a position where mKeys changes. Snapping runs in a single-thread
|
||||
// kernel on one device (every device stream is already ordered after the
|
||||
// whole sort by radixSortAsync's final stream merge) with binary searches for
|
||||
// the run extents; the host only waits on its completion event before reading
|
||||
// back the small offset/count arrays, mirroring how the sort's merge-path
|
||||
// partitions are synchronized. Fully-interior devices are left with empty
|
||||
// stripes, which the rest of the pipeline already handles.
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
// 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]);
|
||||
}
|
||||
static constexpr int snapDeviceId = 0;
|
||||
cudaCheck(cudaSetDevice(snapDeviceId));
|
||||
const auto snapStream = mDeviceMesh[snapDeviceId].stream;
|
||||
kernels::snapBoundariesToRunsKernel<<<1, 1, 0, snapStream>>>(mKeys, static_cast<ptrdiff_t>(coordCount), static_cast<int>(mDeviceMesh.deviceCount()), mStripeOffsets, mStripeCounts);
|
||||
cudaCheckError();
|
||||
cudaCheck(cudaEventRecord(sortEvents[snapDeviceId], snapStream));
|
||||
// CUB needs the rebalanced partition sizes on the host. Synchronize only the tiny snap kernel.
|
||||
cudaCheck(cudaEventSynchronize(sortEvents[snapDeviceId]));
|
||||
}
|
||||
|
||||
// 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.
|
||||
// above before the host read them back, so no per-device rebalance event is needed.
|
||||
for (const auto& [deviceId, stream] : mDeviceMesh) {
|
||||
cudaCheck(cudaSetDevice(deviceId));
|
||||
|
||||
@@ -650,7 +695,11 @@ void DistributedPointsToGrid<BuildT>::countNodes(const PtrT coords, size_t coord
|
||||
|
||||
// util::cuda::memPrefetchAsync(deviceInputKeys, deviceStripeCount * sizeof(uint64_t), deviceId, stream);
|
||||
|
||||
CUB_LAUNCH(DeviceRunLengthEncode::Encode, mTempDevicePools[deviceId], stream, deviceInputKeys, deviceOutputKeys, devicePointsPerTile, deviceNodeCount(deviceId) + 2, deviceStripeCount);
|
||||
if (deviceStripeCount) {
|
||||
CUB_LAUNCH(DeviceRunLengthEncode::Encode, mTempDevicePools[deviceId], stream, deviceInputKeys, deviceOutputKeys, devicePointsPerTile, deviceNodeCount(deviceId) + 2, deviceStripeCount);
|
||||
} else {
|
||||
cudaCheck(cudaMemsetAsync(deviceNodeCount(deviceId) + 2, 0, sizeof(uint32_t), stream));
|
||||
}
|
||||
cudaCheck(cudaEventRecord(runLengthEncodeEvents[deviceId], stream));
|
||||
}
|
||||
|
||||
@@ -660,7 +709,9 @@ void DistributedPointsToGrid<BuildT>::countNodes(const PtrT coords, size_t coord
|
||||
cudaCheck(cudaEventSynchronize(runLengthEncodeEvents[deviceId]));
|
||||
auto deviceStripeOffset = mStripeOffsets[deviceId];
|
||||
uint64_t* deviceKeys = mData->d_keys + deviceStripeOffset;
|
||||
cudaCheck(cudaMemcpyAsync(mData->d_tile_keys + upperOffset, deviceKeys, sizeof(uint64_t) * deviceNodeCount(deviceId)[2], cudaMemcpyDefault, stream));
|
||||
if (deviceNodeCount(deviceId)[2]) {
|
||||
cudaCheck(cudaMemcpyAsync(mData->d_tile_keys + upperOffset, deviceKeys, sizeof(uint64_t) * deviceNodeCount(deviceId)[2], cudaMemcpyDefault, stream));
|
||||
}
|
||||
deviceNodeOffset(deviceId)[2] = upperOffset;
|
||||
upperOffset += deviceNodeCount(deviceId)[2];
|
||||
}
|
||||
@@ -727,23 +778,25 @@ void DistributedPointsToGrid<BuildT>::countNodes(const PtrT coords, size_t coord
|
||||
uint64_t* deviceInputKeys = mKeys + mStripeOffsets[deviceId];
|
||||
const uint64_t* deviceOutputKeys = mData->d_keys + mStripeOffsets[deviceId];
|
||||
|
||||
if (deviceId == 0) {
|
||||
if (deviceId > 0) {
|
||||
cudaCheck(cudaEventSynchronize(voxelCountEvents[deviceId - 1]));
|
||||
devicePointsPerVoxel += mVoxelCounts[deviceId - 1];
|
||||
|
||||
cudaCheck(cudaEventSynchronize(leafCountEvents[deviceId - 1]));
|
||||
devicePointsPerLeaf += deviceNodeCount(deviceId - 1)[0];
|
||||
}
|
||||
|
||||
if (mStripeCounts[deviceId]) {
|
||||
CUB_LAUNCH(DeviceRunLengthEncode::Encode, mTempDevicePools[deviceId], stream, deviceOutputKeys, deviceInputKeys, devicePointsPerVoxel, mVoxelCounts + deviceId, mStripeCounts[deviceId]);
|
||||
cudaCheck(cudaEventRecord(voxelCountEvents[deviceId], stream));
|
||||
|
||||
CUB_LAUNCH(DeviceRunLengthEncode::Encode, mTempDevicePools[deviceId], stream, thrust::make_transform_iterator(deviceOutputKeys, ShiftRight<9>()), deviceInputKeys, devicePointsPerLeaf, deviceNodeCount(deviceId), mStripeCounts[deviceId]);
|
||||
cudaCheck(cudaEventRecord(leafCountEvents[deviceId], stream));
|
||||
}
|
||||
else
|
||||
{
|
||||
cudaCheck(cudaEventSynchronize(voxelCountEvents[deviceId - 1]));
|
||||
devicePointsPerVoxel += mVoxelCounts[deviceId - 1];
|
||||
CUB_LAUNCH(DeviceRunLengthEncode::Encode, mTempDevicePools[deviceId], stream, deviceOutputKeys, deviceInputKeys, devicePointsPerVoxel, mVoxelCounts + deviceId, mStripeCounts[deviceId]);
|
||||
} else {
|
||||
cudaCheck(cudaMemsetAsync(mVoxelCounts + deviceId, 0, sizeof(uint32_t), stream));
|
||||
cudaCheck(cudaEventRecord(voxelCountEvents[deviceId], stream));
|
||||
|
||||
cudaCheck(cudaEventSynchronize(leafCountEvents[deviceId - 1]));
|
||||
devicePointsPerLeaf += deviceNodeCount(deviceId - 1)[0];
|
||||
CUB_LAUNCH(DeviceRunLengthEncode::Encode, mTempDevicePools[deviceId], stream, thrust::make_transform_iterator(deviceOutputKeys, ShiftRight<9>()), deviceInputKeys, devicePointsPerLeaf, deviceNodeCount(deviceId), mStripeCounts[deviceId]);
|
||||
cudaCheck(cudaMemsetAsync(deviceNodeCount(deviceId), 0, sizeof(uint32_t), stream));
|
||||
cudaCheck(cudaEventRecord(leafCountEvents[deviceId], stream));
|
||||
}
|
||||
}
|
||||
@@ -757,7 +810,9 @@ void DistributedPointsToGrid<BuildT>::countNodes(const PtrT coords, size_t coord
|
||||
for (const auto& [deviceId, stream] : mDeviceMesh) {
|
||||
cudaCheck(cudaSetDevice(deviceId));
|
||||
uint64_t* deviceKeys = mKeys + mStripeOffsets[deviceId];
|
||||
cudaCheck(cudaMemcpyAsync(mData->d_leaf_keys + leafOffset, deviceKeys, sizeof(uint64_t) * deviceNodeCount(deviceId)[0], cudaMemcpyDefault, stream));
|
||||
if (deviceNodeCount(deviceId)[0]) {
|
||||
cudaCheck(cudaMemcpyAsync(mData->d_leaf_keys + leafOffset, deviceKeys, sizeof(uint64_t) * deviceNodeCount(deviceId)[0], cudaMemcpyDefault, stream));
|
||||
}
|
||||
deviceNodeOffset(deviceId)[0] = leafOffset;
|
||||
leafOffset += deviceNodeCount(deviceId)[0];
|
||||
}
|
||||
@@ -769,8 +824,12 @@ void DistributedPointsToGrid<BuildT>::countNodes(const PtrT coords, size_t coord
|
||||
uint64_t* deviceInputKeys = mKeys + mStripeOffsets[deviceId];
|
||||
uint64_t* deviceOutputKeys = mData->d_keys + mStripeOffsets[deviceId];
|
||||
|
||||
CUB_LAUNCH(DeviceSelect::Unique, mTempDevicePools[deviceId], stream, thrust::make_transform_iterator(deviceOutputKeys, ShiftRight<21>()), deviceInputKeys, deviceNodeCount(deviceId) + 1, mStripeCounts[deviceId]);
|
||||
cudaEventRecord(lowerCountEvents[deviceId], stream);
|
||||
if (mStripeCounts[deviceId]) {
|
||||
CUB_LAUNCH(DeviceSelect::Unique, mTempDevicePools[deviceId], stream, thrust::make_transform_iterator(deviceOutputKeys, ShiftRight<21>()), deviceInputKeys, deviceNodeCount(deviceId) + 1, mStripeCounts[deviceId]);
|
||||
} else {
|
||||
cudaCheck(cudaMemsetAsync(deviceNodeCount(deviceId) + 1, 0, sizeof(uint32_t), stream));
|
||||
}
|
||||
cudaCheck(cudaEventRecord(lowerCountEvents[deviceId], stream));
|
||||
}
|
||||
|
||||
uint32_t lowerOffset = 0;
|
||||
@@ -778,7 +837,9 @@ void DistributedPointsToGrid<BuildT>::countNodes(const PtrT coords, size_t coord
|
||||
cudaCheck(cudaSetDevice(deviceId));
|
||||
cudaCheck(cudaEventSynchronize(lowerCountEvents[deviceId]));
|
||||
uint64_t* deviceKeys = mKeys + mStripeOffsets[deviceId];
|
||||
cudaCheck(cudaMemcpyAsync(mData->d_lower_keys + lowerOffset, deviceKeys, sizeof(uint64_t) * deviceNodeCount(deviceId)[1], cudaMemcpyDefault, stream));
|
||||
if (deviceNodeCount(deviceId)[1]) {
|
||||
cudaCheck(cudaMemcpyAsync(mData->d_lower_keys + lowerOffset, deviceKeys, sizeof(uint64_t) * deviceNodeCount(deviceId)[1], cudaMemcpyDefault, stream));
|
||||
}
|
||||
deviceNodeOffset(deviceId)[1] = lowerOffset;
|
||||
lowerOffset += deviceNodeCount(deviceId)[1];
|
||||
}
|
||||
@@ -868,11 +929,14 @@ inline void DistributedPointsToGrid<BuildT>::processGridTreeRoot(const PtrT poin
|
||||
util::cuda::lambdaKernel<<<1, 1, 0, stream>>>(1, BuildGridTreeRootFunctor<BuildT, PtrT>(), mData, mPointType, pointCount);// lambdaKernel
|
||||
cudaCheckError();
|
||||
|
||||
// Zero the name field, then copy only the actual string (if any).
|
||||
char *dst = mData->getGrid().mGridName;
|
||||
if (const char *src = mGridName.data()) {
|
||||
cudaCheck(cudaMemcpyAsync(dst, src, GridData::MaxNameSize, cudaMemcpyHostToDevice, stream));
|
||||
} else {
|
||||
cudaCheck(cudaMemsetAsync(dst, 0, GridData::MaxNameSize, stream));
|
||||
cudaCheck(cudaMemsetAsync(dst, 0, GridData::MaxNameSize, stream));
|
||||
if (!mGridName.empty()) {
|
||||
// Copy at most MaxNameSize-1 bytes so the memset's trailing '\0' always
|
||||
// survives; a name >= MaxNameSize is truncated, never left unterminated.
|
||||
const size_t nameSize = std::min<size_t>(mGridName.size(), GridData::MaxNameSize - 1);
|
||||
cudaCheck(cudaMemcpyAsync(dst, mGridName.c_str(), nameSize, cudaMemcpyHostToDevice, stream));
|
||||
}
|
||||
cudaCheck(cudaEventRecord(processGridTreeRootEvent, stream));
|
||||
|
||||
@@ -935,12 +999,14 @@ inline void DistributedPointsToGrid<BuildT>::processNodes()
|
||||
std::vector<cudaEvent_t> leafCountEvents(mDeviceMesh.deviceCount());
|
||||
std::vector<cudaEvent_t> valueIndexPrefixSumEvents(mDeviceMesh.deviceCount());
|
||||
|
||||
auto lastDeviceId = cudaInvalidDeviceId;
|
||||
for (const auto& [deviceId, stream] : mDeviceMesh) {
|
||||
cudaSetDevice(deviceId);
|
||||
cudaEventCreateWithFlags(&leafCountEvents[deviceId], cudaEventDisableTiming);
|
||||
cudaEventCreateWithFlags(&valueIndexPrefixSumEvents[deviceId], cudaEventDisableTiming);
|
||||
|
||||
if (deviceNodeCount(deviceId)[0]) {
|
||||
lastDeviceId = deviceId;
|
||||
kernels::fillValueIndexKernel<BuildT><<<numBlocks(deviceNodeCount(deviceId)[0]), mNumThreads, 0, stream>>>(deviceNodeCount(deviceId)[0], deviceNodeOffset(deviceId)[0], mValueIndex, mData);
|
||||
cudaCheckError();
|
||||
}
|
||||
@@ -949,10 +1015,18 @@ inline void DistributedPointsToGrid<BuildT>::processNodes()
|
||||
LeafCountIterator leafCountIterator(mNodeCounts);
|
||||
inclusiveSumAsync(mDeviceMesh, mTempDevicePools, mValueIndex, mValueIndexPrefix, leafCountIterator, leafCountEvents.data(), valueIndexPrefixSumEvents.data());
|
||||
|
||||
// The first leaf on each device reads the last prefix value produced by the
|
||||
// previous device, while the first leaf globally reads the final prefix value.
|
||||
// Wait for the corresponding producer before launching each leaf-processing kernel.
|
||||
auto previousDeviceId = cudaInvalidDeviceId;
|
||||
for (const auto& [deviceId, stream] : mDeviceMesh) {
|
||||
cudaSetDevice(deviceId);
|
||||
cudaStreamWaitEvent(stream, valueIndexPrefixSumEvents.back());
|
||||
if (deviceNodeCount(deviceId)[0]) {
|
||||
cudaStreamWaitEvent(stream, valueIndexPrefixSumEvents[lastDeviceId]);
|
||||
if (previousDeviceId != cudaInvalidDeviceId) {
|
||||
cudaStreamWaitEvent(stream, valueIndexPrefixSumEvents[previousDeviceId]);
|
||||
}
|
||||
previousDeviceId = deviceId;
|
||||
kernels::leafPrefixSumKernel<BuildT><<<numBlocks(deviceNodeCount(deviceId)[0]), mNumThreads, 0, stream>>>(deviceNodeCount(deviceId)[0], deviceNodeOffset(deviceId)[0], mValueIndexPrefix, mData);
|
||||
cudaCheckError();
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user