Skip to content
Open
3 changes: 3 additions & 0 deletions devices/rtx/device/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ set(SOURCES
spatial_field/StructuredRectilinearSampler.cpp
spatial_field/StructuredRegularSampler.cpp
spatial_field/space_skipping/UniformGrid.cu
spatial_field/AnalyticalFieldSampler.cpp
spatial_field/RegisterAnalyticalFields.cpp

surface/Surface.cpp

Expand Down Expand Up @@ -370,6 +372,7 @@ GenerateEmbeddedPTX(spatial_field NvdbRegularSampler)
GenerateEmbeddedPTX(spatial_field NvdbRectilinearSampler)
GenerateEmbeddedPTX(spatial_field StructuredRegularSampler)
GenerateEmbeddedPTX(spatial_field StructuredRectilinearSampler)
GenerateEmbeddedPTX(spatial_field AnalyticalFieldSampler)
if(VISRTX_ENABLE_MDL_SUPPORT)
GenerateEmbeddedPTX(material/shaders MDLShader ENTRIES __direct__callable__evalSurfaceMaterial)
GenerateEmbeddedPTX(material/shaders MDLTexture)
Expand Down
8 changes: 7 additions & 1 deletion devices/rtx/device/VisRTXDevice.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* Copyright (c) 2019-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
*
Expand Down Expand Up @@ -71,6 +71,8 @@
#include "spatial_field/NvdbRegularSampler.h"
#include "spatial_field/StructuredRectilinearSampler.h"
#include "spatial_field/StructuredRegularSampler.h"
// analytical field sampler (from devices/visrtx)
#include "spatial_field/AnalyticalFieldSampler.h"

// MDL
#ifdef USE_MDL
Expand Down Expand Up @@ -794,6 +796,10 @@ DeviceInitStatus VisRTXDevice::initOptix()
init_module(&state.fieldSamplers.nvdbRectilinear,
NvdbRectilinearSampler::ptx(),
"'nanovdbRectilinear' field sampler"),
// Analytical field sampler module (from devices/visrtx)
init_module(&state.fieldSamplers.analyticalField,
AnalyticalFieldSampler::ptx(),
"'analyticalField' sampler"),
};

for (auto &f : compileTasks)
Expand Down
12 changes: 12 additions & 0 deletions devices/rtx/device/gpu/gpu_objects.h
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,17 @@ struct NVdbRectilinearData
NVdbRectilinearData() = default;
};

struct AnalyticalData {
AnalyticalData() = default;
uint32_t subType;
uint32_t padding_; // Ensure fieldData is 8-byte aligned
// Generic storage for field-specific data
// External projects can use this to store
// their custom field parameters and reinterpret_cast as needed
// Aligned to 8 bytes to support cudaTextureObject_t and other 64-bit types
alignas(8) uint8_t fieldData[256];
};

struct SpatialFieldGPUData
{
SbtCallableEntryPoints samplerCallableIndex{SbtCallableEntryPoints::Invalid};
Expand All @@ -456,6 +467,7 @@ struct SpatialFieldGPUData
NVdbRegularData nvdbRegular;
StructuredRectilinearData structuredRectilinear;
NVdbRectilinearData nvdbRectilinear;
AnalyticalData analytical;
} data;
UniformGridData grid;
box3 roi;
Expand Down
6 changes: 5 additions & 1 deletion devices/rtx/device/gpu/sbt.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,11 @@ enum class SbtCallableEntryPoints : uint32_t
SpatialFieldSamplerNvdbRectilinearFloat =
SpatialFieldSamplerNvdbRectilinearFpN
+ int(SpatialFieldSamplerEntryPoints::Count),
Last = SpatialFieldSamplerNvdbRectilinearFloat
// Generic analytical field sampler - dispatches to specific types at runtime
// based on AnalyticalFieldType in the field data
SpatialFieldSamplerAnalytical = SpatialFieldSamplerNvdbRectilinearFloat
+ int(SpatialFieldSamplerEntryPoints::Count),
Last = SpatialFieldSamplerAnalytical
+ int(SpatialFieldSamplerEntryPoints::Count),
};

Expand Down
1 change: 1 addition & 0 deletions devices/rtx/device/gpu/shadingState.h
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ struct VolumeSamplingState
NvdbRectilinearSamplerState<nanovdb::Fp16> nvdbRectilinearFp16;
NvdbRectilinearSamplerState<nanovdb::FpN> nvdbRectilinearFpN;
NvdbRectilinearSamplerState<float> nvdbRectilinearFloat;
AnalyticalData analytical; // For analytical spatial fields
};
};

