From fd830dc3acc21cb7e3cd000ad6f447caf80b8a44 Mon Sep 17 00:00:00 2001 From: Cyrille Favreau Date: Mon, 26 Jan 2026 11:19:50 +0100 Subject: [PATCH 01/10] Fix stb_image target conflicts when building with Barney/OWL Resolves target name conflicts between VisRTX's stb_image (STATIC library) and Barney/OWL's stb_image (INTERFACE library) when building projects that depend on both. Changes: - Add guard to skip stb_image subdirectory if target already exists - Create visrtx_stb_image as alternative target when stb_image exists - Update tsd_tinygltf to handle both stb_image variants - Add proper include directories for OWL's stb/ subdirectory layout This allows VolumetricPlanets and other projects to build successfully when including both Barney and VisRTX/TSD in the same CMake project. --- devices/rtx/external/CMakeLists.txt | 4 +++- devices/rtx/external/stb_image/CMakeLists.txt | 13 +++++++++++++ tsd/external/tsd_tinygltf/CMakeLists.txt | 13 ++++++++++++- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/devices/rtx/external/CMakeLists.txt b/devices/rtx/external/CMakeLists.txt index b5b4ee61f..319e7bf4c 100644 --- a/devices/rtx/external/CMakeLists.txt +++ b/devices/rtx/external/CMakeLists.txt @@ -29,4 +29,6 @@ add_subdirectory(fmtlib) add_subdirectory(nonstd) -add_subdirectory(stb_image) +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}) diff --git a/tsd/external/tsd_tinygltf/CMakeLists.txt b/tsd/external/tsd_tinygltf/CMakeLists.txt index 59c44c18d..2494a2cac 100644 --- a/tsd/external/tsd_tinygltf/CMakeLists.txt +++ b/tsd/external/tsd_tinygltf/CMakeLists.txt @@ -6,6 +6,17 @@ project(tsd_ext_tinygltf) project_add_library(STATIC) project_include_directories(PUBLIC ${CMAKE_CURRENT_LIST_DIR}/include) project_include_directories(PRIVATE ${CMAKE_CURRENT_LIST_DIR}/src) -project_link_libraries(PRIVATE stb_image) +# Link to VisRTX's stb_image if available, otherwise use stb_image +if (TARGET visrtx_stb_image) + project_link_libraries(PRIVATE visrtx_stb_image) + project_include_directories(PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../../devices/rtx/external/stb_image) +elseif (TARGET stb_image) + project_link_libraries(PRIVATE stb_image) + # For OWL's stb_image, headers are in stb/ subdirectory + get_target_property(STB_IMAGE_INCLUDE_DIR stb_image INTERFACE_INCLUDE_DIRECTORIES) + if (STB_IMAGE_INCLUDE_DIR) + project_include_directories(PRIVATE ${STB_IMAGE_INCLUDE_DIR}/stb) + endif() +endif() project_compile_definitions(PUBLIC TINYGLTF_NOEXCEPTION=1) project_sources(PRIVATE src/tiny_gltf.cc) From 3dd69d0bb803311e3e22790af06b313808ac113c Mon Sep 17 00:00:00 2001 From: Cyrille Favreau Date: Wed, 28 Jan 2026 16:58:28 +0100 Subject: [PATCH 02/10] Add animation time change callback to UpdateDelegate system Extends TSD UpdateDelegate architecture to support animation time change notifications, enabling applications to respond when scene.setAnimationTime() is called. Changes: - Add signalAnimationTimeChanged(float time) to BaseUpdateDelegate interface - Implement signal propagation in MultiUpdateDelegate - Call signal from Scene::setAnimationTime() to notify all delegates - Add no-op implementation in RenderIndex - Update CameraPoses offline rendering to set animation time This enables applications to register custom delegates that respond to animation time changes, useful for updating time-dependent spatial fields during both interactive playback and offline rendering. Architecture benefits: - Clean observer pattern: TSD core decoupled from application code - Extensible: Multiple animation time observers can be registered - Works across all rendering modes: interactive, offline, camera paths --- tsd/src/tsd/core/scene/Scene.cpp | 4 + tsd/src/tsd/core/scene/UpdateDelegate.cpp | 6 + tsd/src/tsd/core/scene/UpdateDelegate.hpp | 3 + tsd/src/tsd/rendering/index/RenderIndex.hpp | 1 + tsd/src/tsd/ui/imgui/windows/CameraPoses.cpp | 123 ++++++++++++------- 5 files changed, 96 insertions(+), 41 deletions(-) diff --git a/tsd/src/tsd/core/scene/Scene.cpp b/tsd/src/tsd/core/scene/Scene.cpp index 27e013cd9..0d91a5735 100644 --- a/tsd/src/tsd/core/scene/Scene.cpp +++ b/tsd/src/tsd/core/scene/Scene.cpp @@ -548,6 +548,10 @@ void Scene::setAnimationTime(float time) m_animations.time = time; for (auto &a : m_animations.objects) a->update(time); + + // Signal delegates that animation time changed + if (m_updateDelegate) + m_updateDelegate->signalAnimationTimeChanged(time); } float Scene::getAnimationTime() const diff --git a/tsd/src/tsd/core/scene/UpdateDelegate.cpp b/tsd/src/tsd/core/scene/UpdateDelegate.cpp index e52240d53..bca87f755 100644 --- a/tsd/src/tsd/core/scene/UpdateDelegate.cpp +++ b/tsd/src/tsd/core/scene/UpdateDelegate.cpp @@ -137,4 +137,10 @@ void MultiUpdateDelegate::signalInvalidateCachedObjects() d->signalInvalidateCachedObjects(); } +void MultiUpdateDelegate::signalAnimationTimeChanged(float time) +{ + for (auto &d : m_delegates) + d->signalAnimationTimeChanged(time); +} + } // namespace tsd::core diff --git a/tsd/src/tsd/core/scene/UpdateDelegate.hpp b/tsd/src/tsd/core/scene/UpdateDelegate.hpp index 4fb56d2e3..9b8250a71 100644 --- a/tsd/src/tsd/core/scene/UpdateDelegate.hpp +++ b/tsd/src/tsd/core/scene/UpdateDelegate.hpp @@ -35,6 +35,7 @@ struct BaseUpdateDelegate virtual void signalActiveLayersChanged() = 0; virtual void signalObjectFilteringChanged() = 0; virtual void signalInvalidateCachedObjects() = 0; + virtual void signalAnimationTimeChanged(float time) = 0; // Not copyable or movable BaseUpdateDelegate(const BaseUpdateDelegate &) = delete; @@ -64,6 +65,7 @@ struct EmptyUpdateDelegate : public BaseUpdateDelegate void signalActiveLayersChanged() override {} void signalObjectFilteringChanged() override {} void signalInvalidateCachedObjects() override {} + void signalAnimationTimeChanged(float) override {} }; /// Update delegate that dispatches signals to N held other update delegates @@ -99,6 +101,7 @@ struct MultiUpdateDelegate : public BaseUpdateDelegate void signalActiveLayersChanged() override; void signalObjectFilteringChanged() override; void signalInvalidateCachedObjects() override; + void signalAnimationTimeChanged(float time) override; private: std::vector> m_delegates; diff --git a/tsd/src/tsd/rendering/index/RenderIndex.hpp b/tsd/src/tsd/rendering/index/RenderIndex.hpp index 36dda8336..0c4796d62 100644 --- a/tsd/src/tsd/rendering/index/RenderIndex.hpp +++ b/tsd/src/tsd/rendering/index/RenderIndex.hpp @@ -49,6 +49,7 @@ struct RenderIndex : public BaseUpdateDelegate void signalObjectRemoved(const Object *o) override; void signalRemoveAllObjects() override; void signalInvalidateCachedObjects() override; + void signalAnimationTimeChanged(float time) override {} // No-op for RenderIndex protected: virtual void updateWorld() = 0; diff --git a/tsd/src/tsd/ui/imgui/windows/CameraPoses.cpp b/tsd/src/tsd/ui/imgui/windows/CameraPoses.cpp index 9dfe3f1bd..273a9efff 100644 --- a/tsd/src/tsd/ui/imgui/windows/CameraPoses.cpp +++ b/tsd/src/tsd/ui/imgui/windows/CameraPoses.cpp @@ -15,6 +15,8 @@ #include "tsd/rendering/view/ManipulatorToAnari.hpp" // imgui #include +// SDL3 +#include // std #include #include @@ -278,38 +280,24 @@ void CameraPoses::buildUI_interpolationControls() // Update timer m_renderTimer.end(); - // Check if rendering is complete - if (m_renderFuture.valid() - && m_renderFuture.wait_for(std::chrono::milliseconds(0)) - == std::future_status::ready) { - m_renderFuture.get(); - m_isRendering = false; - - if (m_cancelRequested) { - tsd::core::logStatus("[CameraPoses] Rendering cancelled (%.2f seconds)", - m_renderTimer.seconds()); - } else { - tsd::core::logStatus("[CameraPoses] Rendering complete (%.2f seconds)", - m_renderTimer.seconds()); - } - } else { - // Calculate progress - float progress = 0.0f; - if (m_totalFrames > 0) { - progress = static_cast(m_currentFrame) - / static_cast(m_totalFrames); - } - - // Show progress bar with percentage - char progressText[64]; - snprintf(progressText, - sizeof(progressText), - "%d / %d frames (%.1f%%)", - m_currentFrame, - m_totalFrames, - progress * 100.0f); - ImGui::ProgressBar(progress, ImVec2(-1.0f, 0.0f), progressText); + // Since rendering is now synchronous, this code path won't be reached + // until rendering is complete. Progress updates happen within the render loop. + // Calculate progress + float progress = 0.0f; + if (m_totalFrames > 0) { + progress = static_cast(m_currentFrame) + / static_cast(m_totalFrames); } + + // Show progress bar with percentage + char progressText[64]; + snprintf(progressText, + sizeof(progressText), + "%d / %d frames (%.1f%%)", + m_currentFrame, + m_totalFrames, + progress * 100.0f); + ImGui::ProgressBar(progress, ImVec2(-1.0f, 0.0f), progressText); } ImGui::Unindent(INDENT_AMOUNT); @@ -409,15 +397,18 @@ void CameraPoses::renderInterpolatedPath() // Calculate total frames for capture const int capturedTotalFrames = m_totalFrames; - // Launch rendering in background - m_renderFuture = std::async(std::launch::async, - [this, - core, - samplesCopy, - capturedOutputDirectory, - capturedFilePrefix, - updateViewport, - capturedTotalFrames]() { + // Execute rendering synchronously in main thread + // NOTE: This WILL block the UI during rendering, but it's safer than + // creating separate ANARI devices from different threads, which causes + // OptiX crashes. For async rendering with UI updates, a more complex + // solution would be needed (e.g., frame-by-frame execution with event loop). + auto renderTask = [this, + core, + samplesCopy, + capturedOutputDirectory, + capturedFilePrefix, + updateViewport, + capturedTotalFrames]() { // Setup render pipeline auto &config = core->offline; auto d = core->anari.loadDevice(config.renderer.libraryName.c_str()); @@ -513,6 +504,9 @@ void CameraPoses::renderInterpolatedPath() // Render interpolated frames int frameIndex = 0; tsd::rendering::Manipulator manipulator; + + // Store original scene animation time to restore later + float originalTime = scene.getAnimationTime(); for (const auto &pose : samplesCopy) { if (m_cancelRequested) { @@ -524,6 +518,14 @@ void CameraPoses::renderInterpolatedPath() } m_currentFrame = frameIndex; + + // Update scene animation time for animated fields (clouds, aurora) + // Map frameIndex to 0.0-1.0 range over the entire animation + float time = capturedTotalFrames > 1 + ? static_cast(frameIndex) / (capturedTotalFrames - 1) + : 0.0f; + scene.setAnimationTime(time); + manipulator.setConfig(pose); tsd::rendering::updateCameraParametersPerspective(d, c, manipulator); anari::commitParameters(d, c); @@ -551,15 +553,54 @@ void CameraPoses::renderInterpolatedPath() pipeline->render(); } frameIndex++; + + // Process SDL events to detect ESC key for cancellation + // We can't process full ImGui UI, but we can detect keyboard input + SDL_Event event; + while (SDL_PollEvent(&event)) { + if (event.type == SDL_EVENT_KEY_DOWN && + event.key.key == SDLK_ESCAPE) { + m_cancelRequested = true; + tsd::core::logStatus("[CameraPoses] ESC pressed - cancelling..."); + } + if (event.type == SDL_EVENT_QUIT || + event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) { + m_cancelRequested = true; + } + } + + // Log progress every 10 frames + if (frameIndex % 10 == 0 || frameIndex == capturedTotalFrames) { + tsd::core::logStatus("[CameraPoses] Rendered %d/%d frames (%.1f%%) - Press ESC to cancel", + frameIndex, + capturedTotalFrames, + (100.0f * frameIndex) / capturedTotalFrames); + } } // Cleanup + scene.setAnimationTime(originalTime); // Restore original animation time pipeline.reset(); core->anari.releaseRenderIndex(d); anari::release(d, c); anari::release(d, r); anari::release(d, d); - }); + }; + + // Execute the rendering task synchronously + renderTask(); + + // Mark rendering as complete + m_isRendering = false; + m_renderTimer.end(); + + if (m_cancelRequested) { + tsd::core::logStatus("[CameraPoses] Rendering cancelled (%.2f seconds)", + m_renderTimer.seconds()); + } else { + tsd::core::logStatus("[CameraPoses] Rendering complete (%.2f seconds)", + m_renderTimer.seconds()); + } } } // namespace tsd::ui::imgui From bcacbc7f1111849e181f87e3fb6deb2004d058a6 Mon Sep 17 00:00:00 2001 From: Cyrille Favreau Date: Fri, 30 Jan 2026 16:38:55 +0100 Subject: [PATCH 03/10] Added analytical fields --- devices/rtx/device/CMakeLists.txt | 1 + devices/rtx/device/VisRTXDevice.cpp | 8 +- devices/rtx/device/gpu/gpu_objects.h | 5 + devices/rtx/device/gpu/sbt.h | 6 +- devices/rtx/device/gpu/shadingState.h | 5 + devices/rtx/device/optix_visrtx.h | 3 + devices/rtx/device/renderer/Renderer.cpp | 15 +++ .../device/spatial_field/AnalyticalField.h | 31 +++++ .../spatial_field/AnalyticalFieldData.h | 122 ++++++++++++++++++ .../RegisterAnalyticalFields.cpp | 24 ++++ .../rtx/device/spatial_field/SpatialField.cpp | 13 +- .../spatial_field/SpatialFieldRegistry.h | 85 ++++++++++++ devices/rtx/external/CMakeLists.txt | 9 +- 13 files changed, 321 insertions(+), 6 deletions(-) create mode 100644 devices/rtx/device/spatial_field/AnalyticalField.h create mode 100644 devices/rtx/device/spatial_field/AnalyticalFieldData.h create mode 100644 devices/rtx/device/spatial_field/RegisterAnalyticalFields.cpp create mode 100644 devices/rtx/device/spatial_field/SpatialFieldRegistry.h diff --git a/devices/rtx/device/CMakeLists.txt b/devices/rtx/device/CMakeLists.txt index 1f1cce19f..c5dceb8cb 100644 --- a/devices/rtx/device/CMakeLists.txt +++ b/devices/rtx/device/CMakeLists.txt @@ -190,6 +190,7 @@ set(SOURCES spatial_field/StructuredRectilinearSampler.cpp spatial_field/StructuredRegularSampler.cpp spatial_field/space_skipping/UniformGrid.cu + spatial_field/RegisterAnalyticalFields.cpp surface/Surface.cpp diff --git a/devices/rtx/device/VisRTXDevice.cpp b/devices/rtx/device/VisRTXDevice.cpp index c6a14113a..69f326627 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 "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 247af4556..853eeeade 100644 --- a/devices/rtx/device/gpu/gpu_objects.h +++ b/devices/rtx/device/gpu/gpu_objects.h @@ -45,6 +45,9 @@ // nanovdb #include +// Custom analytical field data structures +#include "spatial_field/AnalyticalFieldData.h" + // cuda half precision #ifdef VISRTX_USE_NEURAL #include @@ -456,6 +459,8 @@ struct SpatialFieldGPUData NVdbRegularData nvdbRegular; StructuredRectilinearData structuredRectilinear; NVdbRectilinearData nvdbRectilinear; + // Generic analytical field data - type dispatch happens in the sampler + AnalyticalFieldGPUData analytical; } data; UniformGridData grid; box3 roi; diff --git a/devices/rtx/device/gpu/sbt.h b/devices/rtx/device/gpu/sbt.h index 710e65ec6..1576f34ec 100644 --- a/devices/rtx/device/gpu/sbt.h +++ b/devices/rtx/device/gpu/sbt.h @@ -84,7 +84,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 23b0ba48b..427a8d028 100644 --- a/devices/rtx/device/gpu/shadingState.h +++ b/devices/rtx/device/gpu/shadingState.h @@ -185,6 +185,9 @@ struct NvdbRectilinearSamplerState cudaTextureObject_t axisLUT[3]; }; +// Analytical field sampler states (used by devices/visrtx analytical fields) +#include "spatial_field/AnalyticalFieldData.h" + struct VolumeSamplingState { VISRTX_DEVICE VolumeSamplingState() {}; @@ -203,6 +206,8 @@ struct VolumeSamplingState NvdbRectilinearSamplerState nvdbRectilinearFp16; NvdbRectilinearSamplerState nvdbRectilinearFpN; NvdbRectilinearSamplerState nvdbRectilinearFloat; + // Generic analytical field sampler - dispatches by type at runtime + AnalyticalFieldGPUData analytical; }; }; diff --git a/devices/rtx/device/optix_visrtx.h b/devices/rtx/device/optix_visrtx.h index 783a6b56e..91317ce2c 100644 --- a/devices/rtx/device/optix_visrtx.h +++ b/devices/rtx/device/optix_visrtx.h @@ -201,6 +201,9 @@ struct DeviceGlobalState : public helium::BaseGlobalDeviceState OptixModule nvdb{nullptr}; OptixModule structuredRectilinear{nullptr}; OptixModule nvdbRectilinear{nullptr}; + // Analytical field sampler module (from devices/visrtx) + // Contains all analytical field subtypes: magnetic, imf, aurora, planet, cloud + OptixModule analyticalField{nullptr}; } fieldSamplers; struct ObjectUpdates diff --git a/devices/rtx/device/renderer/Renderer.cpp b/devices/rtx/device/renderer/Renderer.cpp index eb7bc5927..eb14e58d8 100644 --- a/devices/rtx/device/renderer/Renderer.cpp +++ b/devices/rtx/device/renderer/Renderer.cpp @@ -672,6 +672,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/spatial_field/AnalyticalField.h b/devices/rtx/device/spatial_field/AnalyticalField.h new file mode 100644 index 000000000..f1ac0e30d --- /dev/null +++ b/devices/rtx/device/spatial_field/AnalyticalField.h @@ -0,0 +1,31 @@ +/* + * 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 + * like magnetic fields, aurora effects, or planetary surfaces. + * + * 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/AnalyticalFieldData.h b/devices/rtx/device/spatial_field/AnalyticalFieldData.h new file mode 100644 index 000000000..9a014190d --- /dev/null +++ b/devices/rtx/device/spatial_field/AnalyticalFieldData.h @@ -0,0 +1,122 @@ +// Copyright 2025-2026 NVIDIA Corporation +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "gpu/gpu_math.h" +#include + +/** + * @file AnalyticalFieldData.h + * @brief GPU data structures for custom analytical spatial fields + * + * This file provides a type-agnostic framework for analytical fields. + * External VisRTX only knows about the generic AnalyticalFieldGPUData struct. + * Specific field implementations (magnetic, aurora, etc.) are defined in the + * VolumetricPlanets project and dispatched at runtime based on the type field. + */ + +namespace visrtx { + +/** + * @brief Type identifier for analytical field subtypes + * + * This enum is used for runtime dispatch in the analytical field sampler. + * New field types should be added here when extending the system. + */ +enum class AnalyticalFieldType : uint32_t +{ + Unknown = 0, + Magnetic, + IMF, + Aurora, + Planet, + Cloud, + // Add new field types here + Count +}; + +} // namespace visrtx + +// Magnetic dipole field parameters +struct MagneticFieldData +{ + float equatorStrength; // Magnetic field strength at equator (nT) + float poleStrength; // Magnetic field strength at poles (nT) + float dipoleTilt; // Dipole tilt angle (radians, converted from degrees) + float invMaxRadius; // 1/maxRadius for bounding optimization +}; + +// Interplanetary Magnetic Field (IMF) parameters +struct IMFFieldData +{ + visrtx::vec3 solarWindVelocity; // Solar wind velocity vector (km/s) + float solarWindDensity; // Solar wind density (particles/cm³) + visrtx::vec3 imfDirection; // IMF direction (normalized) + float imfStrength; // IMF strength (nT) + float parkSpiralAngle; // Parker spiral angle (radians) + float padding[3]; // Alignment padding +}; + +// Aurora field parameters +struct AuroraFieldData +{ + float minAltitude; // Minimum altitude (km) + float maxAltitude; // Maximum altitude (km) + float minLatitude; // Minimum magnetic latitude (radians) + float maxLatitude; // Maximum magnetic latitude (radians) + float intensity; // Aurora intensity multiplier + float planetRadius; // Planet radius for coordinate conversion + float padding[2]; // Alignment padding +}; + +// Planet surface field parameters +struct PlanetFieldData +{ + cudaTextureObject_t elevationMap; // Elevation texture + cudaTextureObject_t diffuseMap; // Diffuse color texture + cudaTextureObject_t normalMap; // Normal map texture + float planetRadius; // Planet radius + float elevationScale; // Elevation scale factor + float atmosphereThickness; // Atmosphere thickness + int hasElevation; // Boolean: has elevation data + int hasDiffuse; // Boolean: has diffuse data + int hasNormal; // Boolean: has normal data + float padding[2]; // Alignment padding +}; + +// Cloud field parameters +struct CloudFieldData +{ + cudaTextureObject_t cloudData; // 3D cloud density texture + visrtx::vec3 cloudDims; // Cloud volume dimensions + float planetRadius; // Planet radius for spherical mapping + float normalEpsilon; // Epsilon for gradient computation + float atmosphereThickness; // Atmosphere layer thickness + int computeNormals; // Boolean: compute gradients for lighting + float padding; // Alignment padding +}; + +/** + * @brief Unified GPU data structure for all analytical field types + * + * This struct is used by external VisRTX. The type field determines + * which union member contains valid data, and the PTX sampler uses + * this for runtime dispatch. + * + * Note: Must be trivially constructible to be used in a union. + */ +struct AnalyticalFieldGPUData +{ + visrtx::AnalyticalFieldType type; + union + { + MagneticFieldData magnetic; + IMFFieldData imf; + AuroraFieldData aurora; + PlanetFieldData planet; + CloudFieldData cloud; + }; + + AnalyticalFieldGPUData() = default; +}; 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 319e7bf4c..3f257d33d 100644 --- a/devices/rtx/external/CMakeLists.txt +++ b/devices/rtx/external/CMakeLists.txt @@ -27,8 +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) +# 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() From 7535b36acba23fa862cecfd79346f2da8e3f8b8a Mon Sep 17 00:00:00 2001 From: Cyrille Favreau Date: Fri, 30 Jan 2026 17:32:20 +0100 Subject: [PATCH 04/10] Add texture accessors and lat/lon bounds for analytical fields - Add textureObject() and imageSize() accessors to Image2D and Image3D for analytical field texture sampling - Add lat/lon bounds (minLat, maxLat, minLon, maxLon) to CloudFieldData for geographic region filtering --- devices/rtx/device/sampler/Image2D.h | 3 +++ devices/rtx/device/sampler/Image3D.cpp | 7 +++++++ devices/rtx/device/sampler/Image3D.h | 6 ++++++ devices/rtx/device/spatial_field/AnalyticalFieldData.h | 7 +++++-- 4 files changed, 21 insertions(+), 2 deletions(-) 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/AnalyticalFieldData.h b/devices/rtx/device/spatial_field/AnalyticalFieldData.h index 9a014190d..021671a0c 100644 --- a/devices/rtx/device/spatial_field/AnalyticalFieldData.h +++ b/devices/rtx/device/spatial_field/AnalyticalFieldData.h @@ -91,10 +91,13 @@ struct CloudFieldData cudaTextureObject_t cloudData; // 3D cloud density texture visrtx::vec3 cloudDims; // Cloud volume dimensions float planetRadius; // Planet radius for spherical mapping - float normalEpsilon; // Epsilon for gradient computation float atmosphereThickness; // Atmosphere layer thickness + float minLat; // Minimum latitude in degrees + float maxLat; // Maximum latitude in degrees + float minLon; // Minimum longitude in degrees + float maxLon; // Maximum longitude in degrees + float normalEpsilon; // Epsilon for gradient computation int computeNormals; // Boolean: compute gradients for lighting - float padding; // Alignment padding }; /** From 5042a2671d254a771c1e19f317413e0e6d707a84 Mon Sep 17 00:00:00 2001 From: Cyrille Favreau Date: Tue, 3 Feb 2026 09:57:48 +0100 Subject: [PATCH 05/10] Added AnalyticalFieldSampler --- devices/rtx/device/CMakeLists.txt | 2 + devices/rtx/device/VisRTXDevice.cpp | 2 +- .../spatial_field/AnalyticalFieldSampler.cpp | 14 +++++ .../spatial_field/AnalyticalFieldSampler.h | 21 ++++++++ .../AnalyticalFieldSampler_ptx.cu | 54 +++++++++++++++++++ 5 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 devices/rtx/device/spatial_field/AnalyticalFieldSampler.cpp create mode 100644 devices/rtx/device/spatial_field/AnalyticalFieldSampler.h create mode 100644 devices/rtx/device/spatial_field/AnalyticalFieldSampler_ptx.cu diff --git a/devices/rtx/device/CMakeLists.txt b/devices/rtx/device/CMakeLists.txt index c5dceb8cb..32340503e 100644 --- a/devices/rtx/device/CMakeLists.txt +++ b/devices/rtx/device/CMakeLists.txt @@ -190,6 +190,7 @@ 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 @@ -371,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 69f326627..5395cab8a 100644 --- a/devices/rtx/device/VisRTXDevice.cpp +++ b/devices/rtx/device/VisRTXDevice.cpp @@ -72,7 +72,7 @@ #include "spatial_field/StructuredRectilinearSampler.h" #include "spatial_field/StructuredRegularSampler.h" // analytical field sampler (from devices/visrtx) -#include "AnalyticalFieldSampler.h" +#include "spatial_field/AnalyticalFieldSampler.h" // MDL #ifdef USE_MDL 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..69f4df29b --- /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: + * - Magnetic, IMF, Aurora, Planet, Cloud + */ +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..8d4108dc4 --- /dev/null +++ b/devices/rtx/device/spatial_field/AnalyticalFieldSampler_ptx.cu @@ -0,0 +1,54 @@ +// 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 module provides a single sampler entry point that dispatches to + * specific field type implementations based on the AnalyticalFieldType + * stored in the field data. This keeps external VisRTX type-agnostic. + * + * Supported field types: + * - Magnetic (dipole field) + * - IMF (Interplanetary Magnetic Field) + * - Aurora (auroral emissions) + * - Planet (planetary surface/atmosphere) + * - Cloud (cloud layer) + */ + +#include "gpu/gpu_decl.h" +#include "gpu/gpu_objects.h" +#include "gpu/shadingState.h" + +using namespace visrtx; + +//============================================================================= +// Exported OptiX callable programs - single dispatch point for all types +//============================================================================= + +/** + * @brief Initialize analytical field sampler state + * + * Copies the field data to the sampler state for use during sampling. + * The type field determines which union member is valid. + */ +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 the type field. + * Returns a normalized field value in [0, 1]. + */ +VISRTX_CALLABLE float __direct_callable__sampleAnalytical( + const VolumeSamplingState *samplerState, + const vec3 *location) +{ + return 0.0f; +} From ec44435d560a283709bd42252f1b2045e477e44a Mon Sep 17 00:00:00 2001 From: Cyrille Favreau Date: Tue, 3 Feb 2026 11:03:39 +0100 Subject: [PATCH 06/10] Make AnalyticalFieldSampler extensible via preprocessor macros - Add VISRTX_ANALYTICAL_FIELD_DATA_HEADER for external field data definitions - Add VISRTX_ANALYTICAL_SAMPLERS_HEADER for external sampler implementations - Add VISRTX_ANALYTICAL_SAMPLE_DISPATCH macro for dispatch logic - Fix 8-byte alignment for fieldData array to support cudaTextureObject_t - Remove AnalyticalFieldData.h (moved to consuming project) - Add AnalyticalData to VolumeSamplingState union for sampler access This allows external projects (like VolumetricPlanets) to provide custom analytical field implementations without modifying VisRTX core. --- devices/rtx/device/gpu/gpu_objects.h | 17 ++- devices/rtx/device/gpu/shadingState.h | 6 +- .../spatial_field/AnalyticalFieldData.h | 125 ------------------ .../AnalyticalFieldSampler_ptx.cu | 47 +++++-- 4 files changed, 47 insertions(+), 148 deletions(-) delete mode 100644 devices/rtx/device/spatial_field/AnalyticalFieldData.h diff --git a/devices/rtx/device/gpu/gpu_objects.h b/devices/rtx/device/gpu/gpu_objects.h index b840ca650..d54357f7d 100644 --- a/devices/rtx/device/gpu/gpu_objects.h +++ b/devices/rtx/device/gpu/gpu_objects.h @@ -45,9 +45,6 @@ // nanovdb #include -// Custom analytical field data structures -#include "spatial_field/AnalyticalFieldData.h" - // cuda half precision #ifdef VISRTX_USE_NEURAL #include @@ -450,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}; @@ -459,8 +467,7 @@ struct SpatialFieldGPUData NVdbRegularData nvdbRegular; StructuredRectilinearData structuredRectilinear; NVdbRectilinearData nvdbRectilinear; - // Generic analytical field data - type dispatch happens in the sampler - AnalyticalFieldGPUData analytical; + AnalyticalData analytical; } data; UniformGridData grid; box3 roi; diff --git a/devices/rtx/device/gpu/shadingState.h b/devices/rtx/device/gpu/shadingState.h index 6acdc2581..7daecb66f 100644 --- a/devices/rtx/device/gpu/shadingState.h +++ b/devices/rtx/device/gpu/shadingState.h @@ -189,9 +189,6 @@ struct NvdbRectilinearSamplerState cudaTextureObject_t axisLUT[3]; }; -// Analytical field sampler states (used by devices/visrtx analytical fields) -#include "spatial_field/AnalyticalFieldData.h" - struct VolumeSamplingState { VISRTX_DEVICE VolumeSamplingState() {}; @@ -210,8 +207,7 @@ struct VolumeSamplingState NvdbRectilinearSamplerState nvdbRectilinearFp16; NvdbRectilinearSamplerState nvdbRectilinearFpN; NvdbRectilinearSamplerState nvdbRectilinearFloat; - // Generic analytical field sampler - dispatches by type at runtime - AnalyticalFieldGPUData analytical; + AnalyticalData analytical; // For analytical spatial fields }; }; diff --git a/devices/rtx/device/spatial_field/AnalyticalFieldData.h b/devices/rtx/device/spatial_field/AnalyticalFieldData.h deleted file mode 100644 index 021671a0c..000000000 --- a/devices/rtx/device/spatial_field/AnalyticalFieldData.h +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2025-2026 NVIDIA Corporation -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -#include "gpu/gpu_math.h" -#include - -/** - * @file AnalyticalFieldData.h - * @brief GPU data structures for custom analytical spatial fields - * - * This file provides a type-agnostic framework for analytical fields. - * External VisRTX only knows about the generic AnalyticalFieldGPUData struct. - * Specific field implementations (magnetic, aurora, etc.) are defined in the - * VolumetricPlanets project and dispatched at runtime based on the type field. - */ - -namespace visrtx { - -/** - * @brief Type identifier for analytical field subtypes - * - * This enum is used for runtime dispatch in the analytical field sampler. - * New field types should be added here when extending the system. - */ -enum class AnalyticalFieldType : uint32_t -{ - Unknown = 0, - Magnetic, - IMF, - Aurora, - Planet, - Cloud, - // Add new field types here - Count -}; - -} // namespace visrtx - -// Magnetic dipole field parameters -struct MagneticFieldData -{ - float equatorStrength; // Magnetic field strength at equator (nT) - float poleStrength; // Magnetic field strength at poles (nT) - float dipoleTilt; // Dipole tilt angle (radians, converted from degrees) - float invMaxRadius; // 1/maxRadius for bounding optimization -}; - -// Interplanetary Magnetic Field (IMF) parameters -struct IMFFieldData -{ - visrtx::vec3 solarWindVelocity; // Solar wind velocity vector (km/s) - float solarWindDensity; // Solar wind density (particles/cm³) - visrtx::vec3 imfDirection; // IMF direction (normalized) - float imfStrength; // IMF strength (nT) - float parkSpiralAngle; // Parker spiral angle (radians) - float padding[3]; // Alignment padding -}; - -// Aurora field parameters -struct AuroraFieldData -{ - float minAltitude; // Minimum altitude (km) - float maxAltitude; // Maximum altitude (km) - float minLatitude; // Minimum magnetic latitude (radians) - float maxLatitude; // Maximum magnetic latitude (radians) - float intensity; // Aurora intensity multiplier - float planetRadius; // Planet radius for coordinate conversion - float padding[2]; // Alignment padding -}; - -// Planet surface field parameters -struct PlanetFieldData -{ - cudaTextureObject_t elevationMap; // Elevation texture - cudaTextureObject_t diffuseMap; // Diffuse color texture - cudaTextureObject_t normalMap; // Normal map texture - float planetRadius; // Planet radius - float elevationScale; // Elevation scale factor - float atmosphereThickness; // Atmosphere thickness - int hasElevation; // Boolean: has elevation data - int hasDiffuse; // Boolean: has diffuse data - int hasNormal; // Boolean: has normal data - float padding[2]; // Alignment padding -}; - -// Cloud field parameters -struct CloudFieldData -{ - cudaTextureObject_t cloudData; // 3D cloud density texture - visrtx::vec3 cloudDims; // Cloud volume dimensions - float planetRadius; // Planet radius for spherical mapping - float atmosphereThickness; // Atmosphere layer thickness - float minLat; // Minimum latitude in degrees - float maxLat; // Maximum latitude in degrees - float minLon; // Minimum longitude in degrees - float maxLon; // Maximum longitude in degrees - float normalEpsilon; // Epsilon for gradient computation - int computeNormals; // Boolean: compute gradients for lighting -}; - -/** - * @brief Unified GPU data structure for all analytical field types - * - * This struct is used by external VisRTX. The type field determines - * which union member contains valid data, and the PTX sampler uses - * this for runtime dispatch. - * - * Note: Must be trivially constructible to be used in a union. - */ -struct AnalyticalFieldGPUData -{ - visrtx::AnalyticalFieldType type; - union - { - MagneticFieldData magnetic; - IMFFieldData imf; - AuroraFieldData aurora; - PlanetFieldData planet; - CloudFieldData cloud; - }; - - AnalyticalFieldGPUData() = default; -}; diff --git a/devices/rtx/device/spatial_field/AnalyticalFieldSampler_ptx.cu b/devices/rtx/device/spatial_field/AnalyticalFieldSampler_ptx.cu index 8d4108dc4..fe6eb35de 100644 --- a/devices/rtx/device/spatial_field/AnalyticalFieldSampler_ptx.cu +++ b/devices/rtx/device/spatial_field/AnalyticalFieldSampler_ptx.cu @@ -5,33 +5,42 @@ * @file AnalyticalFieldSampler_ptx.cu * @brief OptiX callable programs for analytical spatial field sampling * - * This module provides a single sampler entry point that dispatches to - * specific field type implementations based on the AnalyticalFieldType - * stored in the field data. This keeps external VisRTX type-agnostic. + * 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. * - * Supported field types: - * - Magnetic (dipole field) - * - IMF (Interplanetary Magnetic Field) - * - Aurora (auroral emissions) - * - Planet (planetary surface/atmosphere) - * - Cloud (cloud layer) + * 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 - single dispatch point for all types +// Exported OptiX callable programs //============================================================================= /** * @brief Initialize analytical field sampler state * * Copies the field data to the sampler state for use during sampling. - * The type field determines which union member is valid. */ VISRTX_CALLABLE void __direct_callable__initAnalyticalSampler( VolumeSamplingState *samplerState, @@ -43,12 +52,24 @@ VISRTX_CALLABLE void __direct_callable__initAnalyticalSampler( /** * @brief Sample the analytical field at a given location * - * Dispatches to the appropriate sampling function based on the type field. + * 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) { - return 0.0f; +#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 } From befafcb2b21311dcbe40779d2c2cfc6c1defe909 Mon Sep 17 00:00:00 2001 From: Cyrille Favreau Date: Tue, 3 Feb 2026 11:06:34 +0100 Subject: [PATCH 07/10] Cleanup --- devices/rtx/device/optix_visrtx.h | 2 - .../device/spatial_field/AnalyticalField.h | 7 +- .../spatial_field/AnalyticalFieldSampler.h | 6 +- tsd/src/tsd/ui/imgui/windows/CameraPoses.cpp | 369 +++++++++--------- 4 files changed, 187 insertions(+), 197 deletions(-) diff --git a/devices/rtx/device/optix_visrtx.h b/devices/rtx/device/optix_visrtx.h index 6d02e9c40..98570e491 100644 --- a/devices/rtx/device/optix_visrtx.h +++ b/devices/rtx/device/optix_visrtx.h @@ -201,8 +201,6 @@ struct DeviceGlobalState : public helium::BaseGlobalDeviceState OptixModule nvdb{nullptr}; OptixModule structuredRectilinear{nullptr}; OptixModule nvdbRectilinear{nullptr}; - // Analytical field sampler module (from devices/visrtx) - // Contains all analytical field subtypes: magnetic, imf, aurora, planet, cloud OptixModule analyticalField{nullptr}; } fieldSamplers; diff --git a/devices/rtx/device/spatial_field/AnalyticalField.h b/devices/rtx/device/spatial_field/AnalyticalField.h index f1ac0e30d..85e6b10e9 100644 --- a/devices/rtx/device/spatial_field/AnalyticalField.h +++ b/devices/rtx/device/spatial_field/AnalyticalField.h @@ -11,11 +11,10 @@ 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 - * like magnetic fields, aurora effects, or planetary surfaces. - * + * stored data. They provide a framework for implementing custom fields. + * * Subclasses must implement: * - commitParameters(): Parse ANARI parameters * - finalize(): Prepare GPU data diff --git a/devices/rtx/device/spatial_field/AnalyticalFieldSampler.h b/devices/rtx/device/spatial_field/AnalyticalFieldSampler.h index 69f4df29b..957906129 100644 --- a/devices/rtx/device/spatial_field/AnalyticalFieldSampler.h +++ b/devices/rtx/device/spatial_field/AnalyticalFieldSampler.h @@ -9,9 +9,9 @@ namespace visrtx { /** * @brief PTX wrapper for analytical field samplers - * - * This single module contains callable programs for all analytical field subtypes: - * - Magnetic, IMF, Aurora, Planet, Cloud + * + * This single module contains callable programs for all analytical field + * subtypes */ struct AnalyticalFieldSampler { diff --git a/tsd/src/tsd/ui/imgui/windows/CameraPoses.cpp b/tsd/src/tsd/ui/imgui/windows/CameraPoses.cpp index df83f83ee..ebf1ed465 100644 --- a/tsd/src/tsd/ui/imgui/windows/CameraPoses.cpp +++ b/tsd/src/tsd/ui/imgui/windows/CameraPoses.cpp @@ -281,8 +281,8 @@ void CameraPoses::buildUI_interpolationControls() m_renderTimer.end(); // Since rendering is now synchronous, this code path won't be reached - // until rendering is complete. Progress updates happen within the render loop. - // Calculate progress + // until rendering is complete. Progress updates happen within the render + // loop. Calculate progress float progress = 0.0f; if (m_totalFrames > 0) { progress = static_cast(m_currentFrame) @@ -403,204 +403,197 @@ void CameraPoses::renderInterpolatedPath() // OptiX crashes. For async rendering with UI updates, a more complex // solution would be needed (e.g., frame-by-frame execution with event loop). auto renderTask = [this, - core, - samplesCopy, - capturedOutputDirectory, - capturedFilePrefix, - updateViewport, - capturedTotalFrames]() { - // Setup render pipeline - auto &config = core->offline; - auto d = core->anari.loadDevice(config.renderer.libraryName.c_str()); - if (!d) { - tsd::core::logError("[CameraPoses] Failed to load ANARI device"); - return; - } + core, + samplesCopy, + capturedOutputDirectory, + capturedFilePrefix, + updateViewport, + capturedTotalFrames]() { + // Setup render pipeline + auto &config = core->offline; + auto d = core->anari.loadDevice(config.renderer.libraryName.c_str()); + if (!d) { + tsd::core::logError("[CameraPoses] Failed to load ANARI device"); + return; + } - auto &scene = core->tsd.scene; - auto *renderIndex = core->anari.acquireRenderIndex(scene, d); - if (!renderIndex) { - tsd::core::logError("[CameraPoses] Failed to acquire render index"); - anari::release(d, d); - return; - } + auto &scene = core->tsd.scene; + auto *renderIndex = core->anari.acquireRenderIndex(scene, d); + if (!renderIndex) { + tsd::core::logError("[CameraPoses] Failed to acquire render index"); + anari::release(d, d); + return; + } + + // Create renderer + if (config.renderer.rendererObjects.empty() + || config.renderer.activeRenderer < 0 + || config.renderer.activeRenderer + >= static_cast(config.renderer.rendererObjects.size())) { + tsd::core::logError("[CameraPoses] No renderer configured"); + anari::release(d, d); + return; + } - // Create renderer - if (config.renderer.rendererObjects.empty() - || config.renderer.activeRenderer < 0 - || config.renderer.activeRenderer - >= static_cast(config.renderer.rendererObjects.size())) { - tsd::core::logError("[CameraPoses] No renderer configured"); - anari::release(d, d); - return; + // Log renderer details at creation time + tsd::core::logStatus( + "[CameraPoses] Creating renderer in background thread:"); + tsd::core::logStatus( + " activeRenderer index: %d", config.renderer.activeRenderer); + tsd::core::logStatus(" rendererObjects.size(): %zu", + config.renderer.rendererObjects.size()); + + auto &ro = config.renderer.rendererObjects[config.renderer.activeRenderer]; + + tsd::core::logStatus(" Renderer subtype: '%s'", ro.subtype().c_str()); + tsd::core::logStatus(" Renderer name: '%s'", ro.name().c_str()); + + auto r = anari::newObject(d, ro.subtype().c_str()); + tsd::core::logStatus(" Created ANARI renderer object"); + + ro.updateAllANARIParameters(d, r); + tsd::core::logStatus(" Updated renderer parameters"); + + anari::commitParameters(d, r); + tsd::core::logStatus(" Committed renderer parameters"); + + // Create camera + auto c = anari::newObject(d, "perspective"); + anari::setParameter(d, + c, + "aspect", + static_cast(config.frame.width) / config.frame.height); + anari::setParameter(d, c, "fovy", anari::radians(40.f)); + anari::commitParameters(d, c); + + // Create render pipeline + auto pipeline = std::make_unique( + config.frame.width, config.frame.height); + + auto *anariPass = + pipeline->emplace_back(d); + anariPass->setRunAsync(false); + anariPass->setWorld(renderIndex->world()); + anariPass->setRenderer(r); + anariPass->setCamera(c); + + // Add AOV visualization pass if enabled + if (config.aov.aovType != tsd::rendering::AOVType::NONE) { + auto *aovPass = + pipeline->emplace_back(); + aovPass->setAOVType(config.aov.aovType); + aovPass->setDepthRange(config.aov.depthMin, config.aov.depthMax); + aovPass->setEdgeThreshold(config.aov.edgeThreshold); + aovPass->setEdgeInvert(config.aov.edgeInvert); + + // Enable necessary frame channels + if (config.aov.aovType == tsd::rendering::AOVType::ALBEDO) { + anariPass->setEnableAlbedo(true); + } else if (config.aov.aovType == tsd::rendering::AOVType::NORMAL) { + anariPass->setEnableNormals(true); + } else if (config.aov.aovType == tsd::rendering::AOVType::EDGES + || config.aov.aovType == tsd::rendering::AOVType::OBJECT_ID) { + anariPass->setEnableIDs(true); + } else if (config.aov.aovType == tsd::rendering::AOVType::PRIMITIVE_ID) { + anariPass->setEnablePrimitiveId(true); + } else if (config.aov.aovType == tsd::rendering::AOVType::INSTANCE_ID) { + anariPass->setEnableInstanceId(true); + } + } + + auto *savePass = pipeline->emplace_back(); + savePass->setSingleShotMode(false); + + // Render interpolated frames + int frameIndex = 0; + tsd::rendering::Manipulator manipulator; + + // Store original scene animation time to restore later + float originalTime = scene.getAnimationTime(); + + for (const auto &pose : samplesCopy) { + if (m_cancelRequested) { + tsd::core::logInfo("[CameraPoses] Rendering cancelled at frame %d/%d", + frameIndex, + capturedTotalFrames); + break; + } + + m_currentFrame = frameIndex; + + // Map frameIndex to 0.0-1.0 range over the entire animation + float time = capturedTotalFrames > 1 + ? static_cast(frameIndex) / (capturedTotalFrames - 1) + : 0.0f; + scene.setAnimationTime(time); + + manipulator.setConfig(pose); + tsd::rendering::updateCameraParametersPerspective(d, c, manipulator); + anari::commitParameters(d, c); + + // Update viewport camera if requested + if (updateViewport) { + std::lock_guard lock(m_poseMutex); + m_currentPose = pose; + m_hasNewPose.store(true); + } + + // Setup output filename with prefix + std::ostringstream ss; + if (!capturedFilePrefix.empty()) { + ss << capturedFilePrefix << "_"; + } + ss << std::setfill('0') << std::setw(4) << frameIndex << ".png"; + std::filesystem::path filename = + std::filesystem::path(capturedOutputDirectory) / ss.str(); + savePass->setFilename(filename.string()); + + for (int sampleIdx = 0; sampleIdx < config.frame.samples; ++sampleIdx) { + savePass->setEnabled(sampleIdx == config.frame.samples - 1); + pipeline->render(); + } + frameIndex++; + + // Process SDL events to detect ESC key for cancellation + // We can't process full ImGui UI, but we can detect keyboard input + SDL_Event event; + while (SDL_PollEvent(&event)) { + if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE) { + m_cancelRequested = true; + tsd::core::logStatus("[CameraPoses] ESC pressed - cancelling..."); + } + if (event.type == SDL_EVENT_QUIT + || event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) { + m_cancelRequested = true; } + } - // Log renderer details at creation time - tsd::core::logStatus( - "[CameraPoses] Creating renderer in background thread:"); + // Log progress every 10 frames + if (frameIndex % 10 == 0 || frameIndex == capturedTotalFrames) { tsd::core::logStatus( - " activeRenderer index: %d", config.renderer.activeRenderer); - tsd::core::logStatus(" rendererObjects.size(): %zu", - config.renderer.rendererObjects.size()); - - auto &ro = - config.renderer.rendererObjects[config.renderer.activeRenderer]; - - tsd::core::logStatus(" Renderer subtype: '%s'", ro.subtype().c_str()); - tsd::core::logStatus(" Renderer name: '%s'", ro.name().c_str()); - - auto r = anari::newObject(d, ro.subtype().c_str()); - tsd::core::logStatus(" Created ANARI renderer object"); - - ro.updateAllANARIParameters(d, r); - tsd::core::logStatus(" Updated renderer parameters"); - - anari::commitParameters(d, r); - tsd::core::logStatus(" Committed renderer parameters"); - - // Create camera - auto c = anari::newObject(d, "perspective"); - anari::setParameter(d, - c, - "aspect", - static_cast(config.frame.width) / config.frame.height); - anari::setParameter(d, c, "fovy", anari::radians(40.f)); - anari::commitParameters(d, c); - - // Create render pipeline - auto pipeline = std::make_unique( - config.frame.width, config.frame.height); - - auto *anariPass = - pipeline->emplace_back(d); - anariPass->setRunAsync(false); - anariPass->setWorld(renderIndex->world()); - anariPass->setRenderer(r); - anariPass->setCamera(c); - - // Add AOV visualization pass if enabled - if (config.aov.aovType != tsd::rendering::AOVType::NONE) { - auto *aovPass = - pipeline->emplace_back(); - aovPass->setAOVType(config.aov.aovType); - aovPass->setDepthRange(config.aov.depthMin, config.aov.depthMax); - aovPass->setEdgeThreshold(config.aov.edgeThreshold); - aovPass->setEdgeInvert(config.aov.edgeInvert); - - // Enable necessary frame channels - if (config.aov.aovType == tsd::rendering::AOVType::ALBEDO) { - anariPass->setEnableAlbedo(true); - } else if (config.aov.aovType == tsd::rendering::AOVType::NORMAL) { - anariPass->setEnableNormals(true); - } else if (config.aov.aovType == tsd::rendering::AOVType::EDGES - || config.aov.aovType == tsd::rendering::AOVType::OBJECT_ID) { - anariPass->setEnableIDs(true); - } else if (config.aov.aovType - == tsd::rendering::AOVType::PRIMITIVE_ID) { - anariPass->setEnablePrimitiveId(true); - } else if (config.aov.aovType - == tsd::rendering::AOVType::INSTANCE_ID) { - anariPass->setEnableInstanceId(true); - } - } + "[CameraPoses] Rendered %d/%d frames (%.1f%%) - Press ESC to cancel", + frameIndex, + capturedTotalFrames, + (100.0f * frameIndex) / capturedTotalFrames); + } + } - auto *savePass = - pipeline->emplace_back(); - savePass->setSingleShotMode(false); - - // Render interpolated frames - int frameIndex = 0; - tsd::rendering::Manipulator manipulator; - - // Store original scene animation time to restore later - float originalTime = scene.getAnimationTime(); - - for (const auto &pose : samplesCopy) { - if (m_cancelRequested) { - tsd::core::logInfo( - "[CameraPoses] Rendering cancelled at frame %d/%d", - frameIndex, - capturedTotalFrames); - break; - } - - m_currentFrame = frameIndex; - - // Update scene animation time for animated fields (clouds, aurora) - // Map frameIndex to 0.0-1.0 range over the entire animation - float time = capturedTotalFrames > 1 - ? static_cast(frameIndex) / (capturedTotalFrames - 1) - : 0.0f; - scene.setAnimationTime(time); - - manipulator.setConfig(pose); - tsd::rendering::updateCameraParametersPerspective(d, c, manipulator); - anari::commitParameters(d, c); - - // Update viewport camera if requested - if (updateViewport) { - std::lock_guard lock(m_poseMutex); - m_currentPose = pose; - m_hasNewPose.store(true); - } - - // Setup output filename with prefix - std::ostringstream ss; - if (!capturedFilePrefix.empty()) { - ss << capturedFilePrefix << "_"; - } - ss << std::setfill('0') << std::setw(4) << frameIndex << ".png"; - std::filesystem::path filename = - std::filesystem::path(capturedOutputDirectory) / ss.str(); - savePass->setFilename(filename.string()); - - for (int sampleIdx = 0; sampleIdx < config.frame.samples; - ++sampleIdx) { - savePass->setEnabled(sampleIdx == config.frame.samples - 1); - pipeline->render(); - } - frameIndex++; - - // Process SDL events to detect ESC key for cancellation - // We can't process full ImGui UI, but we can detect keyboard input - SDL_Event event; - while (SDL_PollEvent(&event)) { - if (event.type == SDL_EVENT_KEY_DOWN && - event.key.key == SDLK_ESCAPE) { - m_cancelRequested = true; - tsd::core::logStatus("[CameraPoses] ESC pressed - cancelling..."); - } - if (event.type == SDL_EVENT_QUIT || - event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) { - m_cancelRequested = true; - } - } - - // Log progress every 10 frames - if (frameIndex % 10 == 0 || frameIndex == capturedTotalFrames) { - tsd::core::logStatus("[CameraPoses] Rendered %d/%d frames (%.1f%%) - Press ESC to cancel", - frameIndex, - capturedTotalFrames, - (100.0f * frameIndex) / capturedTotalFrames); - } - } + // Cleanup + scene.setAnimationTime(originalTime); // Restore original animation time + pipeline.reset(); + core->anari.releaseRenderIndex(d); + anari::release(d, c); + anari::release(d, r); + anari::release(d, d); + }; - // Cleanup - scene.setAnimationTime(originalTime); // Restore original animation time - pipeline.reset(); - core->anari.releaseRenderIndex(d); - anari::release(d, c); - anari::release(d, r); - anari::release(d, d); - }; - // Execute the rendering task synchronously renderTask(); - + // Mark rendering as complete m_isRendering = false; m_renderTimer.end(); - + if (m_cancelRequested) { tsd::core::logStatus("[CameraPoses] Rendering cancelled (%.2f seconds)", m_renderTimer.seconds()); From 0b8771722312093fbee2fd37ec2eb44fa30e94cc Mon Sep 17 00:00:00 2001 From: Cyrille Favreau Date: Tue, 3 Feb 2026 11:16:34 +0100 Subject: [PATCH 08/10] Revert CameraPoses.cpp to next_release version Remove synchronous rendering and animation time changes to keep CameraPoses aligned with base branch. --- tsd/src/tsd/ui/imgui/windows/CameraPoses.cpp | 400 +++++++++---------- 1 file changed, 183 insertions(+), 217 deletions(-) diff --git a/tsd/src/tsd/ui/imgui/windows/CameraPoses.cpp b/tsd/src/tsd/ui/imgui/windows/CameraPoses.cpp index ebf1ed465..242a41c00 100644 --- a/tsd/src/tsd/ui/imgui/windows/CameraPoses.cpp +++ b/tsd/src/tsd/ui/imgui/windows/CameraPoses.cpp @@ -15,8 +15,6 @@ #include "tsd/rendering/view/ManipulatorToAnari.hpp" // imgui #include -// SDL3 -#include // std #include #include @@ -280,24 +278,38 @@ void CameraPoses::buildUI_interpolationControls() // Update timer m_renderTimer.end(); - // Since rendering is now synchronous, this code path won't be reached - // until rendering is complete. Progress updates happen within the render - // loop. Calculate progress - float progress = 0.0f; - if (m_totalFrames > 0) { - progress = static_cast(m_currentFrame) - / static_cast(m_totalFrames); - } + // Check if rendering is complete + if (m_renderFuture.valid() + && m_renderFuture.wait_for(std::chrono::milliseconds(0)) + == std::future_status::ready) { + m_renderFuture.get(); + m_isRendering = false; + + if (m_cancelRequested) { + tsd::core::logStatus("[CameraPoses] Rendering cancelled (%.2f seconds)", + m_renderTimer.seconds()); + } else { + tsd::core::logStatus("[CameraPoses] Rendering complete (%.2f seconds)", + m_renderTimer.seconds()); + } + } else { + // Calculate progress + float progress = 0.0f; + if (m_totalFrames > 0) { + progress = static_cast(m_currentFrame) + / static_cast(m_totalFrames); + } - // Show progress bar with percentage - char progressText[64]; - snprintf(progressText, - sizeof(progressText), - "%d / %d frames (%.1f%%)", - m_currentFrame, - m_totalFrames, - progress * 100.0f); - ImGui::ProgressBar(progress, ImVec2(-1.0f, 0.0f), progressText); + // Show progress bar with percentage + char progressText[64]; + snprintf(progressText, + sizeof(progressText), + "%d / %d frames (%.1f%%)", + m_currentFrame, + m_totalFrames, + progress * 100.0f); + ImGui::ProgressBar(progress, ImVec2(-1.0f, 0.0f), progressText); + } } ImGui::Unindent(INDENT_AMOUNT); @@ -397,210 +409,164 @@ void CameraPoses::renderInterpolatedPath() // Calculate total frames for capture const int capturedTotalFrames = m_totalFrames; - // Execute rendering synchronously in main thread - // NOTE: This WILL block the UI during rendering, but it's safer than - // creating separate ANARI devices from different threads, which causes - // OptiX crashes. For async rendering with UI updates, a more complex - // solution would be needed (e.g., frame-by-frame execution with event loop). - auto renderTask = [this, - core, - samplesCopy, - capturedOutputDirectory, - capturedFilePrefix, - updateViewport, - capturedTotalFrames]() { - // Setup render pipeline - auto &config = core->offline; - auto d = core->anari.loadDevice(config.renderer.libraryName.c_str()); - if (!d) { - tsd::core::logError("[CameraPoses] Failed to load ANARI device"); - return; - } - - auto &scene = core->tsd.scene; - auto *renderIndex = core->anari.acquireRenderIndex(scene, d); - if (!renderIndex) { - tsd::core::logError("[CameraPoses] Failed to acquire render index"); - anari::release(d, d); - return; - } - - // Create renderer - if (config.renderer.rendererObjects.empty() - || config.renderer.activeRenderer < 0 - || config.renderer.activeRenderer - >= static_cast(config.renderer.rendererObjects.size())) { - tsd::core::logError("[CameraPoses] No renderer configured"); - anari::release(d, d); - return; - } - - // Log renderer details at creation time - tsd::core::logStatus( - "[CameraPoses] Creating renderer in background thread:"); - tsd::core::logStatus( - " activeRenderer index: %d", config.renderer.activeRenderer); - tsd::core::logStatus(" rendererObjects.size(): %zu", - config.renderer.rendererObjects.size()); - - auto &ro = config.renderer.rendererObjects[config.renderer.activeRenderer]; - - tsd::core::logStatus(" Renderer subtype: '%s'", ro.subtype().c_str()); - tsd::core::logStatus(" Renderer name: '%s'", ro.name().c_str()); - - auto r = anari::newObject(d, ro.subtype().c_str()); - tsd::core::logStatus(" Created ANARI renderer object"); - - ro.updateAllANARIParameters(d, r); - tsd::core::logStatus(" Updated renderer parameters"); - - anari::commitParameters(d, r); - tsd::core::logStatus(" Committed renderer parameters"); - - // Create camera - auto c = anari::newObject(d, "perspective"); - anari::setParameter(d, - c, - "aspect", - static_cast(config.frame.width) / config.frame.height); - anari::setParameter(d, c, "fovy", anari::radians(40.f)); - anari::commitParameters(d, c); - - // Create render pipeline - auto pipeline = std::make_unique( - config.frame.width, config.frame.height); - - auto *anariPass = - pipeline->emplace_back(d); - anariPass->setRunAsync(false); - anariPass->setWorld(renderIndex->world()); - anariPass->setRenderer(r); - anariPass->setCamera(c); - - // Add AOV visualization pass if enabled - if (config.aov.aovType != tsd::rendering::AOVType::NONE) { - auto *aovPass = - pipeline->emplace_back(); - aovPass->setAOVType(config.aov.aovType); - aovPass->setDepthRange(config.aov.depthMin, config.aov.depthMax); - aovPass->setEdgeThreshold(config.aov.edgeThreshold); - aovPass->setEdgeInvert(config.aov.edgeInvert); - - // Enable necessary frame channels - if (config.aov.aovType == tsd::rendering::AOVType::ALBEDO) { - anariPass->setEnableAlbedo(true); - } else if (config.aov.aovType == tsd::rendering::AOVType::NORMAL) { - anariPass->setEnableNormals(true); - } else if (config.aov.aovType == tsd::rendering::AOVType::EDGES - || config.aov.aovType == tsd::rendering::AOVType::OBJECT_ID) { - anariPass->setEnableIDs(true); - } else if (config.aov.aovType == tsd::rendering::AOVType::PRIMITIVE_ID) { - anariPass->setEnablePrimitiveId(true); - } else if (config.aov.aovType == tsd::rendering::AOVType::INSTANCE_ID) { - anariPass->setEnableInstanceId(true); - } - } - - auto *savePass = pipeline->emplace_back(); - savePass->setSingleShotMode(false); - - // Render interpolated frames - int frameIndex = 0; - tsd::rendering::Manipulator manipulator; - - // Store original scene animation time to restore later - float originalTime = scene.getAnimationTime(); - - for (const auto &pose : samplesCopy) { - if (m_cancelRequested) { - tsd::core::logInfo("[CameraPoses] Rendering cancelled at frame %d/%d", - frameIndex, - capturedTotalFrames); - break; - } - - m_currentFrame = frameIndex; - - // Map frameIndex to 0.0-1.0 range over the entire animation - float time = capturedTotalFrames > 1 - ? static_cast(frameIndex) / (capturedTotalFrames - 1) - : 0.0f; - scene.setAnimationTime(time); - - manipulator.setConfig(pose); - tsd::rendering::updateCameraParametersPerspective(d, c, manipulator); - anari::commitParameters(d, c); - - // Update viewport camera if requested - if (updateViewport) { - std::lock_guard lock(m_poseMutex); - m_currentPose = pose; - m_hasNewPose.store(true); - } + // Launch rendering in background + m_renderFuture = std::async(std::launch::async, + [this, + core, + samplesCopy, + capturedOutputDirectory, + capturedFilePrefix, + updateViewport, + capturedTotalFrames]() { + // Setup render pipeline + auto &config = core->offline; + auto d = core->anari.loadDevice(config.renderer.libraryName.c_str()); + if (!d) { + tsd::core::logError("[CameraPoses] Failed to load ANARI device"); + return; + } - // Setup output filename with prefix - std::ostringstream ss; - if (!capturedFilePrefix.empty()) { - ss << capturedFilePrefix << "_"; - } - ss << std::setfill('0') << std::setw(4) << frameIndex << ".png"; - std::filesystem::path filename = - std::filesystem::path(capturedOutputDirectory) / ss.str(); - savePass->setFilename(filename.string()); - - for (int sampleIdx = 0; sampleIdx < config.frame.samples; ++sampleIdx) { - savePass->setEnabled(sampleIdx == config.frame.samples - 1); - pipeline->render(); - } - frameIndex++; - - // Process SDL events to detect ESC key for cancellation - // We can't process full ImGui UI, but we can detect keyboard input - SDL_Event event; - while (SDL_PollEvent(&event)) { - if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE) { - m_cancelRequested = true; - tsd::core::logStatus("[CameraPoses] ESC pressed - cancelling..."); + auto &scene = core->tsd.scene; + auto *renderIndex = core->anari.acquireRenderIndex(scene, d); + if (!renderIndex) { + tsd::core::logError("[CameraPoses] Failed to acquire render index"); + anari::release(d, d); + return; } - if (event.type == SDL_EVENT_QUIT - || event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) { - m_cancelRequested = true; + + // Create renderer + if (config.renderer.rendererObjects.empty() + || config.renderer.activeRenderer < 0 + || config.renderer.activeRenderer + >= static_cast(config.renderer.rendererObjects.size())) { + tsd::core::logError("[CameraPoses] No renderer configured"); + anari::release(d, d); + return; } - } - // Log progress every 10 frames - if (frameIndex % 10 == 0 || frameIndex == capturedTotalFrames) { + // Log renderer details at creation time tsd::core::logStatus( - "[CameraPoses] Rendered %d/%d frames (%.1f%%) - Press ESC to cancel", - frameIndex, - capturedTotalFrames, - (100.0f * frameIndex) / capturedTotalFrames); - } - } + "[CameraPoses] Creating renderer in background thread:"); + tsd::core::logStatus( + " activeRenderer index: %d", config.renderer.activeRenderer); + tsd::core::logStatus(" rendererObjects.size(): %zu", + config.renderer.rendererObjects.size()); + + auto &ro = + config.renderer.rendererObjects[config.renderer.activeRenderer]; + + tsd::core::logStatus(" Renderer subtype: '%s'", ro.subtype().c_str()); + tsd::core::logStatus(" Renderer name: '%s'", ro.name().c_str()); + + auto r = anari::newObject(d, ro.subtype().c_str()); + tsd::core::logStatus(" Created ANARI renderer object"); + + ro.updateAllANARIParameters(d, r); + tsd::core::logStatus(" Updated renderer parameters"); + + anari::commitParameters(d, r); + tsd::core::logStatus(" Committed renderer parameters"); + + // Create camera + auto c = anari::newObject(d, "perspective"); + anari::setParameter(d, + c, + "aspect", + static_cast(config.frame.width) / config.frame.height); + anari::setParameter(d, c, "fovy", anari::radians(40.f)); + anari::commitParameters(d, c); + + // Create render pipeline + auto pipeline = std::make_unique( + config.frame.width, config.frame.height); + + auto *anariPass = + pipeline->emplace_back(d); + anariPass->setRunAsync(false); + anariPass->setWorld(renderIndex->world()); + anariPass->setRenderer(r); + anariPass->setCamera(c); + + // Add AOV visualization pass if enabled + if (config.aov.aovType != tsd::rendering::AOVType::NONE) { + auto *aovPass = + pipeline->emplace_back(); + aovPass->setAOVType(config.aov.aovType); + aovPass->setDepthRange(config.aov.depthMin, config.aov.depthMax); + aovPass->setEdgeThreshold(config.aov.edgeThreshold); + aovPass->setEdgeInvert(config.aov.edgeInvert); + + // Enable necessary frame channels + if (config.aov.aovType == tsd::rendering::AOVType::ALBEDO) { + anariPass->setEnableAlbedo(true); + } else if (config.aov.aovType == tsd::rendering::AOVType::NORMAL) { + anariPass->setEnableNormals(true); + } else if (config.aov.aovType == tsd::rendering::AOVType::EDGES + || config.aov.aovType == tsd::rendering::AOVType::OBJECT_ID) { + anariPass->setEnableIDs(true); + } else if (config.aov.aovType + == tsd::rendering::AOVType::PRIMITIVE_ID) { + anariPass->setEnablePrimitiveId(true); + } else if (config.aov.aovType + == tsd::rendering::AOVType::INSTANCE_ID) { + anariPass->setEnableInstanceId(true); + } + } - // Cleanup - scene.setAnimationTime(originalTime); // Restore original animation time - pipeline.reset(); - core->anari.releaseRenderIndex(d); - anari::release(d, c); - anari::release(d, r); - anari::release(d, d); - }; - - // Execute the rendering task synchronously - renderTask(); - - // Mark rendering as complete - m_isRendering = false; - m_renderTimer.end(); - - if (m_cancelRequested) { - tsd::core::logStatus("[CameraPoses] Rendering cancelled (%.2f seconds)", - m_renderTimer.seconds()); - } else { - tsd::core::logStatus("[CameraPoses] Rendering complete (%.2f seconds)", - m_renderTimer.seconds()); - } + auto *savePass = + pipeline->emplace_back(); + savePass->setSingleShotMode(false); + + // Render interpolated frames + int frameIndex = 0; + tsd::rendering::Manipulator manipulator; + + for (const auto &pose : samplesCopy) { + if (m_cancelRequested) { + tsd::core::logInfo( + "[CameraPoses] Rendering cancelled at frame %d/%d", + frameIndex, + capturedTotalFrames); + break; + } + + m_currentFrame = frameIndex; + manipulator.setConfig(pose); + tsd::rendering::updateCameraParametersPerspective(d, c, manipulator); + anari::commitParameters(d, c); + + // Update viewport camera if requested + if (updateViewport) { + std::lock_guard lock(m_poseMutex); + m_currentPose = pose; + m_hasNewPose.store(true); + } + + // Setup output filename with prefix + std::ostringstream ss; + if (!capturedFilePrefix.empty()) { + ss << capturedFilePrefix << "_"; + } + ss << std::setfill('0') << std::setw(4) << frameIndex << ".png"; + std::filesystem::path filename = + std::filesystem::path(capturedOutputDirectory) / ss.str(); + savePass->setFilename(filename.string()); + + for (int sampleIdx = 0; sampleIdx < config.frame.samples; + ++sampleIdx) { + savePass->setEnabled(sampleIdx == config.frame.samples - 1); + pipeline->render(); + } + frameIndex++; + } + + // Cleanup + pipeline.reset(); + core->anari.releaseRenderIndex(d); + anari::release(d, c); + anari::release(d, r); + anari::release(d, d); + }); } } // namespace tsd::ui::imgui From 0424b80bb5cbffffaac0248f7a4f251c1ad6b926 Mon Sep 17 00:00:00 2001 From: Cyrille Favreau Date: Tue, 3 Feb 2026 13:53:37 +0100 Subject: [PATCH 09/10] Fix tsd_tinygltf include path for stb_image.h The previous fix attempted to handle different stb_image variants but introduced a CI failure. Simplify to just use the stb_image target and its INTERFACE_INCLUDE_DIRECTORIES property directly. This ensures stb_image.h is found regardless of which stb_image implementation is being used (TSD's own or from an external source). --- tsd/external/tsd_tinygltf/CMakeLists.txt | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tsd/external/tsd_tinygltf/CMakeLists.txt b/tsd/external/tsd_tinygltf/CMakeLists.txt index 2494a2cac..ea12a9eec 100644 --- a/tsd/external/tsd_tinygltf/CMakeLists.txt +++ b/tsd/external/tsd_tinygltf/CMakeLists.txt @@ -6,17 +6,16 @@ project(tsd_ext_tinygltf) project_add_library(STATIC) project_include_directories(PUBLIC ${CMAKE_CURRENT_LIST_DIR}/include) project_include_directories(PRIVATE ${CMAKE_CURRENT_LIST_DIR}/src) -# Link to VisRTX's stb_image if available, otherwise use stb_image -if (TARGET visrtx_stb_image) - project_link_libraries(PRIVATE visrtx_stb_image) - project_include_directories(PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../../devices/rtx/external/stb_image) -elseif (TARGET stb_image) + +# Link to stb_image and set up include paths +if (TARGET stb_image) project_link_libraries(PRIVATE stb_image) - # For OWL's stb_image, headers are in stb/ subdirectory + # Get the include directory from stb_image target get_target_property(STB_IMAGE_INCLUDE_DIR stb_image INTERFACE_INCLUDE_DIRECTORIES) if (STB_IMAGE_INCLUDE_DIR) - project_include_directories(PRIVATE ${STB_IMAGE_INCLUDE_DIR}/stb) + project_include_directories(PRIVATE ${STB_IMAGE_INCLUDE_DIR}) endif() endif() + project_compile_definitions(PUBLIC TINYGLTF_NOEXCEPTION=1) project_sources(PRIVATE src/tiny_gltf.cc) From 5aa002df3d2e7679297c1c0238de7f4f348fb0f7 Mon Sep 17 00:00:00 2001 From: Cyrille Favreau Date: Tue, 3 Feb 2026 14:05:45 +0100 Subject: [PATCH 10/10] Revert tsd_tinygltf CMakeLists.txt to next_release version The stb_image conflict fixes should be in devices/rtx/external only, not in TSD's tsd_tinygltf. TSD builds its own stb_image and the link dependency is sufficient to propagate include directories. This fixes the CI failure where stb_image.h couldn't be found. --- tsd/external/tsd_tinygltf/CMakeLists.txt | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/tsd/external/tsd_tinygltf/CMakeLists.txt b/tsd/external/tsd_tinygltf/CMakeLists.txt index ea12a9eec..59c44c18d 100644 --- a/tsd/external/tsd_tinygltf/CMakeLists.txt +++ b/tsd/external/tsd_tinygltf/CMakeLists.txt @@ -6,16 +6,6 @@ project(tsd_ext_tinygltf) project_add_library(STATIC) project_include_directories(PUBLIC ${CMAKE_CURRENT_LIST_DIR}/include) project_include_directories(PRIVATE ${CMAKE_CURRENT_LIST_DIR}/src) - -# Link to stb_image and set up include paths -if (TARGET stb_image) - project_link_libraries(PRIVATE stb_image) - # Get the include directory from stb_image target - get_target_property(STB_IMAGE_INCLUDE_DIR stb_image INTERFACE_INCLUDE_DIRECTORIES) - if (STB_IMAGE_INCLUDE_DIR) - project_include_directories(PRIVATE ${STB_IMAGE_INCLUDE_DIR}) - endif() -endif() - +project_link_libraries(PRIVATE stb_image) project_compile_definitions(PUBLIC TINYGLTF_NOEXCEPTION=1) project_sources(PRIVATE src/tiny_gltf.cc)