diff --git a/devices/rtx/device/CMakeLists.txt b/devices/rtx/device/CMakeLists.txt index 4032f387f..d9df5e11c 100644 --- a/devices/rtx/device/CMakeLists.txt +++ b/devices/rtx/device/CMakeLists.txt @@ -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 @@ -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) diff --git a/devices/rtx/device/VisRTXDevice.cpp b/devices/rtx/device/VisRTXDevice.cpp index 520803a26..f416ee98d 100644 --- a/devices/rtx/device/VisRTXDevice.cpp +++ b/devices/rtx/device/VisRTXDevice.cpp @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2019-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: BSD-3-Clause * @@ -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 @@ -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) diff --git a/devices/rtx/device/gpu/gpu_objects.h b/devices/rtx/device/gpu/gpu_objects.h index 21e7c5d85..d54357f7d 100644 --- a/devices/rtx/device/gpu/gpu_objects.h +++ b/devices/rtx/device/gpu/gpu_objects.h @@ -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}; @@ -456,6 +467,7 @@ struct SpatialFieldGPUData NVdbRegularData nvdbRegular; StructuredRectilinearData structuredRectilinear; NVdbRectilinearData nvdbRectilinear; + AnalyticalData analytical; } data; UniformGridData grid; box3 roi; diff --git a/devices/rtx/device/gpu/sbt.h b/devices/rtx/device/gpu/sbt.h index 0725792cb..79646fdf9 100644 --- a/devices/rtx/device/gpu/sbt.h +++ b/devices/rtx/device/gpu/sbt.h @@ -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), }; diff --git a/devices/rtx/device/gpu/shadingState.h b/devices/rtx/device/gpu/shadingState.h index f363514dc..7daecb66f 100644 --- a/devices/rtx/device/gpu/shadingState.h +++ b/devices/rtx/device/gpu/shadingState.h @@ -207,6 +207,7 @@ struct VolumeSamplingState NvdbRectilinearSamplerState nvdbRectilinearFp16; NvdbRectilinearSamplerState nvdbRectilinearFpN; NvdbRectilinearSamplerState nvdbRectilinearFloat; + AnalyticalData analytical; // For analytical spatial fields }; }; diff --git a/devices/rtx/device/optix_visrtx.h b/devices/rtx/device/optix_visrtx.h index 3cf3a723c..98570e491 100644 --- a/devices/rtx/device/optix_visrtx.h +++ b/devices/rtx/device/optix_visrtx.h @@ -201,6 +201,7 @@ struct DeviceGlobalState : public helium::BaseGlobalDeviceState OptixModule nvdb{nullptr}; OptixModule structuredRectilinear{nullptr}; OptixModule nvdbRectilinear{nullptr}; + OptixModule analyticalField{nullptr}; } fieldSamplers; struct ObjectUpdates diff --git a/devices/rtx/device/renderer/Renderer.cpp b/devices/rtx/device/renderer/Renderer.cpp index 128aece42..82fe71d7c 100644 --- a/devices/rtx/device/renderer/Renderer.cpp +++ b/devices/rtx/device/renderer/Renderer.cpp @@ -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()) { diff --git a/devices/rtx/device/sampler/Image2D.h b/devices/rtx/device/sampler/Image2D.h index cb4eca5e9..c42ddf008 100644 --- a/devices/rtx/device/sampler/Image2D.h +++ b/devices/rtx/device/sampler/Image2D.h @@ -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; diff --git a/devices/rtx/device/sampler/Image3D.cpp b/devices/rtx/device/sampler/Image3D.cpp index 58c9b0fc5..748106a58 100644 --- a/devices/rtx/device/sampler/Image3D.cpp +++ b/devices/rtx/device/sampler/Image3D.cpp @@ -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(); diff --git a/devices/rtx/device/sampler/Image3D.h b/devices/rtx/device/sampler/Image3D.h index 8b1964bed..185539eef 100644 --- a/devices/rtx/device/sampler/Image3D.h +++ b/devices/rtx/device/sampler/Image3D.h @@ -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; diff --git a/devices/rtx/device/spatial_field/AnalyticalField.h b/devices/rtx/device/spatial_field/AnalyticalField.h new file mode 100644 index 000000000..85e6b10e9 --- /dev/null +++ b/devices/rtx/device/spatial_field/AnalyticalField.h @@ -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 diff --git a/devices/rtx/device/spatial_field/AnalyticalFieldSampler.cpp b/devices/rtx/device/spatial_field/AnalyticalFieldSampler.cpp new file mode 100644 index 000000000..46686da65 --- /dev/null +++ b/devices/rtx/device/spatial_field/AnalyticalFieldSampler.cpp @@ -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 diff --git a/devices/rtx/device/spatial_field/AnalyticalFieldSampler.h b/devices/rtx/device/spatial_field/AnalyticalFieldSampler.h new file mode 100644 index 000000000..957906129 --- /dev/null +++ b/devices/rtx/device/spatial_field/AnalyticalFieldSampler.h @@ -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 diff --git a/devices/rtx/device/spatial_field/AnalyticalFieldSampler_ptx.cu b/devices/rtx/device/spatial_field/AnalyticalFieldSampler_ptx.cu new file mode 100644 index 000000000..fe6eb35de --- /dev/null +++ b/devices/rtx/device/spatial_field/AnalyticalFieldSampler_ptx.cu @@ -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 +} diff --git a/devices/rtx/device/spatial_field/RegisterAnalyticalFields.cpp b/devices/rtx/device/spatial_field/RegisterAnalyticalFields.cpp new file mode 100644 index 000000000..d1be147ad --- /dev/null +++ b/devices/rtx/device/spatial_field/RegisterAnalyticalFields.cpp @@ -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 diff --git a/devices/rtx/device/spatial_field/SpatialField.cpp b/devices/rtx/device/spatial_field/SpatialField.cpp index 8cba79de8..b0404b473 100644 --- a/devices/rtx/device/spatial_field/SpatialField.cpp +++ b/devices/rtx/device/spatial_field/SpatialField.cpp @@ -30,6 +30,7 @@ */ #include "SpatialField.h" +#include "SpatialFieldRegistry.h" // specific types #include "NvdbRectilinearField.h" #include "NvdbRegularField.h" @@ -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") @@ -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 diff --git a/devices/rtx/device/spatial_field/SpatialFieldRegistry.h b/devices/rtx/device/spatial_field/SpatialFieldRegistry.h new file mode 100644 index 000000000..5cb7de0ba --- /dev/null +++ b/devices/rtx/device/spatial_field/SpatialFieldRegistry.h @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2019-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ + +#pragma once + +#include +#include +#include +#include + +namespace visrtx { + +// Forward declaration +class DeviceGlobalState; +struct SpatialField; + +// Factory function type for creating spatial fields +using SpatialFieldFactory = std::function; + +/** + * @brief Global registry for spatial field types + * + * Allows external plugins to register custom field types at static + * initialization time, enabling runtime extension of supported field types. + */ +class SpatialFieldRegistry { +public: + static SpatialFieldRegistry& instance() { + static SpatialFieldRegistry registry; + return registry; + } + + // Register a new spatial field type + void registerType(const std::string& type, SpatialFieldFactory factory) { + std::lock_guard lock(mutex_); + factories_[type] = factory; + } + + // Create a spatial field by type (returns nullptr if not found) + SpatialField* create(DeviceGlobalState* d, const std::string& type) { + std::lock_guard lock(mutex_); + auto it = factories_.find(type); + if (it != factories_.end()) { + return it->second(d); + } + return nullptr; + } + + // Check if a type is registered + bool hasType(const std::string& type) const { + std::lock_guard lock(mutex_); + return factories_.find(type) != factories_.end(); + } + +private: + SpatialFieldRegistry() = default; + SpatialFieldRegistry(const SpatialFieldRegistry&) = delete; + SpatialFieldRegistry& operator=(const SpatialFieldRegistry&) = delete; + + std::map factories_; + mutable std::mutex mutex_; +}; + +// Helper macro for registration (use in .cpp files) +// Creates a static object that registers the field type before main() runs +#define VISRTX_REGISTER_SPATIAL_FIELD(TYPE_NAME, CLASS_NAME) \ + namespace { \ + struct CLASS_NAME##Registrar { \ + CLASS_NAME##Registrar() { \ + visrtx::SpatialFieldRegistry::instance().registerType(TYPE_NAME, \ + [](visrtx::DeviceGlobalState* d) -> visrtx::SpatialField* { \ + return new CLASS_NAME(d); \ + }); \ + } \ + }; \ + static CLASS_NAME##Registrar g_##CLASS_NAME##Registrar; \ + } + +// Function to register custom analytical fields from external applications +// Called by VolumetricPlanets or other projects to register their field types +void registerAnalyticalField(const std::string& typeName, SpatialFieldFactory factory); + +} // namespace visrtx diff --git a/devices/rtx/external/CMakeLists.txt b/devices/rtx/external/CMakeLists.txt index b5b4ee61f..3f257d33d 100644 --- a/devices/rtx/external/CMakeLists.txt +++ b/devices/rtx/external/CMakeLists.txt @@ -27,6 +27,13 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -add_subdirectory(fmtlib) -add_subdirectory(nonstd) -add_subdirectory(stb_image) +# Guard against duplicate targets when building alongside TSD +if(NOT TARGET fmt) + add_subdirectory(fmtlib) +endif() +if(NOT TARGET nonstd) + add_subdirectory(nonstd) +endif() +if(NOT TARGET stb_image) + add_subdirectory(stb_image) +endif() diff --git a/devices/rtx/external/stb_image/CMakeLists.txt b/devices/rtx/external/stb_image/CMakeLists.txt index 100eb9ee5..6b60f8345 100644 --- a/devices/rtx/external/stb_image/CMakeLists.txt +++ b/devices/rtx/external/stb_image/CMakeLists.txt @@ -31,6 +31,19 @@ if (TARGET visrtx::stb_image) return() endif() +# Check if a different stb_image target exists (e.g., from OWL) +if (TARGET stb_image) + # Create an alias with VisRTX's expected name + if (NOT TARGET visrtx_stb_image) + add_library(visrtx_stb_image STATIC stb_image_write.c stb_image.c) + target_include_directories(visrtx_stb_image INTERFACE ${CMAKE_CURRENT_LIST_DIR}) + add_library(visrtx::stb_image ALIAS visrtx_stb_image) + # Also create the expected bare name alias for internal use + add_library(stb_image_visrtx ALIAS visrtx_stb_image) + endif() + return() +endif() + project(stb_image LANGUAGES C) add_library(stb_image STATIC stb_image_write.c stb_image.c) target_include_directories(stb_image INTERFACE ${CMAKE_CURRENT_LIST_DIR})