Expand Down
1 change: 1 addition & 0 deletions devices/rtx/device/optix_visrtx.h
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ struct DeviceGlobalState : public helium::BaseGlobalDeviceState
OptixModule nvdb{nullptr};
OptixModule structuredRectilinear{nullptr};
OptixModule nvdbRectilinear{nullptr};
OptixModule analyticalField{nullptr};
} fieldSamplers;

struct ObjectUpdates
Expand Down
15 changes: 15 additions & 0 deletions devices/rtx/device/renderer/Renderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,21 @@ void Renderer::initOptixPipeline()
callableDescs[SBT_CALLABLE_SPATIAL_FIELD_NVDB_REC_FLOAT_OFFSET
+ int(SpatialFieldSamplerEntryPoints::Sample)] = samplerDesc;

// Analytical field sampler (from devices/visrtx)
// A single callable pair handles all analytical field subtypes via type dispatch
samplerDesc.callables.moduleDC = state.fieldSamplers.analyticalField;

constexpr auto SBT_CALLABLE_ANALYTICAL_OFFSET =
int(SbtCallableEntryPoints::SpatialFieldSamplerAnalytical);
samplerDesc.callables.entryFunctionNameDC =
"__direct_callable__initAnalyticalSampler";
callableDescs[SBT_CALLABLE_ANALYTICAL_OFFSET
+ int(SpatialFieldSamplerEntryPoints::Init)] = samplerDesc;
samplerDesc.callables.entryFunctionNameDC =
"__direct_callable__sampleAnalytical";
callableDescs[SBT_CALLABLE_ANALYTICAL_OFFSET
+ int(SpatialFieldSamplerEntryPoints::Sample)] = samplerDesc;

#ifdef USE_MDL
if (state.mdl) {
for (const auto &ptxBlob : state.mdl->materialRegistry.getPtxBlobs()) {
Expand Down
3 changes: 3 additions & 0 deletions devices/rtx/device/sampler/Image2D.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ struct Image2D : public Sampler
bool isValid() const override;

int numChannels() const override;

// Public accessor for texture object (used by analytical fields)
cudaTextureObject_t textureObject() const { return m_texture; }

private:
SamplerGPUData gpuData() const override;
Expand Down
7 changes: 7 additions & 0 deletions devices/rtx/device/sampler/Image3D.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,13 @@ bool Image3D::isValid() const
return m_image;
}

uvec3 Image3D::imageSize() const
{
if (!m_image) return uvec3(0);
auto sz = m_image->size();
return uvec3(sz.x, sz.y, sz.z);
}

int Image3D::numChannels() const
{
ANARIDataType format = m_image->elementType();
Expand Down
6 changes: 6 additions & 0 deletions devices/rtx/device/sampler/Image3D.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ struct Image3D : public Sampler
bool isValid() const override;

int numChannels() const override;

// Public accessors for texture object and size (used by analytical fields)
cudaTextureObject_t textureObject() const { return m_texture; }
uvec3 imageSize() const;

Array3D* image() const { return m_image.get(); }

private:
SamplerGPUData gpuData() const override;
Expand Down
30 changes: 30 additions & 0 deletions devices/rtx/device/spatial_field/AnalyticalField.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright (c) 2019-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
*/

#pragma once

#include "SpatialField.h"

namespace visrtx {

/**
* @brief Abstract base class for analytical (procedural) spatial fields
*
* Analytical fields compute their values procedurally rather than from
* stored data. They provide a framework for implementing custom fields.
*
* Subclasses must implement:
* - commitParameters(): Parse ANARI parameters
* - finalize(): Prepare GPU data
* - bounds(): Return field bounding box
* - stepSize(): Return ray marching step size
*/
struct AnalyticalField : public SpatialField
{
AnalyticalField(DeviceGlobalState *d) : SpatialField(d) {}
~AnalyticalField() override = default;
};

} // namespace visrtx
14 changes: 14 additions & 0 deletions devices/rtx/device/spatial_field/AnalyticalFieldSampler.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2025-2026 NVIDIA Corporation
// SPDX-License-Identifier: Apache-2.0

#include "AnalyticalFieldSampler.h"
#include "AnalyticalFieldSampler_ptx.h"

namespace visrtx {

ptx_blob AnalyticalFieldSampler::ptx()
{
return {AnalyticalFieldSampler_ptx, sizeof(AnalyticalFieldSampler_ptx)};
}

} // namespace visrtx
21 changes: 21 additions & 0 deletions devices/rtx/device/spatial_field/AnalyticalFieldSampler.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright 2025-2026 NVIDIA Corporation
// SPDX-License-Identifier: Apache-2.0

#pragma once

#include "optix_visrtx.h"

namespace visrtx {

/**
* @brief PTX wrapper for analytical field samplers
*
* This single module contains callable programs for all analytical field
* subtypes
*/
struct AnalyticalFieldSampler
{
static ptx_blob ptx();
};

} // namespace visrtx
75 changes: 75 additions & 0 deletions devices/rtx/device/spatial_field/AnalyticalFieldSampler_ptx.cu
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright 2025-2026 NVIDIA Corporation
// SPDX-License-Identifier: Apache-2.0

/**
* @file AnalyticalFieldSampler_ptx.cu
* @brief OptiX callable programs for analytical spatial field sampling
*
* This file provides the OptiX callable entry points for analytical fields.
* The actual sampling implementations are provided by including external
* sampler headers that define per-field-type sampling functions.
*
* To add a new analytical field type:
* 1. Define the field data struct and add to AnalyticalFieldType enum
* 2. Create a sampler header with sampleXxx() function
* 3. Include the header below
* 4. Add a case to the switch in __direct_callable__sampleAnalytical
*/

#include "gpu/gpu_decl.h"
#include "gpu/gpu_objects.h"
#include "gpu/shadingState.h"

// Include analytical field data definitions (provides AnalyticalFieldType enum
// and field-specific data structures)
#ifdef VISRTX_ANALYTICAL_FIELD_DATA_HEADER
#include VISRTX_ANALYTICAL_FIELD_DATA_HEADER
#endif

// Include per-field sampler implementations
#ifdef VISRTX_ANALYTICAL_SAMPLERS_HEADER
#include VISRTX_ANALYTICAL_SAMPLERS_HEADER
#endif

using namespace visrtx;

//=============================================================================
// Exported OptiX callable programs
//=============================================================================

/**
* @brief Initialize analytical field sampler state
*
* Copies the field data to the sampler state for use during sampling.
*/
VISRTX_CALLABLE void __direct_callable__initAnalyticalSampler(
VolumeSamplingState *samplerState,
const SpatialFieldGPUData *field)
{
samplerState->analytical = field->data.analytical;
}

/**
* @brief Sample the analytical field at a given location
*
* Dispatches to the appropriate sampling function based on subType.
* Returns a normalized field value in [0, 1].
*
* If no analytical samplers are configured (VISRTX_ANALYTICAL_SAMPLE_DISPATCH
* not defined), returns 0.0 as a fallback.
*/
VISRTX_CALLABLE float __direct_callable__sampleAnalytical(
const VolumeSamplingState *samplerState,
const vec3 *location)
{
#ifdef VISRTX_ANALYTICAL_SAMPLE_DISPATCH
const AnalyticalData& data = samplerState->analytical;
const vec3 P = *location;

// Dispatch macro expands to switch statement with all registered field types
VISRTX_ANALYTICAL_SAMPLE_DISPATCH(data, P)
#else
// No analytical field types configured - return default value
return 0.0f;
#endif
}
24 changes: 24 additions & 0 deletions devices/rtx/device/spatial_field/RegisterAnalyticalFields.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2019-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
*/

#include "SpatialFieldRegistry.h"

namespace visrtx {

// This function is called by external applications (like VolumetricPlanets)
// to register their custom analytical spatial fields with the VisRTX device.
//
// Usage from VolumetricPlanets:
// #include "spatial_field/SpatialFieldRegistry.h"
// visrtx::registerAnalyticalField("magnetic", [](DeviceGlobalState* d) {
// return new MagneticField(d);
// });

void registerAnalyticalField(const std::string& typeName, SpatialFieldFactory factory)
{
SpatialFieldRegistry::instance().registerType(typeName, factory);
}

} // namespace visrtx
13 changes: 11 additions & 2 deletions devices/rtx/device/spatial_field/SpatialField.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
*/

#include "SpatialField.h"
#include "SpatialFieldRegistry.h"
// specific types
#include "NvdbRectilinearField.h"
#include "NvdbRegularField.h"
Expand All @@ -54,6 +55,7 @@ void SpatialField::markFinalized()
SpatialField *SpatialField::createInstance(
std::string_view subtype, DeviceGlobalState *d)
{
// Try built-in types first
if (subtype == "structuredRegular")
return new StructuredRegularField(d);
else if (subtype == "structuredRectilinear")
Expand All @@ -62,8 +64,15 @@ SpatialField *SpatialField::createInstance(
return new NvdbRegularField(d);
else if (subtype == "nanovdbRectilinear")
return new NvdbRectilinearField(d);
else
return new UnknownSpatialField(subtype, d);

// Try registry for custom field types (registered at static init time)
std::string subtypeStr(subtype);
if (auto* customField = SpatialFieldRegistry::instance().create(d, subtypeStr)) {
return customField;
}

// Unknown type
return new UnknownSpatialField(subtype, d);
}

} // namespace visrtx
Expand Down
Loading