From fc4766e749d26798f70d5200af6845e0a2ef35f0 Mon Sep 17 00:00:00 2001 From: Jason Batson Date: Sun, 6 Sep 2026 18:36:19 -0500 Subject: [PATCH 1/5] Support newer Houdini SDKs and optional ROCm 7 runtime Probe the selected HDK/USD APIs and preserve legacy branches while following its C++ standard, ABI, Python and OpenVDB requirements. Update the pinned RPR SDK and add an opt-in private HIP 7 loader independent of Houdini version. Convert modern Hydra implicit surfaces, honor the actual display-buffer format, and map Houdini default beauty to native color. Expose nonduplicate viewport sampling, device and resolution diagnostics. Validated build and live material rendering on Houdini 22.0.429/Linux with SDK 3.1.6.patch1, ROCm 7.2.4 and Radeon AI PRO R9700. The empty HIP override configures successfully; other compatibility paths are retained but untested. Document reproduction, observed HOM pause behavior and validation limits. --- BUILD_HOUDINI.md | 86 +++++++++++++++++++ CMakeLists.txt | 9 +- README.md | 4 +- cmake/defaults/ApiCapabilities.cmake | 61 +++++++++++++ cmake/defaults/Options.cmake | 4 + cmake/defaults/Packages.cmake | 19 ++++ cmake/defaults/apiCapabilities.h.in | 8 ++ cmake/modules/FindHoudiniUSD.cmake | 22 +++-- deps/CMakeLists.txt | 18 ++++ deps/RPR | 2 +- deps/hipRuntimeShim.c | 6 ++ pxr/imaging/plugin/hdRpr/CMakeLists.txt | 30 ++++++- .../hdRpr/implicitSurfaceSceneIndexPlugin.cpp | 35 ++++++++ .../hdRpr/implicitSurfaceSceneIndexPlugin.h | 20 +++++ .../plugin/hdRpr/ndrDiscoveryPlugin.cpp | 42 +++++++-- pxr/imaging/plugin/hdRpr/ndrParserPlugin.cpp | 36 +++++++- pxr/imaging/plugin/hdRpr/plugInfo.json | 62 ++++++------- pxr/imaging/plugin/hdRpr/renderDelegate.cpp | 20 ++++- pxr/imaging/plugin/hdRpr/rendererPlugin.h | 6 +- pxr/imaging/plugin/hdRpr/rprApi.cpp | 32 +++++-- pxr/imaging/plugin/hdRpr/rprApi.h | 4 + .../hdRpr/thirdparty/RPRLibs/pluginUtils.hpp | 9 ++ .../plugin/rprHoudini/UsdRenderers.json | 31 +++++-- .../plugin/rprHoudini/VOP_RPRMaterial.cpp | 4 +- .../plugin/rprHoudini/VOP_RPRMaterial.h | 14 ++- pxr/imaging/rprUsd/CMakeLists.txt | 1 + pxr/imaging/rprUsd/boostIncludePath.h | 12 +++ pxr/imaging/rprUsd/contextHelpers.cpp | 68 +++++++++++++++ .../materialNodes/rpr/arithmeticNode.cpp | 2 +- .../rprUsd/materialNodes/rpr/catcherNode.cpp | 2 +- .../materialNodes/rpr/combineShadersNode.cpp | 2 +- .../rprUsd/materialNodes/rpr/displaceNode.cpp | 2 +- .../materialNodes/rpr/materialXNode.cpp | 2 +- .../rprUsd/materialNodes/rpr/toonNode.cpp | 2 +- pxr/imaging/rprUsd/materialNodes/usdNode.cpp | 2 +- pxr/imaging/rprUsd/materialRegistry.h | 9 ++ pxr/imaging/rprUsd/wrapConfig.cpp | 12 +-- pxr/imaging/rprUsd/wrapContextHelpers.cpp | 10 +-- 38 files changed, 619 insertions(+), 91 deletions(-) create mode 100644 BUILD_HOUDINI.md create mode 100644 cmake/defaults/ApiCapabilities.cmake create mode 100644 cmake/defaults/apiCapabilities.h.in create mode 100644 deps/hipRuntimeShim.c create mode 100644 pxr/imaging/plugin/hdRpr/implicitSurfaceSceneIndexPlugin.cpp create mode 100644 pxr/imaging/plugin/hdRpr/implicitSurfaceSceneIndexPlugin.h diff --git a/BUILD_HOUDINI.md b/BUILD_HOUDINI.md new file mode 100644 index 000000000..b6ee49de7 --- /dev/null +++ b/BUILD_HOUDINI.md @@ -0,0 +1,86 @@ +# Building the Houdini port + +This branch adds capability-based compatibility with recent Houdini/USD SDKs +and an optional, private HIP 7 loader. Houdini 22 and ROCm 7 are supported +choices, not mandatory dependencies. Rebuild the plugin for each HDK/USD ABI. + +The tested workstation uses Houdini 22.0.429 on Linux, GCC 13.3, C++20, +libstdc++ ABI 1, Python 3.13, USD 26.05, MaterialX 1.39.5, OpenVDB 13, +Radeon ProRender SDK v3.1.6.patch1, and ROCm 7.2.4 on an AMD Radeon AI PRO +R9700. NVIDIA drives the desktop. This is local validation, not official +certification of that GPU or a claim that all older configurations were tested. + +| Configuration | Status | +|---|---| +| H22.0.429 / supplied USD / SDK 3.1.6.patch1 / optional HIP 7 loader | Compiles, loads, discovers the R9700 and renders in the GUI | +| Same H22 SDK with `RPR_ROCM7_ROOT` empty | Configure and API probes pass; default runtime not tested on this workstation | +| Older Houdini/USD/Python/OpenVDB and default SDK runtime | Legacy branches retained; not runtime tested | +| Other operating systems and standalone USD | Existing paths retained; not tested by this port | + +The build probes the selected headers for constructor macros, renderer creation, +Sdr discovery, implicit-surface scene indices, and typed HDK port indices. It +uses the C++ standard and Linux ABI exported by the chosen HDK; HDKs without +an ABI definition retain the older ABI-0 fallback. Python bindings and optional +USD libraries are discovered from that same installation. + +Use the pinned submodules, or explicitly provide an SDK checkout with its LFS +kernel files materialized. Houdini supplies MaterialX and OpenVDB for this build. +For example, from an environment configured for the selected Houdini install: + +```sh +cmake -S . -B build-houdini \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=/absolute/path/to/rpr-install \ + -DPYTHON_EXECUTABLE="$HFS/python/bin/python3.13" \ + -DRPR_LOCATION=/absolute/path/to/RadeonProRenderSDK/RadeonProRender \ + -DRPR_NS_KERNELS_DIR=/absolute/path/to/RadeonProRenderSDK/hipbin \ + -DRPR_SDK_PLATFORM=ubuntu20.04 \ + -DPXR_BUILD_TESTS=OFF -DPXR_USE_PYTHON_3=ON +cmake --build build-houdini -j4 +cmake --install build-houdini +``` + +Select the Python executable supplied by your Houdini version. Houdini UI +settings generation imports `hou`; our single-engine session ran that generator +inside the existing GUI, while USD schemas used ordinary bundled Python. +Do not replace installed shared libraries while Houdini has them loaded. + +`RPR_ROCM7_ROOT` is empty by default. To opt into an installed HIP 7 runtime, +configure with `-DRPR_ROCM7_ROOT=/absolute/path/to/rocm-7`. This Linux-only option +builds `lib/hip7-loader/libamdhip64.so.6`, which depends on that installation's +HIP 7 library. Set `RPRUSD_HIP_LIBRARY` to the absolute path of this private +loader at runtime. It does not replace system libraries or select a Houdini +version. Leave both options unset to preserve the SDK's default selection. + +The explicit loader uses `RTLD_LOCAL`; global HIP symbols collide with +Northstar/Orochi function-pointer variables. On this path, GPU discovery is +bounded by the device count from HIP because probing a nonexistent device +crashes the tested SDK. Explicit load failures are reported, not silently +converted into another backend. + +Add the installed `houdini` directory to `HOUDINI_PATH` and `lib/python` to +`PYTHONPATH` through a Houdini package. The installed `houdini/dso/usd_plugins` +entry discovers the delegate. Restart Houdini after package installation, then +select RPR in Solaris. Scene render controls are on the generic Render Settings +LOP's RPR tab; the upstream `rpr_lop_rendererSettings` operator is not registered. + +Runtime checks should cover a mesh, an analytic USD sphere, bound glass and +emissive materials, and an authored Render Settings prim. Houdini can provide a +four-channel display buffer for a three-channel RenderVar: conversion follows +the actual buffer format. Houdini's default `C.*[LO]` beauty expression maps to +the native color AOV, alongside the existing `C.*` alias. Other custom LPEs +retain their separate path and are not broadly validated here. The scene-index +plugin requires an integer priority +in its metadata, even when registered in C++. + +The HUD exposes samples/cap, output resolution, preview scale, render GPU, +CPU render threads and samples/second. Native viewport FPS measures drawing, +not fully sampled animation throughput. Preview downscale 1 means half width +and height while interacting; 0 means full resolution. Still sample limits +and interaction resolution are separate controls. Adaptive convergence may +finish before reaching the maximum sample count. + +In H22.0.429, repeated `hou.SceneViewer.setRendererPaused(False)` calls toggled +the renderer instead of acting idempotently. Check `isRendererPaused()` first +and call the setter only if the state differs. This workaround also works with +normal setters; it should not replace the user's deliberate pause choice. diff --git a/CMakeLists.txt b/CMakeLists.txt index 93b33ebce..3c53f184a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,8 +1,10 @@ -project(hdRpr) - cmake_minimum_required(VERSION 3.15) -set(CMAKE_CXX_STANDARD 17) +project(hdRpr) + +if(NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 17) +endif() set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/defaults @@ -12,6 +14,7 @@ set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/defaults include(Options) include(ProjectDefaults) include(Packages) +include(ApiCapabilities) # This has to be defined after Packages is included, because it relies on the # discovered path to the python executable. diff --git a/README.md b/README.md index bd000868d..39f3651ca 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ This plugin allows fast GPU or CPU accelerated viewport rendering on all OpenCL You can build this plugin as usdview plugin or as houdini plugin. +See [Building the Houdini port](BUILD_HOUDINI.md) for the tested H22 setup, passive older compatibility, and optional HIP 7 runtime selection. + For more details on USD, please visit the web site [here](http://openusd.org). Getting and Building the Code @@ -56,7 +58,7 @@ Provide USD in one of two ways: ##### MaterialX Component By default, MaterialX library will be compiled from the sources located under a MaterialX submodule `deps/MaterialX`. -You can override this behavior by providing a complete build of MaterialX to cmake. Please note, on Linux for Houdini plugin, MaterialX should be compiled with `-D_GLIBCXX_USE_CXX11_ABI=0` definition as it is required by Houdini. +You can override this behavior by providing a complete build of MaterialX to cmake. On Linux, MaterialX must use the C++ ABI required by the selected Houdini HDK. Older builds use ABI 0; the tested H22 SDK uses ABI 1. Prefer the matching MaterialX shipped with that Houdini installation. ##### Optional Components diff --git a/cmake/defaults/ApiCapabilities.cmake b/cmake/defaults/ApiCapabilities.cmake new file mode 100644 index 000000000..5c4613db7 --- /dev/null +++ b/cmake/defaults/ApiCapabilities.cmake @@ -0,0 +1,61 @@ +# Detect APIs from the selected SDK, including vendor backports. No Houdini +# or renderer process is launched by these compile-only checks. +include(CheckCXXSourceCompiles) +include(CMakePushCheckState) +cmake_push_check_state(RESET) +set(_rpr_saved_try_compile_target_type ${CMAKE_TRY_COMPILE_TARGET_TYPE}) +set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) +if(USDMonolithic_FOUND) + set(CMAKE_REQUIRED_LIBRARIES usd_monolithic) +else() + set(CMAKE_REQUIRED_LIBRARIES tf hd) +endif() +check_cxx_source_compiles(" + #include + ARCH_CONSTRUCTOR(rpr_probe, 255) {} +" RPR_USD_ARCH_CONSTRUCTOR_TWO_ARGS) +check_cxx_source_compiles(" + #include + PXR_NAMESPACE_USING_DIRECTIVE + bool probe(const HdRendererPlugin& p, const HdRendererCreateArgs& args) { + return p.IsSupported(args, nullptr); + } +" RPR_USD_RENDERER_CREATE_ARGS) +check_cxx_source_compiles(" + #include +" RPR_USD_SDR_DISCOVERY) +if(TARGET hdsi OR USDMonolithic_FOUND) + check_cxx_source_compiles(" + #include + #include + #include + PXR_NAMESPACE_USING_DIRECTIVE + class Probe : public HdSceneIndexPlugin { + HdSceneIndexBaseRefPtr _AppendSceneIndex( + const HdSceneIndexBaseRefPtr& scene, + const HdContainerDataSourceHandle& args) override { + return HdsiImplicitSurfaceSceneIndex::New(scene, args); + } + }; + " RPR_USD_IMPLICIT_SURFACE_SCENE_INDEX) +endif() +if(TARGET Houdini) + # Houdini is a non-imported interface target and try_compile cannot copy it. + get_target_property(CMAKE_REQUIRED_INCLUDES Houdini INTERFACE_INCLUDE_DIRECTORIES) + set(CMAKE_REQUIRED_LIBRARIES tf) + get_target_property(_rpr_hdk_defs Houdini INTERFACE_COMPILE_DEFINITIONS) + foreach(_def IN LISTS _rpr_hdk_defs) + if(NOT _def MATCHES "[$]<") + list(APPEND CMAKE_REQUIRED_DEFINITIONS "-D${_def}") + endif() + endforeach() + check_cxx_source_compiles(" + #include + auto input = static_cast(&OP_Node::inputLabel); + auto output = static_cast(&OP_Node::outputLabel); + " RPR_HOUDINI_TYPED_PORT_INDICES) +endif() +set(CMAKE_TRY_COMPILE_TARGET_TYPE ${_rpr_saved_try_compile_target_type}) +cmake_pop_check_state() +configure_file(${PROJECT_SOURCE_DIR}/cmake/defaults/apiCapabilities.h.in + ${PROJECT_BINARY_DIR}/apiCapabilities.h) diff --git a/cmake/defaults/Options.cmake b/cmake/defaults/Options.cmake index be2a6530e..b0bef2fb2 100644 --- a/cmake/defaults/Options.cmake +++ b/cmake/defaults/Options.cmake @@ -93,3 +93,7 @@ option(MATERIALX_BUILD_PYTHON "Build the MaterialX Python package from C++ bindi option(MATERIALX_INSTALL_PYTHON "Install the MaterialX Python package as a third-party library when the install target is built." OFF) option(USE_USDSHADE_MTLX "Use UsdShade during material parsing" ON) + +# Optional Linux loader bridge for SDKs that search HIP 6 sonames before HIP 7. +# Independent of the host application's version; empty preserves SDK defaults. +set(RPR_ROCM7_ROOT "" CACHE PATH "Optional ROCm 7 installation for a private HIP loader") diff --git a/cmake/defaults/Packages.cmake b/cmake/defaults/Packages.cmake index 0297547d8..bc412ee80 100644 --- a/cmake/defaults/Packages.cmake +++ b/cmake/defaults/Packages.cmake @@ -105,6 +105,25 @@ if(HoudiniUSD_FOUND) endif() find_package(Houdini REQUIRED CONFIG PATHS ${HOUDINI_CONFIG_DIR}) + get_target_property(_houdini_options Houdini INTERFACE_COMPILE_OPTIONS) + string(REGEX MATCH "-std=c\\+\\+([0-9]+)" _houdini_standard "${_houdini_options}") + if(_houdini_standard AND CMAKE_MATCH_1 GREATER CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD ${CMAKE_MATCH_1}) + endif() + get_target_property(_houdini_definitions Houdini INTERFACE_COMPILE_DEFINITIONS) + set(_houdini_abi_found FALSE) + foreach(_definition IN LISTS _houdini_definitions) + if(_definition MATCHES "^_GLIBCXX_USE_CXX11_ABI=") + add_compile_definitions(${_definition}) + set(_houdini_abi_found TRUE) + endif() + endforeach() + + if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND NOT _houdini_abi_found) + # Older HDKs do not export this definition; retain their original ABI. + add_compile_definitions(_GLIBCXX_USE_CXX11_ABI=0) + endif() + set(OPENEXR_LOCATION ${Houdini_USD_INCLUDE_DIR}) set(OPENEXR_LIB_LOCATION ${Houdini_LIB_DIR}) else() diff --git a/cmake/defaults/apiCapabilities.h.in b/cmake/defaults/apiCapabilities.h.in new file mode 100644 index 000000000..eeb06f00b --- /dev/null +++ b/cmake/defaults/apiCapabilities.h.in @@ -0,0 +1,8 @@ +#ifndef RPRUSD_API_CAPABILITIES_H +#define RPRUSD_API_CAPABILITIES_H +#cmakedefine01 RPR_USD_ARCH_CONSTRUCTOR_TWO_ARGS +#cmakedefine01 RPR_USD_RENDERER_CREATE_ARGS +#cmakedefine01 RPR_USD_SDR_DISCOVERY +#cmakedefine01 RPR_HOUDINI_TYPED_PORT_INDICES +#cmakedefine01 RPR_USD_IMPLICIT_SURFACE_SCENE_INDEX +#endif diff --git a/cmake/modules/FindHoudiniUSD.cmake b/cmake/modules/FindHoudiniUSD.cmake index 24c4aa858..ed7ba58b6 100644 --- a/cmake/modules/FindHoudiniUSD.cmake +++ b/cmake/modules/FindHoudiniUSD.cmake @@ -41,7 +41,7 @@ endif(APPLE) set(Houdini_Python_VARS Houdini_Python_INCLUDE_DIR Houdini_Python_LIB Houdini_Boostpython_LIB) list(APPEND HUSD_REQ_VARS ${Houdini_Python_VARS}) -foreach(python_major_minor "3;12" "3;11" "3;10" "3;9" "3;7" "2;7") +foreach(python_major_minor "3;13" "3;12" "3;11" "3;10" "3;9" "3;7" "2;7") list(GET python_major_minor 0 py_major) list(GET python_major_minor 1 py_minor) @@ -73,6 +73,9 @@ foreach(python_major_minor "3;12" "3;11" "3;10" "3;9" "3;7" "2;7") find_file( Houdini_Boostpython_LIB + NAMES + "libpxr_python${CMAKE_SHARED_LIBRARY_SUFFIX}" # Modern USD vendored Boost.Python + "libpxr_python.lib" # Windows "libhboost_python-mt${CMAKE_SHARED_LIBRARY_SUFFIX}" # Unix "libhboost_python${py_major}${py_minor}-mt-x64${CMAKE_SHARED_LIBRARY_SUFFIX}" # Unix "libhboost_python${py_major}${py_minor}-mt-a64${CMAKE_SHARED_LIBRARY_SUFFIX}" # MacOS ARM @@ -105,7 +108,9 @@ if(NOT HYTHON_EXECUTABLE) message(FATAL "Could not find hython executable") endif() list(APPEND HUSD_REQ_VARS "HYTHON_EXECUTABLE") -set(PYTHON_EXECUTABLE ${HYTHON_EXECUTABLE}) +if(NOT PYTHON_EXECUTABLE) + set(PYTHON_EXECUTABLE ${HYTHON_EXECUTABLE}) +endif() if(WIN32) set(Houdini_LIB_DIR ${HOUDINI_ROOT}/custom/houdini/dsolib) @@ -179,9 +184,12 @@ if(HoudiniUSD_FOUND AND NOT TARGET hd) foreach(targetName arch tf gf js trace work plug vt ar kind sdf ndr sdr pcp usd usdGeom usdVol usdLux usdMedia usdShade usdRender usdHydra usdRi usdSkel usdUI - usdUtils garch hf hio cameraUtil pxOsd glf hgi hgiGL hd hdSt hdMtlx hdx - usdImaging usdImagingGL usdRiImaging usdSkelImaging usdVolI maging + usdUtils garch hf hio cameraUtil pxOsd glf hgi hgiGL hd hdSt hdsi hdMtlx hdx + usdImaging usdImagingGL usdRiImaging usdSkelImaging usdVolImaging usdAppUtils usdviewq) + if(NOT EXISTS "${Houdini_USD_LIB_DIR}/libpxr_${targetName}${CMAKE_SHARED_LIBRARY_SUFFIX}") + continue() + endif() add_library(${targetName} SHARED IMPORTED) set_target_properties(${targetName} PROPERTIES IMPORTED_LOCATION "${Houdini_USD_LIB_DIR}/libpxr_${targetName}${CMAKE_SHARED_LIBRARY_SUFFIX}" @@ -199,10 +207,6 @@ if(HoudiniUSD_FOUND AND NOT TARGET hd) INTERFACE "${HOUDINI_ROOT}/custom/houdini/dsolib") endif() endforeach() - if(CMAKE_SYSTEM_NAME STREQUAL "Linux") - # Houdini builds with the old ABI. We need to match. - add_definitions(-D_GLIBCXX_USE_CXX11_ABI=0) - endif() # Add python to tf, usdImagingGL targets. target_include_directories(tf INTERFACE ${Houdini_Python_INCLUDE_DIR}) target_link_libraries(tf INTERFACE ${Houdini_Python_LIB}) @@ -228,6 +232,6 @@ if(HoudiniUSD_FOUND AND NOT TARGET hd) REQUIRED NO_DEFAULT_PATH) if(USD_SCHEMA_GENERATOR) - set(USD_SCHEMA_GENERATOR ${HOUDINI_BIN}/hython ${USD_SCHEMA_GENERATOR}) + set(USD_SCHEMA_GENERATOR ${PYTHON_EXECUTABLE} ${USD_SCHEMA_GENERATOR}) endif() endif() diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 8657e6d32..d483df21f 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -89,3 +89,21 @@ add_library(murmurhash STATIC ${MURMURHASH_DIR}/MurmurHash3.cpp) target_include_directories(murmurhash PUBLIC ${MURMURHASH_DIR}) set_target_properties(murmurhash PROPERTIES POSITION_INDEPENDENT_CODE ON) + +# This optional shim never enters the default library search path. The caller +# explicitly selects it with RPRUSD_HIP_LIBRARY before creating an RPR context. +if(RPR_ROCM7_ROOT) + if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux") + message(FATAL_ERROR "RPR_ROCM7_ROOT is a Linux-only optional loader") + endif() + find_library(RPR_HIP7_LIBRARY NAMES libamdhip64.so.7 + PATHS "${RPR_ROCM7_ROOT}/lib" "${RPR_ROCM7_ROOT}/lib64" + NO_DEFAULT_PATH REQUIRED) + get_filename_component(_rpr_hip7_libdir "${RPR_HIP7_LIBRARY}" DIRECTORY) + add_library(rprHipRuntimeShim SHARED hipRuntimeShim.c) + target_link_libraries(rprHipRuntimeShim PRIVATE "${RPR_HIP7_LIBRARY}") + set_target_properties(rprHipRuntimeShim PROPERTIES + PREFIX "lib" OUTPUT_NAME amdhip64 SOVERSION 6 + INSTALL_RPATH "${_rpr_hip7_libdir}") + install(TARGETS rprHipRuntimeShim LIBRARY DESTINATION lib/hip7-loader) +endif() diff --git a/deps/RPR b/deps/RPR index 40cfdbcb7..bdf485a8c 160000 --- a/deps/RPR +++ b/deps/RPR @@ -1 +1 @@ -Subproject commit 40cfdbcb7362927f833a2ee09458a63f029ee69d +Subproject commit bdf485a8c4f25ef50c187deb3f7e81c166f24fa0 diff --git a/deps/hipRuntimeShim.c b/deps/hipRuntimeShim.c new file mode 100644 index 000000000..661ae45a4 --- /dev/null +++ b/deps/hipRuntimeShim.c @@ -0,0 +1,6 @@ +/* The dependency supplies the HIP API. The legacy soname lets an older SDK + * loader find an explicitly selected HIP 7 runtime without system symlinks. */ +extern int hipRuntimeGetVersion(int *version); +int rprSelectedHipRuntimeVersion(int *version) { + return hipRuntimeGetVersion(version); +} diff --git a/pxr/imaging/plugin/hdRpr/CMakeLists.txt b/pxr/imaging/plugin/hdRpr/CMakeLists.txt index 9b5ddfeba..db84d7cfa 100644 --- a/pxr/imaging/plugin/hdRpr/CMakeLists.txt +++ b/pxr/imaging/plugin/hdRpr/CMakeLists.txt @@ -15,6 +15,24 @@ if(OpenVDB_FOUND) set(OptClass ${OptClass} field volume) endif(OpenVDB_FOUND) +set(RPR_SCENE_INDEX_PLUGIN_INFO "") +if(RPR_USD_IMPLICIT_SURFACE_SCENE_INDEX) + list(APPEND OptClass implicitSurfaceSceneIndexPlugin) + if(TARGET hdsi) + list(APPEND OptLibs hdsi) + else() + list(APPEND OptLibs usd_monolithic) + endif() + set(RPR_SCENE_INDEX_PLUGIN_INFO [=[ + "HdRprImplicitSurfaceSceneIndexPlugin": { + "bases": ["HdSceneIndexPlugin"], + "loadWithRenderer": "RPR", + "priority": 0, + "displayName": "RPR Implicit Surface Conversion" + }, +]=]) +endif() + find_package(OpenMP) if (OPENMP_FOUND) set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OpenMP_C_FLAGS}") @@ -88,6 +106,16 @@ file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/rif_models.version ${RIF_VERSION_STRING}) list(APPEND RIF_MODEL_RESOURCE_FILES "${CMAKE_CURRENT_BINARY_DIR}/rif_models.version${_sep}rif_models/rif_models.version") +if(RPR_USD_SDR_DISCOVERY) + set(RPR_DISCOVERY_BASE SdrDiscoveryPlugin) + set(RPR_PARSER_BASE SdrParserPlugin) + set(RPR_DISCOVERY_LIB sdr) +else() + set(RPR_DISCOVERY_BASE NdrDiscoveryPlugin) + set(RPR_PARSER_BASE NdrParserPlugin) + set(RPR_DISCOVERY_LIB ndr) +endif() + pxr_plugin(hdRpr DISABLE_PRECOMPILED_HEADERS @@ -98,7 +126,7 @@ pxr_plugin(hdRpr tf gf hf - ndr + ${RPR_DISCOVERY_LIB} usdVol usdLux usdUtils diff --git a/pxr/imaging/plugin/hdRpr/implicitSurfaceSceneIndexPlugin.cpp b/pxr/imaging/plugin/hdRpr/implicitSurfaceSceneIndexPlugin.cpp new file mode 100644 index 000000000..e1e2d5ca9 --- /dev/null +++ b/pxr/imaging/plugin/hdRpr/implicitSurfaceSceneIndexPlugin.cpp @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "implicitSurfaceSceneIndexPlugin.h" +#include "pxr/imaging/hd/retainedDataSource.h" +#include "pxr/imaging/hd/sceneIndexPluginRegistry.h" +#include "pxr/imaging/hdsi/implicitSurfaceSceneIndex.h" + +PXR_NAMESPACE_OPEN_SCOPE + +TF_REGISTRY_FUNCTION(TfType) { + HdSceneIndexPluginRegistry::Define(); +} + +TF_REGISTRY_FUNCTION(HdSceneIndexPlugin) { + HdSceneIndexPluginRegistry::GetInstance().RegisterSceneIndexForRenderer( + "RPR", TfToken("HdRprImplicitSurfaceSceneIndexPlugin"), nullptr, 0, + HdSceneIndexPluginRegistry::InsertionOrderAtStart); +} + +HdSceneIndexBaseRefPtr +HdRprImplicitSurfaceSceneIndexPlugin::_AppendSceneIndex( + const HdSceneIndexBaseRefPtr& inputScene, + const HdContainerDataSourceHandle& inputArgs) { + static const auto args = []() { + // String tokens keep this compatible with SDKs predating the plane token. + const TfToken names[] = {TfToken("sphere"), TfToken("cube"), + TfToken("cone"), TfToken("cylinder"), TfToken("capsule"), TfToken("plane")}; + const auto toMesh = HdRetainedTypedSampledDataSource::New( + HdsiImplicitSurfaceSceneIndexTokens->toMesh); + HdDataSourceBaseHandle values[6] = {toMesh,toMesh,toMesh,toMesh,toMesh,toMesh}; + return HdRetainedContainerDataSource::New(6, names, values); + }(); + return HdsiImplicitSurfaceSceneIndex::New(inputScene, args); +} + +PXR_NAMESPACE_CLOSE_SCOPE diff --git a/pxr/imaging/plugin/hdRpr/implicitSurfaceSceneIndexPlugin.h b/pxr/imaging/plugin/hdRpr/implicitSurfaceSceneIndexPlugin.h new file mode 100644 index 000000000..25c2a46d9 --- /dev/null +++ b/pxr/imaging/plugin/hdRpr/implicitSurfaceSceneIndexPlugin.h @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +#ifndef HDRPR_IMPLICIT_SURFACE_SCENE_INDEX_PLUGIN_H +#define HDRPR_IMPLICIT_SURFACE_SCENE_INDEX_PLUGIN_H + +#include "pxr/imaging/hd/sceneIndexPlugin.h" + +PXR_NAMESPACE_OPEN_SCOPE + +// Hydra's scene-index path preserves analytic primitives. hdRpr consumes meshes. +class HdRprImplicitSurfaceSceneIndexPlugin : public HdSceneIndexPlugin { +public: + HdRprImplicitSurfaceSceneIndexPlugin() = default; +protected: + HdSceneIndexBaseRefPtr _AppendSceneIndex( + const HdSceneIndexBaseRefPtr& inputScene, + const HdContainerDataSourceHandle& inputArgs) override; +}; + +PXR_NAMESPACE_CLOSE_SCOPE +#endif diff --git a/pxr/imaging/plugin/hdRpr/ndrDiscoveryPlugin.cpp b/pxr/imaging/plugin/hdRpr/ndrDiscoveryPlugin.cpp index f33ea34e5..a2d3e9702 100644 --- a/pxr/imaging/plugin/hdRpr/ndrDiscoveryPlugin.cpp +++ b/pxr/imaging/plugin/hdRpr/ndrDiscoveryPlugin.cpp @@ -11,7 +11,13 @@ See the License for the specific language governing permissions and limitations under the License. ************************************************************************/ +#include "pxr/imaging/rprUsd/apiCapabilities.h" + +#if RPR_USD_SDR_DISCOVERY +#include "pxr/usd/sdr/discoveryPlugin.h" +#else #include "pxr/usd/ndr/discoveryPlugin.h" +#endif #include "pxr/imaging/rprUsd/materialRegistry.h" PXR_NAMESPACE_OPEN_SCOPE @@ -20,18 +26,36 @@ PXR_NAMESPACE_OPEN_SCOPE /// /// Enumerates materials from RprUsdMaterialRegistry. /// -class HdRprNdrDiscoveryPlugin final : public NdrDiscoveryPlugin { +#if RPR_USD_SDR_DISCOVERY +using RprDiscoveryBase = SdrDiscoveryPlugin; +using RprDiscoveryResults = SdrShaderNodeDiscoveryResultVec; +using RprDiscoveryVersion = SdrVersion; +using RprDiscoveryMetadata = SdrTokenMap; +using RprSearchURIs = SdrStringVec; +#else +using RprDiscoveryBase = NdrDiscoveryPlugin; +using RprDiscoveryResults = NdrNodeDiscoveryResultVec; +using RprDiscoveryVersion = NdrVersion; +using RprDiscoveryMetadata = NdrTokenMap; +using RprSearchURIs = NdrStringVec; +#endif + +class HdRprNdrDiscoveryPlugin final : public RprDiscoveryBase { public: - NdrNodeDiscoveryResultVec DiscoverNodes(const Context& ctx) override { +#if RPR_USD_SDR_DISCOVERY + RprDiscoveryResults DiscoverShaderNodes(const Context& ctx) override { +#else + RprDiscoveryResults DiscoverNodes(const Context& ctx) override { +#endif static TfToken rpr("rpr", TfToken::Immortal); - NdrNodeDiscoveryResultVec ret; + RprDiscoveryResults ret; for (auto& nodeDesc : RprUsdMaterialRegistry::GetInstance().GetRegisteredNodes()) { if (!nodeDesc.info) continue; ret.emplace_back( /* identifier = */ TfToken(nodeDesc.info->GetName()), - /* version = */ NdrVersion(1), + /* version = */ RprDiscoveryVersion(1), /* name = */ nodeDesc.info->GetName(), /* family = */ TfToken(nodeDesc.info->GetUIFolder()), /* discoveryType = */ rpr, @@ -39,19 +63,23 @@ class HdRprNdrDiscoveryPlugin final : public NdrDiscoveryPlugin { /* uri = */ std::string(), /* resolvedUri = */ std::string(), /* sourceCode = */ std::string(), - /* metadata = */ NdrTokenMap(), + /* metadata = */ RprDiscoveryMetadata(), /* blindData = */ std::string() ); } return ret; } - const NdrStringVec& GetSearchURIs() const override { - static NdrStringVec s_searchURIs; + const RprSearchURIs& GetSearchURIs() const override { + static RprSearchURIs s_searchURIs; return s_searchURIs; } }; +#if RPR_USD_SDR_DISCOVERY +SDR_REGISTER_DISCOVERY_PLUGIN(HdRprNdrDiscoveryPlugin); +#else NDR_REGISTER_DISCOVERY_PLUGIN(HdRprNdrDiscoveryPlugin); +#endif PXR_NAMESPACE_CLOSE_SCOPE diff --git a/pxr/imaging/plugin/hdRpr/ndrParserPlugin.cpp b/pxr/imaging/plugin/hdRpr/ndrParserPlugin.cpp index d2db06db7..457da5f64 100644 --- a/pxr/imaging/plugin/hdRpr/ndrParserPlugin.cpp +++ b/pxr/imaging/plugin/hdRpr/ndrParserPlugin.cpp @@ -11,9 +11,17 @@ See the License for the specific language governing permissions and limitations under the License. ************************************************************************/ +#include "pxr/imaging/rprUsd/apiCapabilities.h" + +#if RPR_USD_SDR_DISCOVERY +#include "pxr/usd/sdr/shaderNode.h" +#include "pxr/usd/sdr/parserPlugin.h" +#include "pxr/usd/sdr/shaderNodeDiscoveryResult.h" +#else #include "pxr/usd/ndr/node.h" #include "pxr/usd/ndr/parserPlugin.h" #include "pxr/usd/ndr/nodeDiscoveryResult.h" +#endif #include "pxr/base/tf/staticTokens.h" @@ -25,8 +33,24 @@ PXR_NAMESPACE_OPEN_SCOPE /// /// This does the minimal amount of work so Hydra will let us have our shaders. /// -class HdRprNdrParserPlugin final : public NdrParserPlugin { +#if RPR_USD_SDR_DISCOVERY +using RprParserBase = SdrParserPlugin; +using RprParserTypes = SdrTokenVec; +#else +using RprParserBase = NdrParserPlugin; +using RprParserTypes = NdrTokenVec; +#endif + +class HdRprNdrParserPlugin final : public RprParserBase { public: +#if RPR_USD_SDR_DISCOVERY + SdrShaderNodeUniquePtr ParseShaderNode(const SdrShaderNodeDiscoveryResult& d) override { + return std::make_unique( + d.identifier, d.version, d.name, d.family, + RprUsdTokens->rpr, RprUsdTokens->rpr, d.uri, d.resolvedUri, + SdrShaderPropertyUniquePtrVec{}, d.metadata, d.sourceCode); + } +#else NdrNodeUniquePtr Parse(const NdrNodeDiscoveryResult& discoveryResult) override { return std::make_unique( /* identifier = */ discoveryResult.identifier, @@ -45,8 +69,10 @@ class HdRprNdrParserPlugin final : public NdrParserPlugin { ); } - const NdrTokenVec& GetDiscoveryTypes() const override { - static NdrTokenVec s_discoveryTypes{RprUsdTokens->rpr}; +#endif + + const RprParserTypes& GetDiscoveryTypes() const override { + static RprParserTypes s_discoveryTypes{RprUsdTokens->rpr}; return s_discoveryTypes; } @@ -55,6 +81,10 @@ class HdRprNdrParserPlugin final : public NdrParserPlugin { } }; +#if RPR_USD_SDR_DISCOVERY +SDR_REGISTER_PARSER_PLUGIN(HdRprNdrParserPlugin); +#else NDR_REGISTER_PARSER_PLUGIN(HdRprNdrParserPlugin); +#endif PXR_NAMESPACE_CLOSE_SCOPE diff --git a/pxr/imaging/plugin/hdRpr/plugInfo.json b/pxr/imaging/plugin/hdRpr/plugInfo.json index 5fae003f0..1011eeb76 100644 --- a/pxr/imaging/plugin/hdRpr/plugInfo.json +++ b/pxr/imaging/plugin/hdRpr/plugInfo.json @@ -1,31 +1,31 @@ -{ - "Plugins": [ - { - "Info": { - "Types": { - "HdRprPlugin": { - "bases": [ - "HdRendererPlugin" - ], - "displayName": "RPR", - "priority": 99 - }, - "HdRprNdrDiscoveryPlugin": { - "bases": ["NdrDiscoveryPlugin"], - "displayName": "RPR Node Discovery" - }, - "HdRprNdrParserPlugin": { - "bases": ["NdrParserPlugin"], - "displayName": "RPR Node Parser" - } - } - }, - "LibraryPath": "@PLUG_INFO_LIBRARY_PATH@", - "Name": "hdRpr", - "ResourcePath": "@PLUG_INFO_RESOURCE_PATH@", - "Root": "@PLUG_INFO_ROOT@", - "Type": "library" - } - ] -} - +{ + "Plugins": [ + { + "Info": { + "Types": { +@RPR_SCENE_INDEX_PLUGIN_INFO@ "HdRprPlugin": { + "bases": [ + "HdRendererPlugin" + ], + "displayName": "RPR", + "priority": 99 + }, + "HdRprNdrDiscoveryPlugin": { + "bases": ["@RPR_DISCOVERY_BASE@"], + "displayName": "RPR Node Discovery" + }, + "HdRprNdrParserPlugin": { + "bases": ["@RPR_PARSER_BASE@"], + "displayName": "RPR Node Parser" + } + } + }, + "LibraryPath": "@PLUG_INFO_LIBRARY_PATH@", + "Name": "hdRpr", + "ResourcePath": "@PLUG_INFO_RESOURCE_PATH@", + "Root": "@PLUG_INFO_ROOT@", + "Type": "library" + } + ] +} + diff --git a/pxr/imaging/plugin/hdRpr/renderDelegate.cpp b/pxr/imaging/plugin/hdRpr/renderDelegate.cpp index 8723a5f94..762c51f29 100644 --- a/pxr/imaging/plugin/hdRpr/renderDelegate.cpp +++ b/pxr/imaging/plugin/hdRpr/renderDelegate.cpp @@ -44,6 +44,7 @@ limitations under the License. #include #include #include +#include PXR_NAMESPACE_OPEN_SCOPE @@ -355,7 +356,10 @@ HdRenderSettingDescriptorList HdRprDelegate::GetRenderSettingDescriptors() const std::string join(const std::vector& vec, const std::string& delim) { std::stringstream res; - copy(vec.begin(), vec.end(), std::ostream_iterator(res, delim.c_str())); + for (size_t i = 0; i < vec.size(); ++i) { + if (i) res << delim; + res << vec[i]; + } return res.str(); } @@ -378,6 +382,20 @@ VtDictionary HdRprDelegate::GetRenderStats() const { stats["cacheCreationTime"] = rprStats.cacheCreationTime; stats["syncTime"] = rprStats.syncTime; + const double samplesPerSecond = rprStats.averageRenderTimePerSample > 0.0 + ? 1.0 / rprStats.averageRenderTimePerSample : 0.0; + stats["samplesPerSecond"] = samplesPerSecond; + stats["numSamples"] = rprStats.numSamples; + stats["maxSamples"] = rprStats.maxSamples; + stats["outputResolution"] = rprStats.viewportSize; + // Northstar uses 2^n downscaling. Hybrid chooses its scale internally. + const std::string previewScale = rprStats.previewDownscale < 0 + ? "automatic" + : TfStringPrintf("%.1f%%", 100.0 / std::pow(2.0, rprStats.previewDownscale)); + stats["previewScale"] = previewScale; + stats["renderProgressAnnotation"] = TfStringPrintf("%d / %d samples", + rprStats.numSamples, rprStats.maxSamples); + stats["renderStatsAnnotation"] = std::string(); return stats; } diff --git a/pxr/imaging/plugin/hdRpr/rendererPlugin.h b/pxr/imaging/plugin/hdRpr/rendererPlugin.h index 832674755..36d6dc620 100644 --- a/pxr/imaging/plugin/hdRpr/rendererPlugin.h +++ b/pxr/imaging/plugin/hdRpr/rendererPlugin.h @@ -15,6 +15,7 @@ limitations under the License. #define HDRPR_RENDERER_PLUGIN_H #include "pxr/imaging/hd/rendererPlugin.h" +#include "pxr/imaging/rprUsd/apiCapabilities.h" PXR_NAMESPACE_OPEN_SCOPE @@ -32,7 +33,10 @@ class HdRprPlugin final : public HdRendererPlugin { void DeleteRenderDelegate(HdRenderDelegate* renderDelegate) override; -#if PXR_VERSION < 2302 +#if RPR_USD_RENDERER_CREATE_ARGS + bool IsSupported(const HdRendererCreateArgs& args, + std::string* reasonWhyNot = nullptr) const override { return true; } +#elif PXR_VERSION < 2302 bool IsSupported() const override { return true; } #else bool IsSupported(bool gpuEnabled = true) const override { return true; } diff --git a/pxr/imaging/plugin/hdRpr/rprApi.cpp b/pxr/imaging/plugin/hdRpr/rprApi.cpp index 93bb99e0a..eed00cd83 100644 --- a/pxr/imaging/plugin/hdRpr/rprApi.cpp +++ b/pxr/imaging/plugin/hdRpr/rprApi.cpp @@ -2058,9 +2058,11 @@ class HdRprApiImpl { if (m_isInteractive) { downscale = preferences.GetQualityInteractiveDownscaleResolution(); } + m_previewDownscale = downscale; RPR_ERROR_CHECK(m_rprContext->SetParameter(RPR_CONTEXT_PREVIEW, uint32_t(downscale)), "Failed to set preview mode"); } else { bool enableDownscale = m_isInteractive && preferences.GetQualityInteractiveDownscaleEnable(); + m_previewDownscale = enableDownscale ? -1 : 0; RPR_ERROR_CHECK(m_rprContext->SetParameter(RPR_CONTEXT_PREVIEW, uint32_t(enableDownscale)), "Failed to set preview mode"); } @@ -2270,9 +2272,11 @@ class HdRprApiImpl { if (m_isInteractive) { downscale = preferences.GetQualityInteractiveDownscaleResolution(); } + m_previewDownscale = downscale; RPR_ERROR_CHECK(m_rprContext->SetParameter(RPR_CONTEXT_PREVIEW, uint32_t(downscale)), "Failed to set preview mode"); } else { bool enableDownscale = m_isInteractive && preferences.GetQualityInteractiveDownscaleEnable(); + m_previewDownscale = enableDownscale ? -1 : 0; RPR_ERROR_CHECK(m_rprContext->SetParameter(RPR_CONTEXT_PREVIEW, uint32_t(enableDownscale)), "Failed to set preview mode"); } @@ -2453,7 +2457,7 @@ class HdRprApiImpl { projection = HdRprCamera::Orthographic; GfVec3f ndcTopLeft(-1.0f, 1.0f, 0.0f); - GfVec3f nearPlaneTrace = m_cameraProjectionMatrix.GetInverse().Transform(ndcTopLeft); + GfVec3f nearPlaneTrace(m_cameraProjectionMatrix.GetInverse().Transform(ndcTopLeft)); sensorWidth = std::abs(nearPlaneTrace[0]) * 2.0; sensorHeight = std::abs(nearPlaneTrace[1]) * 2.0; @@ -2542,7 +2546,11 @@ class HdRprApiImpl { auto& aovDesc = HdRprAovRegistry::GetInstance().GetAovDesc(name); if (aovDesc.id != kAovNone && aovDesc.format != HdFormatInvalid) { *aovName = name; - *format = ConvertUsdRenderVarDataType(GetAovSetting(UsdRenderTokens->dataType, aovBinding).Get()); + // The logical RenderVar type can differ from the host buffer + // (Houdini may request RGB LPE data in an RGBA display AOV). + // Conversion must match the storage receiving the pixels. + *format = aovBinding.renderBuffer ? aovBinding.renderBuffer->GetFormat() + : ConvertUsdRenderVarDataType(GetAovSetting(UsdRenderTokens->dataType, aovBinding).Get()); return *format != HdFormatInvalid; } else { TF_RUNTIME_ERROR("Unsupported UsdRenderVar sourceName: %s", name.GetText()); @@ -2553,10 +2561,16 @@ class HdRprApiImpl { return false; } - *format = ConvertUsdRenderVarDataType(GetAovSetting(UsdRenderTokens->dataType, aovBinding).Get()); + // The logical RenderVar type can differ from the host buffer + // (Houdini may request RGB LPE data in an RGBA display AOV). + // Conversion must match the storage receiving the pixels. + *format = aovBinding.renderBuffer ? aovBinding.renderBuffer->GetFormat() + : ConvertUsdRenderVarDataType(GetAovSetting(UsdRenderTokens->dataType, aovBinding).Get()); - if (sourceName == "C.*") { - // We can use RPR_AOV_COLOR here instead of reserving one of the available LPE AOV slots + if (sourceName == "C.*" || sourceName == "C.*[LO]") { + // Houdini supplies C.*[LO] for its default beauty RenderVar. + // Use native beauty, preserving its material/lighting response, + // instead of allocating an equivalent custom LPE AOV. *aovName = HdAovTokens->color; } else { if (m_rprContextMetadata.pluginType != kPluginNorthstar) { @@ -3561,6 +3575,10 @@ Don't show this message again? HdRprApi::RenderStats GetRenderStats() const { HdRprApi::RenderStats stats = {}; + stats.numSamples = m_numSamples; + stats.maxSamples = m_maxSamples; + stats.previewDownscale = m_previewDownscale; + stats.viewportSize = m_viewportSize; // rprsExport has no progress callback if (!m_rprSceneExportPath.empty()) { @@ -3577,7 +3595,8 @@ Don't show this message again? stats.percentDone = 100.0 * progress; if (m_numSamples > 0) { - double numRenderedSamples = progress * m_maxSamples; + // Adaptive pixel convergence is not a count of rendered samples. + double numRenderedSamples = m_numSamples; auto resolveTime = m_frameResolveTotalTime / numRenderedSamples; auto renderTime = m_frameRenderTotalTime / numRenderedSamples; @@ -4659,6 +4678,7 @@ Don't show this message again? uint32_t m_frameCount = 0; bool m_isInteractive = false; + int m_previewDownscale = 0; int m_numSamples = 0; int m_numSamplesPerIter = 0; int m_activePixels = -1; diff --git a/pxr/imaging/plugin/hdRpr/rprApi.h b/pxr/imaging/plugin/hdRpr/rprApi.h index 7b39ae1e8..17882f61b 100644 --- a/pxr/imaging/plugin/hdRpr/rprApi.h +++ b/pxr/imaging/plugin/hdRpr/rprApi.h @@ -167,6 +167,10 @@ class HdRprApi final { void Restart(); struct RenderStats { + int numSamples; + int maxSamples; + int previewDownscale; + GfVec2i viewportSize; double percentDone; double averageRenderTimePerSample; double averageResolveTimePerSample; diff --git a/pxr/imaging/plugin/hdRpr/thirdparty/RPRLibs/pluginUtils.hpp b/pxr/imaging/plugin/hdRpr/thirdparty/RPRLibs/pluginUtils.hpp index 795101fee..2e7ed5a49 100644 --- a/pxr/imaging/plugin/hdRpr/thirdparty/RPRLibs/pluginUtils.hpp +++ b/pxr/imaging/plugin/hdRpr/thirdparty/RPRLibs/pluginUtils.hpp @@ -9,6 +9,9 @@ #include #include +#if OPENVDB_LIBRARY_MAJOR_VERSION_NUMBER >= 13 +#include +#endif #include "pluginUtils.h" // These are the functions that are used in Maya plug-in to read vdb files thus far @@ -100,7 +103,13 @@ bool ProcessVDBGrid( values.push_back(value + gridBackgroundVal); } +#if OPENVDB_LIBRARY_MAJOR_VERSION_NUMBER >= 13 + const auto extrema = openvdb::tools::minMax(grid->tree()); + outGrid.minValue = extrema.min(); + outGrid.maxValue = extrema.max(); +#else grid->evalMinMax(outGrid.minValue, outGrid.maxValue); +#endif return true; } diff --git a/pxr/imaging/plugin/rprHoudini/UsdRenderers.json b/pxr/imaging/plugin/rprHoudini/UsdRenderers.json index 94c595f50..16d87d159 100644 --- a/pxr/imaging/plugin/rprHoudini/UsdRenderers.json +++ b/pxr/imaging/plugin/rprHoudini/UsdRenderers.json @@ -1,8 +1,29 @@ { - "HdRprPlugin" : { - "valid" : true, - "menulabel" : "RPR", - "menupriority" : 0, - "restartrendersettings" : ["rpr:core:renderQuality"] + "HdRprPlugin": { + "valid": true, + "menulabel": "RPR", + "menupriority": 0, + "restartrendersettings": [ + "rpr:core:renderQuality" + ], + "pauseonupdate": false, + "allowbackgroundupdate": true, + "viewstats": [ + "Render GPU", + "CPU render threads", + "Samples per second", + "Output resolution", + "RPR preview scale" + ], + "statsdatapaths": { + "totalClockTime": "frameRenderTotalTime", + "percentDone": "percentDone", + "renderProgressAnnotation": "renderProgressAnnotation", + "Render GPU": "gpuUsedNames", + "CPU render threads": "threadCountUsed", + "Samples per second": "samplesPerSecond", + "Output resolution": "outputResolution", + "RPR preview scale": "previewScale" + } } } diff --git a/pxr/imaging/plugin/rprHoudini/VOP_RPRMaterial.cpp b/pxr/imaging/plugin/rprHoudini/VOP_RPRMaterial.cpp index 54fdefe83..644c3d18f 100644 --- a/pxr/imaging/plugin/rprHoudini/VOP_RPRMaterial.cpp +++ b/pxr/imaging/plugin/rprHoudini/VOP_RPRMaterial.cpp @@ -312,11 +312,11 @@ VOP_RPRMaterial::VOP_RPRMaterial(OP_Network* parent, const char* name, OP_Operat } } -const char* VOP_RPRMaterial::inputLabel(unsigned i_idx) const { +const char* VOP_RPRMaterial::inputLabel(RprInputIdx i_idx) const { return m_shaderInfo->GetInput(i_idx)->GetName(); } -const char* VOP_RPRMaterial::outputLabel(unsigned i_idx) const { +const char* VOP_RPRMaterial::outputLabel(RprOutputIdx i_idx) const { return m_shaderInfo->GetOutput(i_idx)->GetName(); } diff --git a/pxr/imaging/plugin/rprHoudini/VOP_RPRMaterial.h b/pxr/imaging/plugin/rprHoudini/VOP_RPRMaterial.h index be834d511..ad3fbd96a 100644 --- a/pxr/imaging/plugin/rprHoudini/VOP_RPRMaterial.h +++ b/pxr/imaging/plugin/rprHoudini/VOP_RPRMaterial.h @@ -42,6 +42,16 @@ struct VOP_RPRMaterialOperator : public VOP_Operator { VOP_RPRMaterialOperator(RprUsdMaterialNodeInfo const* shaderInfo, OP_Constructor construct, PRM_Template* templates); }; +#include "pxr/imaging/rprUsd/apiCapabilities.h" + +#if RPR_HOUDINI_TYPED_PORT_INDICES +using RprInputIdx = OP_InputIdx; +using RprOutputIdx = OP_OutputIdx; +#else +using RprInputIdx = unsigned; +using RprOutputIdx = unsigned; +#endif + class VOP_RPRMaterial : public VOP_Node { public: /// Returns the templates for the shader's input parameters @@ -50,9 +60,9 @@ class VOP_RPRMaterial : public VOP_Node { VOP_RPRMaterial(OP_Network* parent, const char* name, OP_Operator* entry); /// Returns the label for input port at index i_idx - const char* inputLabel(unsigned i_idx) const override; + const char* inputLabel(RprInputIdx i_idx) const override; /// Returns the label for output port at index i_idx - const char* outputLabel(unsigned i_idx) const override; + const char* outputLabel(RprOutputIdx i_idx) const override; /// Minimum inputs that must be connected to a node for it to cook. unsigned minInputs() const override; diff --git a/pxr/imaging/rprUsd/CMakeLists.txt b/pxr/imaging/rprUsd/CMakeLists.txt index db2b8abad..3b1189952 100644 --- a/pxr/imaging/rprUsd/CMakeLists.txt +++ b/pxr/imaging/rprUsd/CMakeLists.txt @@ -128,6 +128,7 @@ pxr_library(rprUsd lightRegistry PUBLIC_HEADERS + ${PROJECT_BINARY_DIR}/apiCapabilities.h contextMetadata.h error.h helpers.h diff --git a/pxr/imaging/rprUsd/boostIncludePath.h b/pxr/imaging/rprUsd/boostIncludePath.h index d532acceb..d1f7904a1 100644 --- a/pxr/imaging/rprUsd/boostIncludePath.h +++ b/pxr/imaging/rprUsd/boostIncludePath.h @@ -25,4 +25,16 @@ limitations under the License. # define BOOST_INCLUDE_PATH(suffix) __STRINGIZE_PATH(boost/suffix) #endif +// Modern USD vendors Python bindings only; other Boost utilities still use +// the host's Boost installation. +#if __has_include("pxr/external/boost/python.hpp") +# define RPR_BOOST_PYTHON_NS PXR_NS::pxr_boost::python +# define RPR_BOOST_PYTHON_INCLUDE(suffix) __STRINGIZE_PATH(pxr/external/boost/suffix) +# define RPR_BOOST_NONCOPYABLE RPR_BOOST_PYTHON_NS::noncopyable +#else +# define RPR_BOOST_PYTHON_NS BOOST_NS::python +# define RPR_BOOST_PYTHON_INCLUDE(suffix) BOOST_INCLUDE_PATH(suffix) +# define RPR_BOOST_NONCOPYABLE BOOST_NS::noncopyable +#endif + #endif // BOOST_INCLUDE_PATH_H diff --git a/pxr/imaging/rprUsd/contextHelpers.cpp b/pxr/imaging/rprUsd/contextHelpers.cpp index ef1b14630..caf9777d7 100644 --- a/pxr/imaging/rprUsd/contextHelpers.cpp +++ b/pxr/imaging/rprUsd/contextHelpers.cpp @@ -53,6 +53,8 @@ PXR_NAMESPACE_OPEN_SCOPE TF_DEFINE_ENV_SETTING(RPRUSD_ENABLE_TRACING, false, "Enable tracing of RPR core"); TF_DEFINE_ENV_SETTING(RPRUSD_TRACING_DIR, "", "Where to store RPR core tracing files. Must be a path to valid directory"); +TF_DEFINE_ENV_SETTING(RPRUSD_HIP_LIBRARY, "", + "Optional absolute HIP runtime/loader library path on Linux. Uses HIP and queries its device count; empty preserves SDK defaults."); TF_DEFINE_ENV_SETTING(RPRUSD_CPU_ONLY, false, "Disable RIF API and GPU context creation. This will allow running on CPU only machines, but some AOV will no longer work"); @@ -122,6 +124,46 @@ void SetupRprTracing() { } } +// Retain the selected library for the lifetime of every renderer context. +// This path is opt-in and is unrelated to the Houdini/USD API version. +struct ExplicitHipRuntime { + bool requested = false; + bool valid = false; + int deviceCount = 0; + void* handle = nullptr; + ExplicitHipRuntime() { +#if defined(__linux__) + const auto path = TfGetEnvSetting(RPRUSD_HIP_LIBRARY); + requested = !path.empty(); + if (!requested) return; + if (path.front() != '/') { + TF_RUNTIME_ERROR("RPRUSD_HIP_LIBRARY must be an absolute path"); + return; + } + // Northstar/Orochi exports HIP function-pointer variables with the + // same names as HIP functions. Global loading would interpose them. + handle = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); + if (!handle) { + TF_RUNTIME_ERROR("Cannot load selected HIP runtime %s: %s", path.c_str(), dlerror()); + return; + } + using GetDeviceCount = int (*)(int*); + auto getDeviceCount = reinterpret_cast(dlsym(handle, "hipGetDeviceCount")); + if (!getDeviceCount || getDeviceCount(&deviceCount) != 0 || deviceCount < 0) { + TF_RUNTIME_ERROR("Cannot enumerate devices using selected HIP runtime %s", path.c_str()); + return; + } + valid = true; + PRINT_CONTEXT_CREATION_DEBUG_INFO("Explicit HIP runtime %s: %d devices\n", path.c_str(), deviceCount); +#endif + } +}; + +const ExplicitHipRuntime& GetExplicitHipRuntime() { + static const ExplicitHipRuntime runtime; + return runtime; +} + const rpr::CreationFlags kGpuCreationFlags[] = { RPR_CREATION_FLAGS_ENABLE_GPU0, RPR_CREATION_FLAGS_ENABLE_GPU1, @@ -156,6 +198,10 @@ const std::map kPluginLibNames = { }; rpr_int GetPluginID(RprUsdPluginType pluginType) { + if (pluginType == kPluginNorthstar) { + const auto& runtime = GetExplicitHipRuntime(); + if (runtime.requested && !runtime.valid) return -1; + } auto pluginLibNameIter = kPluginLibNames.find(pluginType); if (pluginLibNameIter == kPluginLibNames.end()) { TF_RUNTIME_ERROR("Plugin is not supported: %d", pluginType); @@ -176,12 +222,25 @@ rpr_int GetPluginID(RprUsdPluginType pluginType) { std::string GetGpuName(RprUsdPluginType pluginType, rpr_int pluginID, rpr::CreationFlags creationFlag, rpr::ContextInfo gpuNameId, const char* cachePath) { rpr::CreationFlags additionalFlags = 0x0; + if (pluginType == kPluginNorthstar && GetExplicitHipRuntime().requested) { + additionalFlags |= RPR_CREATION_FLAGS_ENABLE_HIP; + } #if defined(__APPLE__) additionalFlags |= RPR_CREATION_FLAGS_ENABLE_METAL; #endif std::vector properties; + // Device discovery creates real contexts too. Supply the same native + // kernels as rendering, avoiding an unintended runtime-compiler path. + std::string precompiledKernelsPath; + if (pluginType == kPluginNorthstar) { + RprUsdConfig* config; + auto configLock = RprUsdConfig::GetInstance(&config); + precompiledKernelsPath = config->GetPrecompiledKernelDir(); + properties.push_back((rpr_context_properties)RPR_CONTEXT_PRECOMPILED_BINARY_PATH); + properties.push_back((rpr_context_properties)precompiledKernelsPath.c_str()); + } #ifdef HDRPR_ENABLE_VULKAN_INTEROP_SUPPORT // This fix is required since for 4GB VRAM default Hybrid allocation might be too big @@ -202,6 +261,7 @@ std::string GetGpuName(RprUsdPluginType pluginType, rpr_int pluginID, rpr::Creat properties.push_back(nullptr); + PRINT_CONTEXT_CREATION_DEBUG_INFO("Discovering GPU flags=%u kernels=%s\n", creationFlag | additionalFlags, precompiledKernelsPath.c_str()); try { rpr::Status status; std::unique_ptr context(rpr::Context::Create(RPR_API_VERSION, &pluginID, 1, creationFlag | additionalFlags, properties.data(), cachePath, &status)); @@ -219,6 +279,7 @@ template void ForEachGpu(RprUsdPluginType pluginType, rpr_int pluginID, const char* cachePath, Func&& func) { #define GPU_ACTION(index) \ do { \ + if (pluginType == kPluginNorthstar && GetExplicitHipRuntime().requested && index >= GetExplicitHipRuntime().deviceCount) return; \ rpr::CreationFlags gpuFlag = RPR_CREATION_FLAGS_ENABLE_GPU ## index; \ std::string name = GetGpuName(pluginType, pluginID, gpuFlag, RPR_CONTEXT_GPU ## index ## _NAME, cachePath); \ func(index, gpuFlag, name); \ @@ -387,6 +448,13 @@ rpr::Context* RprUsdCreateContext(RprUsdContextMetadata* metadata) { // set up HIP/CUDA support if (metadata->pluginType == kPluginNorthstar) { + if (GetExplicitHipRuntime().requested) { + if (metadata->useOpenCL) { + TF_RUNTIME_ERROR("The explicit HIP runtime conflicts with the legacy OpenCL render setting"); + return nullptr; + } + creationFlags |= RPR_CREATION_FLAGS_ENABLE_HIP; + } if (metadata->useOpenCL) { creationFlags |= RPR_CREATION_FLAGS_ENABLE_OPENCL; } diff --git a/pxr/imaging/rprUsd/materialNodes/rpr/arithmeticNode.cpp b/pxr/imaging/rprUsd/materialNodes/rpr/arithmeticNode.cpp index 3bae4c093..1659c3be6 100644 --- a/pxr/imaging/rprUsd/materialNodes/rpr/arithmeticNode.cpp +++ b/pxr/imaging/rprUsd/materialNodes/rpr/arithmeticNode.cpp @@ -150,7 +150,7 @@ protected: \ rpr::MaterialNodeArithmeticOperation GetOp() const final { return kOp; } \ VtValue EvalOperation() const final { eval } \ }; \ -ARCH_CONSTRUCTOR(RprUsd_InitArithmeticNode ## op, 255, void) { \ +RPRUSD_ARCH_CONSTRUCTOR(RprUsd_InitArithmeticNode ## op, 255) { \ RprUsd_RprArithmeticNodeRegistry::GetInstance().Register(op); \ } diff --git a/pxr/imaging/rprUsd/materialNodes/rpr/catcherNode.cpp b/pxr/imaging/rprUsd/materialNodes/rpr/catcherNode.cpp index 7b92b250e..f8e8d1503 100644 --- a/pxr/imaging/rprUsd/materialNodes/rpr/catcherNode.cpp +++ b/pxr/imaging/rprUsd/materialNodes/rpr/catcherNode.cpp @@ -94,7 +94,7 @@ class RprUsd_RprCatcherNode : public RprUsd_MaterialNode { }; #define REGISTER_CATCHER_NODE(CATCHER_TYPE) \ -ARCH_CONSTRUCTOR(RprUsd_Init ## CATCHER_TYPE ## CatcherNode, 255, void) { \ +RPRUSD_ARCH_CONSTRUCTOR(RprUsd_Init ## CATCHER_TYPE ## CatcherNode, 255) { \ auto nodeInfo = RprUsd_RprCatcherNode::GetInfo(#CATCHER_TYPE); \ RprUsdMaterialRegistry::GetInstance().Register( \ TfToken(nodeInfo->name, TfToken::Immortal), \ diff --git a/pxr/imaging/rprUsd/materialNodes/rpr/combineShadersNode.cpp b/pxr/imaging/rprUsd/materialNodes/rpr/combineShadersNode.cpp index eccd59376..28513b8c2 100644 --- a/pxr/imaging/rprUsd/materialNodes/rpr/combineShadersNode.cpp +++ b/pxr/imaging/rprUsd/materialNodes/rpr/combineShadersNode.cpp @@ -106,7 +106,7 @@ class RprUsd_RprCombineShadersNode : public RprUsd_MaterialNode { std::map m_outputs; }; -ARCH_CONSTRUCTOR(RprUsd_InitCombineShadersNode, 255, void) { +RPRUSD_ARCH_CONSTRUCTOR(RprUsd_InitCombineShadersNode, 255) { auto nodeInfo = RprUsd_RprCombineShadersNode::GetInfo(); RprUsdMaterialRegistry::GetInstance().Register( TfToken(nodeInfo->name, TfToken::Immortal), diff --git a/pxr/imaging/rprUsd/materialNodes/rpr/displaceNode.cpp b/pxr/imaging/rprUsd/materialNodes/rpr/displaceNode.cpp index 61014b53a..5f476e1dc 100644 --- a/pxr/imaging/rprUsd/materialNodes/rpr/displaceNode.cpp +++ b/pxr/imaging/rprUsd/materialNodes/rpr/displaceNode.cpp @@ -132,7 +132,7 @@ class RprUsd_RprDisplaceNode : public RprUsd_MaterialNode { VtValue m_output; }; -ARCH_CONSTRUCTOR(RprUsd_InitDisplaceNode, 255, void) { +RPRUSD_ARCH_CONSTRUCTOR(RprUsd_InitDisplaceNode, 255) { auto nodeInfo = RprUsd_RprDisplaceNode::GetInfo(); RprUsdMaterialRegistry::GetInstance().Register( TfToken(nodeInfo->name, TfToken::Immortal), diff --git a/pxr/imaging/rprUsd/materialNodes/rpr/materialXNode.cpp b/pxr/imaging/rprUsd/materialNodes/rpr/materialXNode.cpp index f8963a9e6..c7a5e6ea6 100644 --- a/pxr/imaging/rprUsd/materialNodes/rpr/materialXNode.cpp +++ b/pxr/imaging/rprUsd/materialNodes/rpr/materialXNode.cpp @@ -419,7 +419,7 @@ class RprUsd_RprMaterialXNode : public RprUsd_MaterialNode { std::shared_ptr m_displacementNode; }; -ARCH_CONSTRUCTOR(RprUsd_InitMaterialXNode, 255, void) { +RPRUSD_ARCH_CONSTRUCTOR(RprUsd_InitMaterialXNode, 255) { auto nodeInfo = RprUsd_RprMaterialXNode::GetInfo(); RprUsdMaterialRegistry::GetInstance().Register( RprUsdRprMaterialXNodeTokens->rpr_materialx_node, diff --git a/pxr/imaging/rprUsd/materialNodes/rpr/toonNode.cpp b/pxr/imaging/rprUsd/materialNodes/rpr/toonNode.cpp index 5916e2969..d8cc8e04d 100644 --- a/pxr/imaging/rprUsd/materialNodes/rpr/toonNode.cpp +++ b/pxr/imaging/rprUsd/materialNodes/rpr/toonNode.cpp @@ -335,7 +335,7 @@ class RprUsd_RprToonNode : public RprUsd_MaterialNode { } }; -ARCH_CONSTRUCTOR(RprUsd_InitToonNode, 255, void) { +RPRUSD_ARCH_CONSTRUCTOR(RprUsd_InitToonNode, 255) { auto nodeInfo = RprUsd_RprToonNode::GetInfo(); RprUsdMaterialRegistry::GetInstance().Register( TfToken(nodeInfo->name, TfToken::Immortal), diff --git a/pxr/imaging/rprUsd/materialNodes/usdNode.cpp b/pxr/imaging/rprUsd/materialNodes/usdNode.cpp index 978201bfa..518b8750f 100644 --- a/pxr/imaging/rprUsd/materialNodes/usdNode.cpp +++ b/pxr/imaging/rprUsd/materialNodes/usdNode.cpp @@ -591,7 +591,7 @@ void RprUsd_RegisterUsdNode(const char* id) { &RprUsd_CreateUsdNode); } -ARCH_CONSTRUCTOR(RprUsd_RegisterUsdNodes, 255, void) { +RPRUSD_ARCH_CONSTRUCTOR(RprUsd_RegisterUsdNodes, 255) { RprUsd_RegisterUsdNode("UsdPreviewSurface"); RprUsd_RegisterUsdNode("UsdPrimvarReader_float2"); RprUsd_RegisterUsdNode("UsdTransform2d"); diff --git a/pxr/imaging/rprUsd/materialRegistry.h b/pxr/imaging/rprUsd/materialRegistry.h index 3ded4e5cf..f3dd97165 100644 --- a/pxr/imaging/rprUsd/materialRegistry.h +++ b/pxr/imaging/rprUsd/materialRegistry.h @@ -18,6 +18,15 @@ limitations under the License. #include "pxr/imaging/rprUsd/debugCodes.h" #include "pxr/imaging/hd/material.h" #include "pxr/base/arch/demangle.h" +#include "pxr/base/arch/attributes.h" + +#include "pxr/imaging/rprUsd/apiCapabilities.h" + +#if RPR_USD_ARCH_CONSTRUCTOR_TWO_ARGS +#define RPRUSD_ARCH_CONSTRUCTOR(name, priority) ARCH_CONSTRUCTOR(name, priority) +#else +#define RPRUSD_ARCH_CONSTRUCTOR(name, priority) ARCH_CONSTRUCTOR(name, priority, void) +#endif #include "pxr/base/tf/singleton.h" #include diff --git a/pxr/imaging/rprUsd/wrapConfig.cpp b/pxr/imaging/rprUsd/wrapConfig.cpp index 8aa77be7e..da2cf15fb 100644 --- a/pxr/imaging/rprUsd/wrapConfig.cpp +++ b/pxr/imaging/rprUsd/wrapConfig.cpp @@ -14,12 +14,12 @@ limitations under the License. #include "config.h" #include "boostIncludePath.h" -#include BOOST_INCLUDE_PATH(python.hpp) -#include BOOST_INCLUDE_PATH(python/class.hpp) -#include BOOST_INCLUDE_PATH(python/def.hpp) -#include BOOST_INCLUDE_PATH(python/scope.hpp) +#include RPR_BOOST_PYTHON_INCLUDE(python.hpp) +#include RPR_BOOST_PYTHON_INCLUDE(python/class.hpp) +#include RPR_BOOST_PYTHON_INCLUDE(python/def.hpp) +#include RPR_BOOST_PYTHON_INCLUDE(python/scope.hpp) -using namespace BOOST_NS::python; +using namespace RPR_BOOST_PYTHON_NS; PXR_NAMESPACE_USING_DIRECTIVE @@ -41,7 +41,7 @@ void wrapConfig() { }) \ .staticmethod(#getter) - scope s = class_("Config", no_init) + scope s = class_("Config", no_init) CONFIG_SETTER(SetRestartWarning, bool) CONFIG_SETTER(SetTextureCacheDir, std::string) CONFIG_SETTER(SetKernelCacheDir, std::string) diff --git a/pxr/imaging/rprUsd/wrapContextHelpers.cpp b/pxr/imaging/rprUsd/wrapContextHelpers.cpp index 448874aa1..bcce5216b 100644 --- a/pxr/imaging/rprUsd/wrapContextHelpers.cpp +++ b/pxr/imaging/rprUsd/wrapContextHelpers.cpp @@ -18,12 +18,12 @@ limitations under the License. #include "pxr/base/tf/registryManager.h" #include "pxr/base/tf/pyResultConversions.h" -#include BOOST_INCLUDE_PATH(python.hpp) -#include BOOST_INCLUDE_PATH(python/class.hpp) -#include BOOST_INCLUDE_PATH(python/def.hpp) -#include BOOST_INCLUDE_PATH(python/scope.hpp) +#include RPR_BOOST_PYTHON_INCLUDE(python.hpp) +#include RPR_BOOST_PYTHON_INCLUDE(python/class.hpp) +#include RPR_BOOST_PYTHON_INCLUDE(python/def.hpp) +#include RPR_BOOST_PYTHON_INCLUDE(python/scope.hpp) -using namespace BOOST_NS::python; +using namespace RPR_BOOST_PYTHON_NS; PXR_NAMESPACE_USING_DIRECTIVE From 430c514260832a0ee8f5493831ddc3968a8f3f2d Mon Sep 17 00:00:00 2001 From: Jason Batson Date: Sun, 6 Sep 2026 20:03:50 -0500 Subject: [PATCH 2/5] Add optional OIDN previews and HybridPro device selection Add optional CPU OIDN filtering for Northstar beauty before display transforms, with guide handling and an installed runtime search path for the selected OIDN. Separate HybridPro denoising from FSR2 upscaling and expose backend/mode stats. Register Linux HybridPro and discover advertised physical GPU indices through the optional SDK export. Keep legacy GPU0 fallback when unavailable, and reject explicit multiple or stale Hybrid device selections instead of changing cards. Document tested Houdini runtime behavior, passive compatibility and the observed AMD RADV HybridPro device loss. Northstar/HIP and NVIDIA HybridPro preview checks passed; no matched GPU benchmark or AMD Vulkan stability claim is made. --- BUILD_HOUDINI.md | 61 ++++++ cmake/modules/FindOpenImageDenoise.cmake | 25 +++ pxr/imaging/plugin/hdRpr/CMakeLists.txt | 21 +++ pxr/imaging/plugin/hdRpr/cpuDenoiser.cpp | 176 +++++++++++++++++ pxr/imaging/plugin/hdRpr/cpuDenoiser.h | 35 ++++ .../python/generateRenderSettingFiles.py | 25 ++- pxr/imaging/plugin/hdRpr/renderDelegate.cpp | 6 +- pxr/imaging/plugin/hdRpr/rprApi.cpp | 28 ++- pxr/imaging/plugin/hdRpr/rprApi.h | 3 + pxr/imaging/plugin/hdRpr/rprApiAov.cpp | 177 +++++++++++++++++- pxr/imaging/plugin/hdRpr/rprApiAov.h | 33 +++- .../plugin/rprHoudini/UsdRenderers.json | 8 +- pxr/imaging/rprUsd/contextHelpers.cpp | 86 ++++++++- 13 files changed, 665 insertions(+), 19 deletions(-) create mode 100644 cmake/modules/FindOpenImageDenoise.cmake create mode 100644 pxr/imaging/plugin/hdRpr/cpuDenoiser.cpp create mode 100644 pxr/imaging/plugin/hdRpr/cpuDenoiser.h diff --git a/BUILD_HOUDINI.md b/BUILD_HOUDINI.md index b6ee49de7..9a922ea5e 100644 --- a/BUILD_HOUDINI.md +++ b/BUILD_HOUDINI.md @@ -84,3 +84,64 @@ In H22.0.429, repeated `hou.SceneViewer.setRendererPaused(False)` calls toggled the renderer instead of acting idempotently. Check `isRendererPaused()` first and call the setter only if the state differs. This workaround also works with normal setters; it should not replace the user's deliberate pause choice. + + +## Optional preview paths + +`HDRPR_ENABLE_OIDN` defaults to ON but is optional. CMake looks for Open Image +Denoise 2, preferring the selected Houdini installation. If disabled or absent, +the renderer still builds and shows the raw image. When available, Northstar can +apply the CPU OIDN filter to beauty with albedo/normal guides before tone mapping +and gamma. Alpha is preserved. GPU ray tracing stays on the selected render +GPU; the CPU only runs this spatial filter. The installed plugin retains the +selected OIDN library directory in its runtime search path. Rebuild for a moved +or different host installation. + +Northstar exposes `rpr:denoising:enable` (default false) and +`rpr:denoising:minSamples` (default 16). Adaptive sampling can be used with this +filter. Filtering can soften fine reflected or transmitted features, so compare +against a higher-sample raw reference when approving materials. + +Linux now registers the SDK's HybridPro backend. Its SVGF/ASVGF denoising can run +at native resolution by selecting `None` for `rpr:hybrid:upscalingQuality`. +The existing FSR2 quality modes remain available when a temporal denoiser is +enabled. This separates denoising from upscaling without changing the upstream +default quality selection. The HUD identifies the active backend, denoiser and +FSR2 mode; display updates are not completed rendered frames. + +Hybrid/HybridPro device discovery uses the optional SDK supported-device export. +Each advertised physical index is probed with one GPU flag; its name is read as +active device zero. Older SDKs without that export retain GPU0 discovery. +Hybrid supports one selected GPU per context. Explicit multiple or stale device +selections fail rather than silently falling back to a different GPU. Device +configuration is reread when a context is created, so switching devices needs a +renderer-context restart, not a Houdini restart. + +Both GPUs must be visible to Vulkan for index selection. On the tested Linux +workstation, `MESA_VK_DEVICE_SELECT=10de:1e84` prefers NVIDIA for Houdini's display +while leaving AMD available to HybridPro. A trailing `!` or +`MESA_VK_DEVICE_SELECT_FORCE_DEFAULT_DEVICE` hides other devices. Restricting +`VK_DRIVER_FILES` to a single driver likewise changes discovery. These are +workstation environment choices, not mandatory plugin settings. + +Local checks include a normal-launcher load with bundled OIDN 2.3.3, matched +128-sample Northstar raw/OIDN captures, and adaptive sampling plus OIDN. A +standalone helper check verified finite HDR output, preserved alpha, resolution +changes, and operation with/without guides. Configure also succeeds with both +`HDRPR_ENABLE_OIDN=OFF` and `RPR_ROCM7_ROOT` empty. Other Houdini/OS combinations +remain passively supported but untested. + + +HybridPro device-index discovery was checked on NVIDIA RTX 2070 Super, AMD +Radeon AI PRO R9700 and llvmpipe. AMD selection survived an in-GUI context +restart with an unchanged camera. However, a later AMD HybridPro render/switch +on RADV 25.2.8 hit VK_ERROR_DEVICE_LOST, hard GPU recovery and a Houdini crash. +Treat this AMD Vulkan path as experimental; a device-name query or an earlier +completed frame does not establish stability. Northstar/HIP is the validated +AMD path for this workstation. No matched AMD/NVIDIA performance claim is made +from the incomplete HybridPro sequence. + +NVIDIA HybridPro also completed a fixed-camera128-sample SVGF capture at native +resolution. It reduced noise in some regions while leaving substantial noise in +reflections/transmission. ASVGF/FSR2 image quality and temporal behavior remain +unvalidated in the GUI; those controls are exposed but not production-approved. diff --git a/cmake/modules/FindOpenImageDenoise.cmake b/cmake/modules/FindOpenImageDenoise.cmake new file mode 100644 index 000000000..3bb183bf7 --- /dev/null +++ b/cmake/modules/FindOpenImageDenoise.cmake @@ -0,0 +1,25 @@ +# Optional OIDN 2 discovery. Prefer the libraries belonging to the selected +# Houdini installation; older Houdini and standalone USD builds may omit OIDN. +find_path(OpenImageDenoise_INCLUDE_DIR + NAMES OpenImageDenoise/oidn.h + HINTS "${HOUDINI_ROOT}/toolkit/include" "${OpenImageDenoise_ROOT}/include") +find_library(OpenImageDenoise_LIBRARY + NAMES OpenImageDenoise + HINTS "${Houdini_LIB_DIR}" "${HOUDINI_ROOT}/dsolib" + "${OpenImageDenoise_ROOT}/lib" "${OpenImageDenoise_ROOT}/lib64") + +set(OpenImageDenoise_VERSION "") +if(EXISTS "${OpenImageDenoise_INCLUDE_DIR}/OpenImageDenoise/config.h") + file(STRINGS "${OpenImageDenoise_INCLUDE_DIR}/OpenImageDenoise/config.h" + _oidn_version REGEX "^#define OIDN_VERSION_STRING ") + if(_oidn_version) + string(REGEX REPLACE ".*\"([0-9]+\\.[0-9]+\\.[0-9]+)\".*" "\\1" + OpenImageDenoise_VERSION "${_oidn_version}") + endif() +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(OpenImageDenoise + REQUIRED_VARS OpenImageDenoise_INCLUDE_DIR OpenImageDenoise_LIBRARY OpenImageDenoise_VERSION + VERSION_VAR OpenImageDenoise_VERSION) +mark_as_advanced(OpenImageDenoise_INCLUDE_DIR OpenImageDenoise_LIBRARY) diff --git a/pxr/imaging/plugin/hdRpr/CMakeLists.txt b/pxr/imaging/plugin/hdRpr/CMakeLists.txt index db84d7cfa..660744c4d 100644 --- a/pxr/imaging/plugin/hdRpr/CMakeLists.txt +++ b/pxr/imaging/plugin/hdRpr/CMakeLists.txt @@ -6,6 +6,11 @@ set(OptBin ${ARGN}) set(OptIncludeDir ${ARGN}) set(OptClass${ARGN}) +option(HDRPR_ENABLE_OIDN "Enable optional Open Image Denoise 2 CPU denoising" ON) +if(HDRPR_ENABLE_OIDN) + find_package(OpenImageDenoise 2 QUIET MODULE) +endif() + if(OpenVDB_FOUND) add_definitions(-DUSE_VOLUME -DOPENVDB_DLL) @@ -161,6 +166,7 @@ pxr_plugin(hdRpr renderParam rprApi rprApiAov + cpuDenoiser rprApiFramebuffer mesh instancer @@ -193,6 +199,21 @@ pxr_plugin(hdRpr ndrParserPlugin.cpp ) +if(HDRPR_ENABLE_OIDN AND OpenImageDenoise_FOUND) + target_compile_definitions(hdRpr PRIVATE HDRPR_HAS_OIDN) + target_include_directories(hdRpr PRIVATE ${OpenImageDenoise_INCLUDE_DIR}) + target_link_libraries(hdRpr ${OpenImageDenoise_LIBRARY}) + if(UNIX) + # The USD install helpers omit external link directories from the RPATH. + # Keep the selected host or standalone OIDN discoverable after install. + get_filename_component(_oidn_library_dir "${OpenImageDenoise_LIBRARY}" DIRECTORY) + set_property(TARGET hdRpr APPEND PROPERTY INSTALL_RPATH "${_oidn_library_dir}") + endif() + message(STATUS "Northstar spatial denoising: Open Image Denoise ${OpenImageDenoise_VERSION} (CPU)") +else() + message(STATUS "Northstar spatial denoising: disabled (optional Open Image Denoise 2 not found or disabled)") +endif() + if(RPR_EXR_EXPORT_ENABLED) target_compile_definitions(hdRpr PRIVATE -DRPR_EXR_EXPORT_ENABLED) endif() diff --git a/pxr/imaging/plugin/hdRpr/cpuDenoiser.cpp b/pxr/imaging/plugin/hdRpr/cpuDenoiser.cpp new file mode 100644 index 000000000..8fbd8256a --- /dev/null +++ b/pxr/imaging/plugin/hdRpr/cpuDenoiser.cpp @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "cpuDenoiser.h" + +#include +#include +#include +#include +#include +#include + +#ifdef HDRPR_HAS_OIDN +#include +#endif + +PXR_NAMESPACE_OPEN_SCOPE + +struct HdRprCpuDenoiser::Impl { +#ifdef HDRPR_HAS_OIDN + OIDNDevice device = nullptr; + OIDNFilter filter = nullptr; + bool hasAlbedo = false; + bool hasNormal = false; + std::vector color; + std::vector albedo; + std::vector normal; + + ~Impl() { + if (filter) oidnReleaseFilter(filter); + if (device) oidnReleaseDevice(device); + } + + bool Check(std::string* error) { + const char* message = nullptr; + if (oidnGetDeviceError(device, &message) == OIDN_ERROR_NONE) return true; + if (error) *error = message ? message : "Open Image Denoise error"; + return false; + } + + bool Init(std::string* error) { + if (filter) return true; + if (!device) { + device = oidnNewDevice(OIDN_DEVICE_TYPE_CPU); + if (!device) { + if (error) *error = "Cannot create Open Image Denoise CPU device"; + return false; + } + oidnCommitDevice(device); + if (!Check(error)) { + oidnReleaseDevice(device); + device = nullptr; + return false; + } + } + filter = oidnNewFilter(device, "RT"); + if (!Check(error) || !filter) { + if (filter) oidnReleaseFilter(filter); + filter = nullptr; + if (error && error->empty()) *error = "Cannot create Open Image Denoise RT filter"; + return false; + } + oidnSetFilterBool(filter, "hdr", true); + oidnSetFilterBool(filter, "srgb", false); + oidnSetFilterBool(filter, "cleanAux", false); + if (!Check(error)) { + oidnReleaseFilter(filter); + filter = nullptr; + return false; + } + return true; + } + + void SetImage(const char* name, const float* pixels, size_t width, size_t height) { + oidnSetSharedFilterImage(filter, name, const_cast(pixels), + OIDN_FORMAT_FLOAT3, width, height, 0, + 4 * sizeof(float), width * 4 * sizeof(float)); + } +#endif +}; + +HdRprCpuDenoiser::HdRprCpuDenoiser() : m_impl(new Impl) {} +HdRprCpuDenoiser::~HdRprCpuDenoiser() = default; + +bool HdRprCpuDenoiser::IsAvailable() { +#ifdef HDRPR_HAS_OIDN + return true; +#else + return false; +#endif +} + +bool HdRprCpuDenoiser::Execute(const float* color, const float* albedo, + const float* normal, float* output, + size_t width, size_t height, std::string* error) { + if (error) error->clear(); +#ifdef HDRPR_HAS_OIDN + if (!color || !output || !width || !height || + width > std::numeric_limits::max() / height / 4 / sizeof(float)) { + if (error) *error = "Invalid denoising buffer dimensions"; + return false; + } + const size_t count = width * height * 4; + std::memcpy(output, color, count * sizeof(float)); + try { + if (!m_impl->Init(error)) return false; + m_impl->color.assign(color, color + count); + // Keep the original render buffer untouched. HDR values above one are + // retained; only negative reconstruction undershoot is clamped for OIDN. + for (size_t i = 0; i < count; i += 4) { + for (size_t channel = 0; channel < 3; ++channel) { + if (!std::isfinite(color[i + channel])) { + if (error) *error = "Non-finite color in denoising input"; + return false; + } + m_impl->color[i + channel] = std::max(0.0f, color[i + channel]); + } + } + m_impl->SetImage("color", m_impl->color.data(), width, height); + m_impl->SetImage("output", output, width, height); + if (albedo) { + m_impl->albedo.resize(count); + for (size_t i = 0; i < count; i += 4) { + for (size_t channel = 0; channel < 3; ++channel) { + const float value = albedo[i + channel]; + m_impl->albedo[i + channel] = std::isfinite(value) + ? std::max(0.0f, std::min(1.0f, value)) : 0.0f; + } + } + m_impl->SetImage("albedo", m_impl->albedo.data(), width, height); + } else if (m_impl->hasAlbedo) { + oidnUnsetFilterImage(m_impl->filter, "albedo"); + } + // OIDN supports color alone, color+albedo, or color+albedo+normal. + const bool useNormal = albedo && normal; + if (useNormal) { + m_impl->normal.resize(count); + for (size_t i = 0; i < count; i += 4) { + const bool background = normal[i] == 0.0f && + normal[i + 1] == 0.0f && normal[i + 2] == 0.0f; + for (size_t channel = 0; channel < 3; ++channel) { + const float value = normal[i + channel]; + m_impl->normal[i + channel] = !background && std::isfinite(value) + ? 2.0f * std::max(0.0f, std::min(1.0f, value)) - 1.0f : 0.0f; + } + } + m_impl->SetImage("normal", m_impl->normal.data(), width, height); + } else if (m_impl->hasNormal) { + oidnUnsetFilterImage(m_impl->filter, "normal"); + } + m_impl->hasAlbedo = albedo != nullptr; + m_impl->hasNormal = useNormal; + oidnCommitFilter(m_impl->filter); + if (!m_impl->Check(error)) return false; + oidnExecuteFilter(m_impl->filter); + if (!m_impl->Check(error)) return false; + for (size_t i = 0; i < count; i += 4) { + output[i + 3] = color[i + 3]; + for (size_t channel = 0; channel < 3; ++channel) { + if (!std::isfinite(output[i + channel])) { + if (error) *error = "Non-finite denoising output"; + return false; + } + } + } + return true; + } catch (std::exception const& e) { + if (error) *error = e.what(); + return false; + } +#else + (void)color; (void)albedo; (void)normal; (void)output; (void)width; (void)height; + if (error) *error = "Open Image Denoise 2 was not available at build time"; + return false; +#endif +} + +PXR_NAMESPACE_CLOSE_SCOPE diff --git a/pxr/imaging/plugin/hdRpr/cpuDenoiser.h b/pxr/imaging/plugin/hdRpr/cpuDenoiser.h new file mode 100644 index 000000000..37a2da4cc --- /dev/null +++ b/pxr/imaging/plugin/hdRpr/cpuDenoiser.h @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +#ifndef HDRPR_CPU_DENOISER_H +#define HDRPR_CPU_DENOISER_H + +#include "pxr/pxr.h" + +#include +#include +#include + +PXR_NAMESPACE_OPEN_SCOPE + +// Optional, spatial-only Open Image Denoise support. The implementation always +// chooses the CPU so rendering on a different GPU does not change this backend. +class HdRprCpuDenoiser { +public: + HdRprCpuDenoiser(); + ~HdRprCpuDenoiser(); + + static bool IsAvailable(); + + // All buffers contain width * height float4 pixels. Only RGB is filtered; + // output alpha is copied from color. RPR normal guides are encoded in [0,1]. + // Guides are optional, and are treated as noisy (cleanAux=false). + bool Execute(const float* color, const float* albedo, const float* normal, + float* output, size_t width, size_t height, std::string* error); + +private: + struct Impl; + std::unique_ptr m_impl; +}; + +PXR_NAMESPACE_CLOSE_SCOPE + +#endif diff --git a/pxr/imaging/plugin/hdRpr/python/generateRenderSettingFiles.py b/pxr/imaging/plugin/hdRpr/python/generateRenderSettingFiles.py index fcc60c9c2..cda81468f 100644 --- a/pxr/imaging/plugin/hdRpr/python/generateRenderSettingFiles.py +++ b/pxr/imaging/plugin/hdRpr/python/generateRenderSettingFiles.py @@ -280,6 +280,26 @@ def houdini_parm_name(name): } ] }, + { + 'name': 'Denoising', + 'houdini': {'hidewhen': hidewhen_not_northstar}, + 'settings': [ + { + 'name': 'denoising:enable', + 'ui_name': 'Enable OIDN Denoising', + 'help': 'Apply optional Open Image Denoise 2 on the CPU to Northstar beauty before display transforms. Unavailable builds retain the raw image.', + 'defaultValue': False, + }, + { + 'name': 'denoising:minSamples', + 'ui_name': 'Denoising Start Samples', + 'help': 'Show the raw image until this many samples have accumulated.', + 'defaultValue': 16, + 'minValue': 1, + 'maxValue': 2 ** 16, + }, + ], + }, { 'name': 'AdaptiveSampling', 'houdini': { @@ -531,9 +551,10 @@ def houdini_parm_name(name): { 'name': 'hybrid:upscalingQuality', 'ui_name': 'Upscaling Quality', - 'help': '', + 'help': 'None keeps native resolution while allowing SVGF/ASVGF denoising. Other modes enable FSR2 reconstruction.', 'defaultValue': 'Ultra Performance', 'values': [ + SettingValue('None'), SettingValue('Ultra Quality'), SettingValue('Quality'), SettingValue('Balance'), @@ -1149,7 +1170,7 @@ def process_setting(setting): if generate_ds_files: production_render_setting_categories = [category for category in render_setting_categories if category['name'] != 'ViewportSettings'] generate_houdini_ds(install_path, 'Global', production_render_setting_categories) - viewport_render_setting_categories = [category for category in render_setting_categories if category['name'] in ('RenderQuality', 'Sampling', 'AdaptiveSampling', 'Hybrid', 'ViewportSettings')] + viewport_render_setting_categories = [category for category in render_setting_categories if category['name'] in ('RenderQuality', 'Sampling', 'AdaptiveSampling', 'Denoising', 'Hybrid', 'ViewportSettings')] for category in (cat for cat in viewport_render_setting_categories if cat['name'] == 'RenderQuality'): for setting in (s for s in category['settings'] if s['name'] == 'core:renderQuality'): setting['values'] = [SettingValue(value.get_key(), value.get_key() if value.get_key() != 'Northstar' else 'Full') for value in setting['values']] diff --git a/pxr/imaging/plugin/hdRpr/renderDelegate.cpp b/pxr/imaging/plugin/hdRpr/renderDelegate.cpp index 762c51f29..758a11779 100644 --- a/pxr/imaging/plugin/hdRpr/renderDelegate.cpp +++ b/pxr/imaging/plugin/hdRpr/renderDelegate.cpp @@ -392,7 +392,11 @@ VtDictionary HdRprDelegate::GetRenderStats() const { const std::string previewScale = rprStats.previewDownscale < 0 ? "automatic" : TfStringPrintf("%.1f%%", 100.0 / std::pow(2.0, rprStats.previewDownscale)); - stats["previewScale"] = previewScale; + stats["previewScale"] = rprStats.upscalingMode != "None" + ? rprStats.upscalingMode : previewScale; + stats["rendererBackend"] = rprStats.rendererBackend; + stats["denoisingMode"] = rprStats.denoisingMode; + stats["upscalingMode"] = rprStats.upscalingMode; stats["renderProgressAnnotation"] = TfStringPrintf("%d / %d samples", rprStats.numSamples, rprStats.maxSamples); stats["renderStatsAnnotation"] = std::string(); diff --git a/pxr/imaging/plugin/hdRpr/rprApi.cpp b/pxr/imaging/plugin/hdRpr/rprApi.cpp index eed00cd83..e09acca11 100644 --- a/pxr/imaging/plugin/hdRpr/rprApi.cpp +++ b/pxr/imaging/plugin/hdRpr/rprApi.cpp @@ -1789,6 +1789,7 @@ class HdRprApiImpl { void ResolveFramebuffers() { auto startTime = std::chrono::high_resolution_clock::now(); + m_colorAov->SetDenoisingSampleCount(m_numSamples); m_resolveData.ForAllAovs([&](ResolveData::AovEntry& e) { if (m_isFirstSample || e.isMultiSampled) { @@ -2176,10 +2177,12 @@ class HdRprApiImpl { } if (preferences.IsDirty(HdRprConfig::DirtyHybrid) || force) { - if (preferences.GetHybridDenoising() != HdRprHybridDenoisingTokens->None) { + const auto qualityToken = preferences.GetHybridUpscalingQuality(); + const bool enableUpscaling = preferences.GetHybridDenoising() != HdRprHybridDenoisingTokens->None + && qualityToken != HdRprHybridUpscalingQualityTokens->None; + if (enableUpscaling) { RPR_ERROR_CHECK(m_rprContext->SetParameter(rpr::ContextInfo(RPR_CONTEXT_UPSCALER), RPR_UPSCALER_FSR2), "Failed to set upscaler"); rpr_uint upscaleQuality = RPR_FSR2_QUALITY_MODE_ULTRA_PERFORMANCE; - TfToken qualityToken = preferences.GetHybridUpscalingQuality(); if (qualityToken == HdRprHybridUpscalingQualityTokens->UltraQuality) { upscaleQuality = RPR_FSR2_QUALITY_ULTRA_QUALITY; } else if (qualityToken == HdRprHybridUpscalingQualityTokens->Quality) { @@ -2196,6 +2199,7 @@ class HdRprApiImpl { else { RPR_ERROR_CHECK(m_rprContext->SetParameter(rpr::ContextInfo(RPR_CONTEXT_UPSCALER), RPR_UPSCALER_NONE), "Failed to set upscaler"); } + m_upscalingMode = enableUpscaling ? "FSR2 " + qualityToken.GetString() : "None"; m_dirtyFlags |= ChangeTracker::DirtyScene; } } @@ -2230,6 +2234,7 @@ class HdRprApiImpl { } auto hybridDenoising = preferences.GetHybridDenoising(); + m_denoisingMode = hybridDenoising.GetString(); if (hybridDenoising == HdRprHybridDenoisingTokens->None) { RPR_ERROR_CHECK(m_rprContext->SetParameter(rpr::ContextInfo(RPR_CONTEXT_PT_DENOISER), RPR_DENOISER_NONE), "Failed to set denoiser"); } else if (hybridDenoising == HdRprHybridDenoisingTokens->SVGF) { @@ -2301,6 +2306,17 @@ class HdRprApiImpl { UpdateHybridSettings(preferences, force); } + if (m_rprContextMetadata.pluginType == kPluginNorthstar && + (preferences.IsDirty(HdRprConfig::DirtyDenoising) || force)) { + const bool requested = preferences.GetDenoisingEnable(); + const bool enabled = requested && HdRprApiColorAov::IsDenoisingAvailable(); + auto albedo = enabled ? CreateAov(HdRprAovTokens->albedo) : nullptr; + auto normal = enabled ? CreateAov(HdRprAovTokens->viewShadingNormal) : nullptr; + m_colorAov->SetDenoising(enabled, preferences.GetDenoisingMinSamples(), albedo, normal); + m_denoisingMode = enabled ? "OIDN CPU" : (requested ? "OIDN unavailable" : "None"); + m_dirtyFlags |= ChangeTracker::DirtyAOVRegistry; + } + if (preferences.IsDirty(HdRprConfig::DirtyAlpha) || force || (m_rprContextMetadata.pluginType == kPluginNorthstar && preferences.IsDirty(HdRprConfig::DirtyRenderMode))) { m_isAlphaEnabled = preferences.GetAlphaEnable(); @@ -2660,6 +2676,7 @@ class HdRprApiImpl { }); if (clearAovs) { + m_colorAov->ResetDenoising(); m_numSamples = 0; m_activePixels = -1; m_isFirstSample = true; @@ -3579,6 +3596,10 @@ Don't show this message again? stats.maxSamples = m_maxSamples; stats.previewDownscale = m_previewDownscale; stats.viewportSize = m_viewportSize; + stats.rendererBackend = m_rprContextMetadata.pluginType == kPluginNorthstar ? "Northstar" + : (m_rprContextMetadata.pluginType == kPluginHybridPro ? "HybridPro" : "Hybrid"); + stats.denoisingMode = m_denoisingMode; + stats.upscalingMode = m_upscalingMode; // rprsExport has no progress callback if (!m_rprSceneExportPath.empty()) { @@ -3741,6 +3762,7 @@ Don't show this message again? #endif // HDRPR_ENABLE_VULKAN_INTEROP_SUPPORT void Restart() { + if (m_colorAov) m_colorAov->ResetDenoising(); m_numSamples = 0; } private: @@ -4679,6 +4701,8 @@ Don't show this message again? bool m_isInteractive = false; int m_previewDownscale = 0; + std::string m_denoisingMode = "None"; + std::string m_upscalingMode = "None"; int m_numSamples = 0; int m_numSamplesPerIter = 0; int m_activePixels = -1; diff --git a/pxr/imaging/plugin/hdRpr/rprApi.h b/pxr/imaging/plugin/hdRpr/rprApi.h index 17882f61b..620c5bdaf 100644 --- a/pxr/imaging/plugin/hdRpr/rprApi.h +++ b/pxr/imaging/plugin/hdRpr/rprApi.h @@ -171,6 +171,9 @@ class HdRprApi final { int maxSamples; int previewDownscale; GfVec2i viewportSize; + std::string rendererBackend; + std::string denoisingMode; + std::string upscalingMode; double percentDone; double averageRenderTimePerSample; double averageResolveTimePerSample; diff --git a/pxr/imaging/plugin/hdRpr/rprApiAov.cpp b/pxr/imaging/plugin/hdRpr/rprApiAov.cpp index 43ab63957..f71622070 100644 --- a/pxr/imaging/plugin/hdRpr/rprApiAov.cpp +++ b/pxr/imaging/plugin/hdRpr/rprApiAov.cpp @@ -14,8 +14,13 @@ limitations under the License. #include "rprApiAov.h" #include "rprApi.h" #include "rprApiFramebuffer.h" +#include "cpuDenoiser.h" #include "rifcpp/rifError.h" +#include +#include +#include + #include "pxr/imaging/rprUsd/contextMetadata.h" #include "pxr/imaging/rprUsd/error.h" @@ -267,8 +272,60 @@ void HdRprApiAov::OnSizeChange(rif::Context* rifContext) { HdRprApiColorAov::HdRprApiColorAov(HdFormat format, std::shared_ptr rawColorAov, rpr::Context* rprContext, RprUsdContextMetadata const& rprContextMetadata) : HdRprApiAov(HdRprAovRegistry::GetInstance().GetAovDesc(rpr::Aov(kColorAlpha), true), format) - , m_retainedRawColor(std::move(rawColorAov)) { + , m_retainedRawColor(std::move(rawColorAov)) + , m_isNorthstar(rprContextMetadata.pluginType == kPluginNorthstar) { + +} + +HdRprApiColorAov::~HdRprApiColorAov() { + // Detach consumers before destroying the CPU-denoised RIF source image. + m_filter.reset(); + m_auxFilters.clear(); +} +bool HdRprApiColorAov::IsDenoisingAvailable() { + return HdRprCpuDenoiser::IsAvailable(); +} + +void HdRprApiColorAov::SetDenoising(bool enable, uint32_t minSamples, + std::shared_ptr albedo, + std::shared_ptr normal) { + if (enable && m_isNorthstar && !IsDenoisingAvailable() && !m_denoisingUnavailableWarned) { + TF_WARN("Northstar denoising requires optional Open Image Denoise 2 support; using raw beauty"); + m_denoisingUnavailableWarned = true; + } + enable = enable && m_isNorthstar && IsDenoisingAvailable(); + minSamples = std::max(uint32_t(1), minSamples); + if (!enable) { + albedo.reset(); + normal.reset(); + } + if (enable != m_denoisingEnabled || minSamples != m_denoisingMinSamples || + albedo != m_denoisingAlbedo || normal != m_denoisingNormal) { + m_denoisingEnabled = enable; + m_denoisingMinSamples = minSamples; + m_denoisingAlbedo = std::move(albedo); + m_denoisingNormal = std::move(normal); + m_dirtyBits |= ChangeTracker::DirtySize; + ResetDenoising(); + if (!enable) { + m_cpuDenoiser.reset(); + std::vector().swap(m_denoisingColorBuffer); + std::vector().swap(m_denoisingAlbedoBuffer); + std::vector().swap(m_denoisingNormalBuffer); + std::vector().swap(m_denoisedColorBuffer); + } + } +} + +void HdRprApiColorAov::SetDenoisingSampleCount(uint32_t samples) { + m_denoisingSampleCount = samples; +} + +void HdRprApiColorAov::ResetDenoising() { + m_denoisingOutputValid = false; + m_denoisingFailed = false; + m_denoisingSampleCount = 0; } void HdRprApiColorAov::SetFilter(Filter filter, bool enable) { @@ -461,6 +518,16 @@ void HdRprApiColorAov::Update(HdRprApi const* rprApi, rif::Context* rifContext) } bool HdRprApiColorAov::GetData(void* dstBuffer, size_t dstBufferSize) { + if (m_denoisingEnabled && m_denoisingOutputValid && !m_filter) { + const size_t bytes = m_denoisedColorBuffer.size() * sizeof(float); + if (!dstBuffer || m_format != HdFormatFloat32Vec4 || dstBufferSize < bytes) return false; + std::memcpy(dstBuffer, m_denoisedColorBuffer.data(), bytes); + return true; + } + if (m_denoisingEnabled && !m_denoisingOutputValid) { + auto rawColor = m_retainedRawColor->GetResolvedFb(); + return rawColor && rawColor->GetData(dstBuffer, dstBufferSize); + } if (!m_filter) { if (auto resolvedRawColorFb = m_retainedRawColor->GetResolvedFb()) { return resolvedRawColorFb->GetData(dstBuffer, dstBufferSize); @@ -475,6 +542,9 @@ bool HdRprApiColorAov::GetData(void* dstBuffer, size_t dstBufferSize) { } void HdRprApiColorAov::Resolve() { + if (m_denoisingEnabled) { + ResolveDenoising(); + } HdRprApiAov::Resolve(); for (auto& auxFilter : m_auxFilters) { @@ -482,6 +552,76 @@ void HdRprApiColorAov::Resolve() { } } +void HdRprApiColorAov::ResolveDenoising() { + // Raw AOVs have already been resolved before computed color AOVs. Cache only + // completed denoising; ResetDenoising invalidates this on scene/camera edits. + if (m_denoisingOutputValid && !m_denoisingFailed && + m_denoisingSampleCount >= m_denoisingMinSamples && + m_denoisingOutputSampleCount == m_denoisingSampleCount) return; + m_denoisingOutputValid = false; + auto rawColor = m_retainedRawColor->GetResolvedFb(); + if (!rawColor) return; + const auto desc = rawColor->GetDesc(); + const size_t width = desc.fb_width, height = desc.fb_height; + if (!width || !height || + width > std::numeric_limits::max() / height / 4 / sizeof(float)) return; + const size_t count = width * height * 4; + const size_t bytes = count * sizeof(float); + try { + m_denoisingColorBuffer.resize(count); + if (!rawColor->GetData(m_denoisingColorBuffer.data(), bytes)) return; + m_denoisedColorBuffer = m_denoisingColorBuffer; + if (!m_denoisingFailed && m_denoisingSampleCount >= m_denoisingMinSamples) { + auto readGuide = [=](std::shared_ptr const& aov, + std::vector& buffer) -> bool { + if (!aov) return true; + auto framebuffer = aov->GetResolvedFb(); + if (!framebuffer || !aov->GetAovFb()) return false; + const auto guideDesc = framebuffer->GetDesc(); + if (guideDesc.fb_width != width || guideDesc.fb_height != height) return false; + // Normals are marked single-sampled in the registry. Refresh the + // underlying float4 guide at the current accumulated sample count. + aov->GetAovFb()->Resolve(framebuffer); + buffer.resize(count); + return framebuffer->GetData(buffer.data(), bytes); + }; + std::string error; + if (!readGuide(m_denoisingAlbedo, m_denoisingAlbedoBuffer) || + !readGuide(m_denoisingNormal, m_denoisingNormalBuffer)) { + error = "Cannot read denoising guide AOV"; + } else { + if (!m_cpuDenoiser) m_cpuDenoiser.reset(new HdRprCpuDenoiser); + if (!m_cpuDenoiser->Execute(m_denoisingColorBuffer.data(), + m_denoisingAlbedo ? m_denoisingAlbedoBuffer.data() : nullptr, + m_denoisingNormal ? m_denoisingNormalBuffer.data() : nullptr, + m_denoisedColorBuffer.data(), width, height, &error) && error.empty()) { + error = "Open Image Denoise failed"; + } + } + if (!error.empty()) { + TF_WARN("Northstar denoising failed: %s; using raw beauty until accumulation resets", error.c_str()); + m_denoisingFailed = true; + m_denoisedColorBuffer = m_denoisingColorBuffer; + } + } + // Only post-processing needs a RIF image. Float4 output without display + // filters can use the CPU result directly, including CPU-only rendering. + if (m_denoisedImage) { + void* mapped = nullptr; + RIF_ERROR_CHECK_THROW(rifImageMap(m_denoisedImage->GetHandle(), RIF_IMAGE_MAP_WRITE, &mapped), + "Cannot map denoised beauty image"); + std::memcpy(mapped, m_denoisedColorBuffer.data(), bytes); + RIF_ERROR_CHECK_THROW(rifImageUnmap(m_denoisedImage->GetHandle(), mapped), + "Cannot unmap denoised beauty image"); + } + m_denoisingOutputSampleCount = m_denoisingSampleCount; + m_denoisingOutputValid = true; + } catch (std::exception const& e) { + if (!m_denoisingFailed) TF_WARN("Northstar denoising failed: %s; using raw beauty", e.what()); + m_denoisingFailed = true; + } +} + void HdRprApiColorAov::OnFormatChange(rif::Context* rifContext) { SetFilter(kFilterResample, m_format != HdFormatFloat32Vec4); SetFilter(kFilterComposeOpacity, CanComposeAlpha()); @@ -489,10 +629,10 @@ void HdRprApiColorAov::OnFormatChange(rif::Context* rifContext) { } template -void HdRprApiColorAov::ResizeFilter(int width, int height, Filter filterType, rif::Filter* filter, T input) { +void HdRprApiColorAov::ResizeFilter(int width, int height, Filter filterType, rif::Filter* filter, T input, HdFormat outputFormat) { filter->Resize(width, height); filter->SetInput(rif::Color, input); - filter->SetOutput(rif::Image::GetDesc(width, height, m_format)); + filter->SetOutput(rif::Image::GetDesc(width, height, outputFormat)); if (filterType == kFilterComposeOpacity) { filter->SetInput("alphaImage", m_retainedOpacity->GetResolvedFb()); @@ -509,25 +649,48 @@ void HdRprApiColorAov::ResizeFilter(int width, int height, Filter filterType, ri } void HdRprApiColorAov::OnSizeChange(rif::Context* rifContext) { + m_denoisingOutputValid = false; + m_denoisingFailed = false; + m_denoisedImage.reset(); if (!m_filter) { return; } auto fbDesc = m_retainedRawColor->GetAovFb()->GetDesc(); + if (m_denoisingEnabled && rifContext && fbDesc.fb_width && fbDesc.fb_height) { + try { + m_denoisedImage = rifContext->CreateImage( + rif::Image::GetDesc(fbDesc.fb_width, fbDesc.fb_height, HdFormatFloat32Vec4)); + } catch (std::exception const& e) { + TF_WARN("Cannot allocate denoised beauty image: %s; using raw beauty", e.what()); + m_denoisingEnabled = false; + } + } + auto resizeFirstFilter = [&](Filter filterType, rif::Filter* filter, HdFormat outputFormat) { + if (m_denoisedImage) { + ResizeFilter(fbDesc.fb_width, fbDesc.fb_height, filterType, filter, + m_denoisedImage->GetHandle(), outputFormat); + } else { + ResizeFilter(fbDesc.fb_width, fbDesc.fb_height, filterType, filter, + m_retainedRawColor->GetResolvedFb(), outputFormat); + } + }; if (m_auxFilters.empty()) { - ResizeFilter(fbDesc.fb_width, fbDesc.fb_height, m_mainFilterType, m_filter.get(), m_retainedRawColor->GetResolvedFb()); + resizeFirstFilter(m_mainFilterType, m_filter.get(), m_format); } else { // Ideally we would use "Filter combining" functionality, but it does not work with user-defined filter // So we attach each filter separately auto filter = m_auxFilters.front().second.get(); - ResizeFilter(fbDesc.fb_width, fbDesc.fb_height, m_auxFilters.front().first, filter, m_retainedRawColor->GetResolvedFb()); + // Preserve HDR until tone mapping, and cast only the final display output. + const HdFormat intermediateFormat = m_denoisingEnabled ? HdFormatFloat32Vec4 : m_format; + resizeFirstFilter(m_auxFilters.front().first, filter, intermediateFormat); for (int i = 1; i < m_auxFilters.size(); ++i) { auto filterInput = m_auxFilters[i - 1].second->GetOutput(); - ResizeFilter(fbDesc.fb_width, fbDesc.fb_height, m_auxFilters[i].first, m_auxFilters[i].second.get(), filterInput); + ResizeFilter(fbDesc.fb_width, fbDesc.fb_height, m_auxFilters[i].first, m_auxFilters[i].second.get(), filterInput, intermediateFormat); } - ResizeFilter(fbDesc.fb_width, fbDesc.fb_height, m_mainFilterType, m_filter.get(), m_auxFilters.back().second->GetOutput()); + ResizeFilter(fbDesc.fb_width, fbDesc.fb_height, m_mainFilterType, m_filter.get(), m_auxFilters.back().second->GetOutput(), m_format); } } diff --git a/pxr/imaging/plugin/hdRpr/rprApiAov.h b/pxr/imaging/plugin/hdRpr/rprApiAov.h index dc17d7ab3..6b15a5e92 100644 --- a/pxr/imaging/plugin/hdRpr/rprApiAov.h +++ b/pxr/imaging/plugin/hdRpr/rprApiAov.h @@ -23,6 +23,7 @@ limitations under the License. PXR_NAMESPACE_OPEN_SCOPE class HdRprApi; +class HdRprCpuDenoiser; struct RprUsdContextMetadata; class HdRprApiAov { @@ -74,7 +75,7 @@ class HdRprApiAov { class HdRprApiColorAov : public HdRprApiAov { public: HdRprApiColorAov(HdFormat format, std::shared_ptr rawColorAov, rpr::Context* rprContext, RprUsdContextMetadata const& rprContextMetadata); - ~HdRprApiColorAov() override = default; + ~HdRprApiColorAov() override; void Resize(int width, int height, HdFormat format) override; void Update(HdRprApi const* rprApi, rif::Context* rifContext) override; @@ -83,6 +84,16 @@ class HdRprApiColorAov : public HdRprApiAov { void SetOpacityAov(std::shared_ptr opacity); + static bool IsDenoisingAvailable(); + // Optional Northstar spatial denoising. Guide framebuffers must contain + // resolved RPR float4 albedo and [0,1]-encoded shading/view shading normals. + void SetDenoising(bool enable, uint32_t minSamples, + std::shared_ptr albedo = nullptr, + std::shared_ptr normal = nullptr); + void SetDenoisingSampleCount(uint32_t samples); + // Call whenever render accumulation is cleared, even at unchanged size. + void ResetDenoising(); + struct TonemapParams { bool enable; float exposureTime; @@ -131,16 +142,34 @@ class HdRprApiColorAov : public HdRprApiAov { void SetFilter(Filter filter, bool enable); template - void ResizeFilter(int width, int height, Filter filterType, rif::Filter* filter, T input); + void ResizeFilter(int width, int height, Filter filterType, rif::Filter* filter, T input, HdFormat outputFormat); void SetTonemapFilterParams(rif::Filter* filter); bool CanComposeAlpha(); + void ResolveDenoising(); private: std::shared_ptr m_retainedRawColor; std::shared_ptr m_retainedOpacity; + const bool m_isNorthstar; + bool m_denoisingEnabled = false; + bool m_denoisingFailed = false; + bool m_denoisingOutputValid = false; + bool m_denoisingUnavailableWarned = false; + uint32_t m_denoisingMinSamples = 1; + uint32_t m_denoisingSampleCount = 0; + uint32_t m_denoisingOutputSampleCount = 0; + std::shared_ptr m_denoisingAlbedo; + std::shared_ptr m_denoisingNormal; + std::unique_ptr m_cpuDenoiser; + std::unique_ptr m_denoisedImage; + std::vector m_denoisingColorBuffer; + std::vector m_denoisingAlbedoBuffer; + std::vector m_denoisingNormalBuffer; + std::vector m_denoisedColorBuffer; + Filter m_mainFilterType = kFilterNone; std::vector>> m_auxFilters; diff --git a/pxr/imaging/plugin/rprHoudini/UsdRenderers.json b/pxr/imaging/plugin/rprHoudini/UsdRenderers.json index 16d87d159..8aa4e5cba 100644 --- a/pxr/imaging/plugin/rprHoudini/UsdRenderers.json +++ b/pxr/imaging/plugin/rprHoudini/UsdRenderers.json @@ -9,11 +9,13 @@ "pauseonupdate": false, "allowbackgroundupdate": true, "viewstats": [ + "Render backend", "Render GPU", "CPU render threads", "Samples per second", "Output resolution", - "RPR preview scale" + "RPR preview scale", + "Denoising" ], "statsdatapaths": { "totalClockTime": "frameRenderTotalTime", @@ -23,7 +25,9 @@ "CPU render threads": "threadCountUsed", "Samples per second": "samplesPerSecond", "Output resolution": "outputResolution", - "RPR preview scale": "previewScale" + "RPR preview scale": "previewScale", + "Render backend": "rendererBackend", + "Denoising": "denoisingMode" } } } diff --git a/pxr/imaging/rprUsd/contextHelpers.cpp b/pxr/imaging/rprUsd/contextHelpers.cpp index caf9777d7..28733bf32 100644 --- a/pxr/imaging/rprUsd/contextHelpers.cpp +++ b/pxr/imaging/rprUsd/contextHelpers.cpp @@ -23,6 +23,7 @@ using json = nlohmann::json; #include "pxr/imaging/rprUsd/util.h" #include "pxr/base/arch/env.h" +#include "pxr/base/arch/library.h" #include "pxr/base/tf/diagnostic.h" #include "pxr/base/tf/envSetting.h" @@ -42,7 +43,9 @@ using json = nlohmann::json; #include #endif // __APPLE__ +#include #include +#include #include #include @@ -192,6 +195,7 @@ const std::map kPluginLibNames = { #elif defined __linux__ {kPluginNorthstar, "libNorthstar64.so"}, {kPluginHybrid, "Hybrid.so"}, + {kPluginHybridPro, "HybridPro.so"}, #elif defined __APPLE__ {kPluginNorthstar, "libNorthstar64.dylib"}, #endif @@ -220,6 +224,54 @@ rpr_int GetPluginID(RprUsdPluginType pluginType) { return pluginID; } +std::vector GetHybridGpuIndices(RprUsdPluginType pluginType) { + auto pluginLibNameIter = kPluginLibNames.find(pluginType); + if (pluginLibNameIter == kPluginLibNames.end()) { + return {}; + } + const std::string sdkPath = GetRprSdkPath(); + const std::string pluginPath = sdkPath.empty() ? pluginLibNameIter->second : sdkPath + "/" + pluginLibNameIter->second; + std::unique_ptr library( + ArchLibraryOpen(pluginPath, ARCH_LIBRARY_NOW | ARCH_LIBRARY_LOCAL), &ArchLibraryClose); + + // This optional Hybrid export is absent from older SDKs. Resolve it from + // the registered backend, without requiring Vulkan interop or newer headers. + using GetSupportedDevices = rpr_status (*)(int*, size_t*); + auto getSupportedDevices = library ? reinterpret_cast( + ArchLibraryGetSymbolAddress(library.get(), "rprGetSupportedDevices")) : nullptr; + if (!getSupportedDevices) { + PRINT_CONTEXT_CREATION_DEBUG_INFO("Hybrid device enumeration is unavailable; discovering GPU0 only\n"); + return {0}; + } + + int indices[kMaxNumGpus]; + std::fill_n(indices, kMaxNumGpus, -1); + size_t count = kMaxNumGpus; + rpr_status status = getSupportedDevices(indices, &count); + if (status == RPR_ERROR_UNSUPPORTED) { + return {0}; + } + if (status != RPR_SUCCESS || count > size_t(kMaxNumGpus)) { + TF_RUNTIME_ERROR("Hybrid device enumeration failed (status %d, device count %zu)", status, count); + return {}; + } + + std::vector ret; + bool seen[kMaxNumGpus] = {}; + for (size_t i = 0; i < count; ++i) { + const int index = indices[i]; + if (index < 0 || index >= kMaxNumGpus) { + TF_WARN("Ignoring invalid Hybrid GPU index %d", index); + continue; + } + if (!seen[index]) { + seen[index] = true; + ret.push_back(index); + } + } + return ret; +} + std::string GetGpuName(RprUsdPluginType pluginType, rpr_int pluginID, rpr::CreationFlags creationFlag, rpr::ContextInfo gpuNameId, const char* cachePath) { rpr::CreationFlags additionalFlags = 0x0; if (pluginType == kPluginNorthstar && GetExplicitHipRuntime().requested) { @@ -312,6 +364,7 @@ RprUsdDevicesInfo LoadDevicesConfiguration(RprUsdPluginType pluginType, std::str } RprUsdDevicesInfo ret; + size_t configuredHybridGpuCount = 0; auto isLoaded = [&]() { try { @@ -336,6 +389,19 @@ RprUsdDevicesInfo LoadDevicesConfiguration(RprUsdPluginType pluginType, std::str auto& pluginDevicesConfig = *pluginDevicesConfigIt; + if (RprUsdIsHybrid(pluginType)) { + // Check before validating names: stale entries must not turn an + // explicit multi-GPU selection into a silent GPU0 fallback. + for (auto& gpuConfig : pluginDevicesConfig.at("gpu_configs")) { + if (gpuConfig.at("is_enabled").get()) { + ++configuredHybridGpuCount; + } + } + if (configuredHybridGpuCount > 1) { + return false; + } + } + auto& cpuConfig = pluginDevicesConfig.at("cpu_config"); auto& cpuInfo = cpuConfig.at("cpu_info"); @@ -364,6 +430,15 @@ RprUsdDevicesInfo LoadDevicesConfiguration(RprUsdPluginType pluginType, std::str } }(); + if (!isLoaded && configuredHybridGpuCount > 0) { + if (configuredHybridGpuCount > 1) { + TF_RUNTIME_ERROR("Hybrid supports one GPU per context; enable only one GPU in %s", deviceConfigurationFilepath.c_str()); + } else { + TF_RUNTIME_ERROR("The configured Hybrid GPU is unavailable or its configuration is invalid: %s", deviceConfigurationFilepath.c_str()); + } + return {}; + } + if (!isLoaded || (ret.cpu.numThreads == 0 && ret.gpus.empty())) { // Setup default configuration: either use the first GPU or, if it is not available, CPU @@ -519,9 +594,14 @@ RprUsdDevicesInfo RprUsdGetDevicesInfo(RprUsdPluginType pluginType) { ret.cpu.numThreads = 0; if (!RprUsdIsCpuOnly()) { - std::string name = GetGpuName(pluginType, pluginID, RPR_CREATION_FLAGS_ENABLE_GPU0, RPR_CONTEXT_GPU0_NAME, cachePath.c_str()); - if (!name.empty()) { - ret.gpus.push_back({ 0, name }); + for (int index : GetHybridGpuIndices(pluginType)) { + // Hybrid's name queries address active devices in the context. + // Each discovery context uses one physical GPU, so its name is + // always GPU0_NAME even when the creation flag selects GPU1+. + std::string name = GetGpuName(pluginType, pluginID, kGpuCreationFlags[index], RPR_CONTEXT_GPU0_NAME, cachePath.c_str()); + if (!name.empty()) { + ret.gpus.push_back({index, name}); + } } } } else { From 8367d5bca2d07e9ffe462a7052a8d59aa6c03efe Mon Sep 17 00:00:00 2001 From: Jason Batson Date: Sun, 6 Sep 2026 21:56:51 -0500 Subject: [PATCH 3/5] Fix render cancellation, buffer ownership and context selection Join workers before delegate state is destroyed, preserve stop/pause semantics, and refresh reallocated render buffers. Let Houdini own renderer restarts with optional per-context Hybrid GPU selection. Report failed rendering as incomplete and reset accumulation coherently. Add bounded CPU thread regressions and correct motion-mesh property storage and undefined initialization. Document remaining HybridPro image corruption, GPU timeouts, and SDK teardown crashes; successful sample counters are not accepted image validation. --- BUILD_HOUDINI.md | 47 ++++++++ pxr/imaging/plugin/hdRpr/light.h | 4 +- .../python/generateRenderSettingFiles.py | 11 ++ pxr/imaging/plugin/hdRpr/renderBuffer.cpp | 32 +++-- pxr/imaging/plugin/hdRpr/renderBuffer.h | 4 + pxr/imaging/plugin/hdRpr/renderDelegate.cpp | 44 ++++++- pxr/imaging/plugin/hdRpr/renderDelegate.h | 7 ++ pxr/imaging/plugin/hdRpr/renderParam.cpp | 41 +++---- pxr/imaging/plugin/hdRpr/renderParam.h | 10 +- pxr/imaging/plugin/hdRpr/renderPass.cpp | 17 ++- pxr/imaging/plugin/hdRpr/renderPass.h | 4 + pxr/imaging/plugin/hdRpr/renderThread.cpp | 91 ++++++++------ pxr/imaging/plugin/hdRpr/renderThread.h | 11 +- pxr/imaging/plugin/hdRpr/rprApi.cpp | 87 ++++++++++---- pxr/imaging/plugin/hdRpr/rprApi.h | 1 + pxr/imaging/plugin/hdRpr/testenv/README.md | 37 ++++++ .../hdRpr/testenv/runRenderThreadTests.sh | 39 ++++++ .../hdRpr/testenv/testHdRprPausedShutdown.cpp | 26 ++++ .../hdRpr/testenv/testHdRprRenderThread.cpp | 111 ++++++++++++++++++ .../plugin/rprHoudini/UsdRenderers.json | 8 +- pxr/imaging/rprUsd/contextHelpers.cpp | 19 ++- pxr/imaging/rprUsd/contextMetadata.h | 2 + 22 files changed, 535 insertions(+), 118 deletions(-) create mode 100644 pxr/imaging/plugin/hdRpr/testenv/README.md create mode 100644 pxr/imaging/plugin/hdRpr/testenv/runRenderThreadTests.sh create mode 100644 pxr/imaging/plugin/hdRpr/testenv/testHdRprPausedShutdown.cpp create mode 100644 pxr/imaging/plugin/hdRpr/testenv/testHdRprRenderThread.cpp diff --git a/BUILD_HOUDINI.md b/BUILD_HOUDINI.md index 9a922ea5e..128e875d5 100644 --- a/BUILD_HOUDINI.md +++ b/BUILD_HOUDINI.md @@ -145,3 +145,50 @@ NVIDIA HybridPro also completed a fixed-camera128-sample SVGF capture at native resolution. It reduced noise in some regions while leaving substantial noise in reflections/transmission. ASVGF/FSR2 image quality and temporal behavior remain unvalidated in the GUI; those controls are exposed but not production-approved. + +Renderer lifecycle changes make Houdini the single restart owner. The optional +`rpr:core:hybridGpuIndex` render setting selects one machine-local Hybrid GPU +index; `-1` retains the saved Devices configuration. An explicit index creates +only the selected context and reads its actual GPU name, without creating +throwaway contexts on the other GPUs. Houdini's `restartrendersettings` includes +this setting, render quality, and the legacy OpenCL backend choice. A controller +should author those settings together and must not also queue timed explicit +restarts. These additions do not require Houdini 22 or ROCm 7. + +The delegate joins its worker before destroying configuration, wakes a paused +worker during shutdown, honors blocking/nonblocking stop, and synchronizes AOVs +before restarting. Edit guards now belong to individual delegates. Reallocating +an existing output buffer refreshes its SDK binding and write pointer. Interrupted +or failed renders no longer claim convergence; failed contexts stop submitting +frames and expose an error status. `renderContextId` identifies delegate +recreation for integration checks. CPU-only regression tests are documented in +`pxr/imaging/plugin/hdRpr/testenv/README.md`. + +Live validation remains incomplete. A later NVIDIA HybridPro test on the RTX +2070 Super/595.84 driver stopped after two successful samples with a Vulkan +device loss; the kernel recorded Xid 109 (`CTX SWITCH TIMEOUT`). Attempting to +switch from that lost context crashed inside the SDK's HybridPro context +cleanup before AMD rendering began. Joining the worker correctly does not +establish that this SDK can recover from device loss. Earlier successful +captures must not be taken as proof of switching stability on either GPU. + + +The latest fixed-view sequence is **not accepted image/stability validation**. +AMD HybridPro completed 32 samples, switched to Northstar and back, and later +reported 128 samples; its floor pixels after increasing the sample cap were +exactly one quarter of the previous 32-sample values. Another return from Karma +produced large opaque white HDR bands with unchanged scene/view transform. +Completion counters alone must not pass this test. At 21:37:52 on September 6, +2026, AMD gfx ring timeouts/reset began in the same Houdini process. Switching +subsequently crashed at 21:38:37 in the same HybridPro context deletion stack. +Thus the failure is not confined to the NVIDIA device or merely color management. +The initiating cause remains unisolated; neither SDK nor driver blame is proven. + +Source inspection also corrected motion-mesh property storage that allocated one +entry but wrote three, initialized light transforms/displacement state and timing +counters, and supplied defined clipping defaults when a reconstructed camera has +lens parameters without a clipping range. These corrections need live validation; +they are not presented as a demonstrated fix for the HybridPro GPU resets. +A small direct-SDK accumulation/teardown reproduction has been compiled locally, +but has not yet run. No application restart, screenshot, or GPU test is needed +for the standalone CPU synchronization tests above. diff --git a/pxr/imaging/plugin/hdRpr/light.h b/pxr/imaging/plugin/hdRpr/light.h index f84d8a6a3..7b4d18f45 100644 --- a/pxr/imaging/plugin/hdRpr/light.h +++ b/pxr/imaging/plugin/hdRpr/light.h @@ -77,8 +77,8 @@ class HdRprLight : public HdLight { struct LightReleaser; GfVec3f m_emisionColor = GfVec3f(0.0f); - GfMatrix4f m_transform; - GfMatrix4f m_localTransform; + GfMatrix4f m_transform = GfMatrix4f(1.0f); + GfMatrix4f m_localTransform = GfMatrix4f(1.0f); }; PXR_NAMESPACE_CLOSE_SCOPE diff --git a/pxr/imaging/plugin/hdRpr/python/generateRenderSettingFiles.py b/pxr/imaging/plugin/hdRpr/python/generateRenderSettingFiles.py index cda81468f..1b4d93833 100644 --- a/pxr/imaging/plugin/hdRpr/python/generateRenderSettingFiles.py +++ b/pxr/imaging/plugin/hdRpr/python/generateRenderSettingFiles.py @@ -83,6 +83,17 @@ def houdini_parm_name(name): 'hidewhen': hidewhen_not_northstar } }, + { + 'name': 'core:hybridGpuIndex', + 'ui_name': 'Hybrid GPU Index', + 'help': 'Select one machine-local Hybrid GPU index. -1 uses the saved Devices configuration. Changing this requires a render restart.', + 'defaultValue': -1, + 'minValue': -1, + 'maxValue': 15, + 'houdini': { + 'hidewhen': lambda settings: hidewhen_render_quality('==', 'Northstar', settings) + } + }, { 'name': 'core:legacyToon', 'ui_name': 'Use Legacy RPR Toon', diff --git a/pxr/imaging/plugin/hdRpr/renderBuffer.cpp b/pxr/imaging/plugin/hdRpr/renderBuffer.cpp index 8eeb6824f..781c7b609 100644 --- a/pxr/imaging/plugin/hdRpr/renderBuffer.cpp +++ b/pxr/imaging/plugin/hdRpr/renderBuffer.cpp @@ -17,6 +17,8 @@ limitations under the License. #include "pxr/imaging/hd/sceneDelegate.h" +#include + #ifdef HDRPR_ENABLE_VULKAN_INTEROP_SUPPORT #include #include @@ -39,7 +41,9 @@ void HdRprRenderBuffer::Sync(HdSceneDelegate* sceneDelegate, if (*dirtyBits & DirtyDescription) { // hdRpr has the background thread write directly into render buffers, // so we need to stop the render thread before reallocating them. - static_cast(renderParam)->AcquireRprApiForEdit(); + auto edit = static_cast(renderParam)->AcquireRprApiForEdit(); + HdRenderBuffer::Sync(sceneDelegate, renderParam, dirtyBits); + return; } HdRenderBuffer::Sync(sceneDelegate, renderParam, dirtyBits); @@ -48,7 +52,7 @@ void HdRprRenderBuffer::Sync(HdSceneDelegate* sceneDelegate, void HdRprRenderBuffer::Finalize(HdRenderParam* renderParam) { // hdRpr has the background thread write directly into render buffers, // so we need to stop the render thread before reallocating them. - static_cast(renderParam)->AcquireRprApiForEdit(); + auto edit = static_cast(renderParam)->AcquireRprApiForEdit(); HdRenderBuffer::Finalize(renderParam); } @@ -56,11 +60,21 @@ void HdRprRenderBuffer::Finalize(HdRenderParam* renderParam) { bool HdRprRenderBuffer::Allocate(GfVec3i const& dimensions, HdFormat format, bool multiSampled) { - if (dimensions[2] != 1) { - TF_WARN("HdRprRenderBuffer supports 2D buffers only"); + if (dimensions[0] < 0 || dimensions[1] < 0 || dimensions[2] != 1 || format == HdFormatInvalid) { + TF_WARN("HdRprRenderBuffer requires nonnegative 2D dimensions and a valid format"); return false; } + const size_t pixelSize = HdDataSizeOfFormat(format); + const size_t width = dimensions[0]; + const size_t height = dimensions[1]; + if (width && height && (!pixelSize || + width > std::numeric_limits::max() / pixelSize / height)) { + TF_WARN("HdRprRenderBuffer allocation size overflows"); + return false; + } + const size_t dataByteSize = width * height * pixelSize; + #ifdef ENABLE_MULTITHREADED_RENDER_BUFFER std::unique_lock lock(m_mapMutex); m_mapConditionVar.wait(lock, [this]() { return m_numMappers == 0; }); @@ -72,13 +86,8 @@ bool HdRprRenderBuffer::Allocate(GfVec3i const& dimensions, m_multiSampled = multiSampled; m_isConverged.store(false); - size_t dataByteSize = m_width * m_height * HdDataSizeOfFormat(m_format); - if (dataByteSize) { - m_mappedBuffer.reserve(dataByteSize); - std::memset(m_mappedBuffer.data(), 0, dataByteSize); - } else { - m_mappedBuffer = std::vector(); - } + m_mappedBuffer.assign(dataByteSize, uint8_t(0)); + m_allocationVersion.fetch_add(1); return true; } @@ -95,6 +104,7 @@ void HdRprRenderBuffer::_Deallocate() { m_format = HdFormatInvalid; m_isConverged.store(false); m_mappedBuffer = std::vector(); + m_allocationVersion.fetch_add(1); } void* HdRprRenderBuffer::Map() { diff --git a/pxr/imaging/plugin/hdRpr/renderBuffer.h b/pxr/imaging/plugin/hdRpr/renderBuffer.h index f97357632..6e9e954c1 100644 --- a/pxr/imaging/plugin/hdRpr/renderBuffer.h +++ b/pxr/imaging/plugin/hdRpr/renderBuffer.h @@ -16,6 +16,8 @@ limitations under the License. #include "pxr/imaging/hd/renderBuffer.h" +#include + #include PXR_NAMESPACE_OPEN_SCOPE @@ -58,6 +60,7 @@ class HdRprRenderBuffer final : public HdRenderBuffer { bool IsConverged() const override; void SetConverged(bool converged); + std::uint64_t GetAllocationVersion() const { return m_allocationVersion.load(); } void* GetPointerForWriting() { return m_mappedBuffer.data(); } @@ -76,6 +79,7 @@ class HdRprRenderBuffer final : public HdRenderBuffer { bool m_multiSampled = false; std::atomic m_isConverged; + std::atomic m_allocationVersion {0}; HdRprApi* m_rprApi = nullptr; diff --git a/pxr/imaging/plugin/hdRpr/renderDelegate.cpp b/pxr/imaging/plugin/hdRpr/renderDelegate.cpp index 758a11779..2b67b7a32 100644 --- a/pxr/imaging/plugin/hdRpr/renderDelegate.cpp +++ b/pxr/imaging/plugin/hdRpr/renderDelegate.cpp @@ -49,6 +49,7 @@ limitations under the License. PXR_NAMESPACE_OPEN_SCOPE static HdRprApi* g_rprApi = nullptr; +static std::atomic g_renderContextId {0}; class HdRprDiagnosticMgrDelegate : public TfDiagnosticMgr::Delegate { public: @@ -155,7 +156,8 @@ const TfTokenVector HdRprDelegate::SUPPORTED_BPRIM_TYPES = { HdRprDelegate* HdRprDelegate::m_lastCreatedInstance = nullptr; -HdRprDelegate::HdRprDelegate(HdRenderSettingsMap const& renderSettings) { +HdRprDelegate::HdRprDelegate(HdRenderSettingsMap const& renderSettings) + : m_renderContextId(g_renderContextId.fetch_add(1) + 1) { for (auto& entry : renderSettings) { SetRenderSetting(entry.first, entry.second); } @@ -191,8 +193,15 @@ HdRprDelegate::HdRprDelegate(HdRenderSettingsMap const& renderSettings) { } HdRprDelegate::~HdRprDelegate() { - g_rprApi = nullptr; - m_lastCreatedInstance = nullptr; + // In particular, m_configInstance is declared after m_renderThread and + // would otherwise be destroyed while the callback can still access it. + m_renderThread.StopThread(); + if (g_rprApi == m_rprApi.get()) { + g_rprApi = nullptr; + } + if (m_lastCreatedInstance == this) { + m_lastCreatedInstance = nullptr; + } } HdRenderParam* HdRprDelegate::GetRenderParam() const { @@ -367,6 +376,8 @@ VtDictionary HdRprDelegate::GetRenderStats() const { auto rprStats = m_rprApi->GetRenderStats(); VtDictionary stats; + stats["renderContextId"] = m_renderContextId; + stats["renderStatus"] = rprStats.renderStatus; stats[_tokens->percentDone.GetString()] = rprStats.percentDone; stats["averageRenderTimePerSample"] = rprStats.averageRenderTimePerSample; stats["averageResolveTimePerSample"] = rprStats.averageResolveTimePerSample; @@ -399,7 +410,9 @@ VtDictionary HdRprDelegate::GetRenderStats() const { stats["upscalingMode"] = rprStats.upscalingMode; stats["renderProgressAnnotation"] = TfStringPrintf("%d / %d samples", rprStats.numSamples, rprStats.maxSamples); - stats["renderStatsAnnotation"] = std::string(); + stats["renderStatsAnnotation"] = + (rprStats.renderStatus == "Error - restart required" || + rprStats.renderStatus == "Restart required") ? rprStats.renderStatus : std::string(); return stats; } @@ -408,6 +421,14 @@ bool HdRprDelegate::IsPauseSupported() const { return true; } +bool HdRprDelegate::IsPaused() const { + return m_renderThread.IsPaused(); +} + +bool HdRprDelegate::IsStopped() const { + return m_isStopped.load(); +} + bool HdRprDelegate::Pause() { m_renderThread.PauseRender(); return true; @@ -429,13 +450,26 @@ bool HdRprDelegate::Stop(bool blocking) { #else bool HdRprDelegate::Stop() { #endif + m_isStopped.store(true); +#if PXR_VERSION >= 2203 + m_renderThread.StopRender(blocking); +#else m_renderThread.StopRender(); +#endif return true; } bool HdRprDelegate::Restart() { + // Finish a possibly nonblocking Stop before resetting convergence. The + // render pass will synchronize pending settings/AOVs before starting again. + m_renderThread.StopRender(); m_renderParam->RestartRender(); - m_renderThread.StartRender(); + for (auto const& binding : m_rprApi->GetAovBindings()) { + if (binding.renderBuffer) { + static_cast(binding.renderBuffer)->SetConverged(false); + } + } + m_isStopped.store(false); return true; } diff --git a/pxr/imaging/plugin/hdRpr/renderDelegate.h b/pxr/imaging/plugin/hdRpr/renderDelegate.h index 4aebcfe92..da93084d5 100644 --- a/pxr/imaging/plugin/hdRpr/renderDelegate.h +++ b/pxr/imaging/plugin/hdRpr/renderDelegate.h @@ -20,6 +20,8 @@ limitations under the License. #include "pxr/imaging/hd/renderDelegate.h" +#include + PXR_NAMESPACE_OPEN_SCOPE #if PXR_VERSION >= 2102 @@ -89,6 +91,9 @@ class HdRprDelegate final : public HdRenderDelegate { VtDictionary GetRenderStats() const override; bool IsPauseSupported() const override; + // These queries also compile with USD versions predating their virtual API. + bool IsPaused() const; + bool IsStopped() const; bool Pause() override; bool Resume() override; @@ -114,6 +119,8 @@ class HdRprDelegate final : public HdRenderDelegate { static const TfTokenVector SUPPORTED_SPRIM_TYPES; static const TfTokenVector SUPPORTED_BPRIM_TYPES; + const std::uint64_t m_renderContextId; + std::atomic m_isStopped {false}; std::unique_ptr m_rprApi; std::unique_ptr m_renderParam; HdRenderSettingDescriptorList m_settingDescriptors; diff --git a/pxr/imaging/plugin/hdRpr/renderParam.cpp b/pxr/imaging/plugin/hdRpr/renderParam.cpp index e74c28cf7..dc7cb709d 100644 --- a/pxr/imaging/plugin/hdRpr/renderParam.cpp +++ b/pxr/imaging/plugin/hdRpr/renderParam.cpp @@ -82,45 +82,32 @@ void HdRprRenderParam::MaterialDidChange(HdSceneDelegate* sceneDelegate, SdfPath } } -size_t RprApiSafeWrapper::m_ptrCounter = 0; -std::mutex RprApiSafeWrapper::m_threadControlMutex; - RprApiSafeWrapper::RprApiSafeWrapper(HdRprRenderParam* renderParam, HdRprApi* rprApi) : m_renderParam(renderParam) - , m_rprApi(rprApi) -{ - std::lock_guard lock(m_threadControlMutex); - if (m_ptrCounter == 0) - { + , m_rprApi(rprApi) { + std::lock_guard lock(m_renderParam->m_threadControlMutex); + if (m_renderParam->m_editCount == 0) { m_renderParam->GetRenderThread()->StopRender(); } - - ++m_ptrCounter; + ++m_renderParam->m_editCount; } -RprApiSafeWrapper::RprApiSafeWrapper(RprApiSafeWrapper&& other) -{ - std::lock_guard lock(m_threadControlMutex); - if (this == &other) - return; - - m_renderParam = other.m_renderParam; +RprApiSafeWrapper::RprApiSafeWrapper(RprApiSafeWrapper&& other) noexcept + : m_renderParam(other.m_renderParam) + , m_rprApi(other.m_rprApi) { + // Transfer the existing edit ownership; a moved-from guard owns no count. other.m_renderParam = nullptr; - - m_rprApi = other.m_rprApi; other.m_rprApi = nullptr; - - ++m_ptrCounter; } -RprApiSafeWrapper::~RprApiSafeWrapper() -{ - std::lock_guard lock(m_threadControlMutex); - if (--m_ptrCounter != 0) +RprApiSafeWrapper::~RprApiSafeWrapper() { + if (!m_renderParam) { return; - - if (m_renderParam) + } + std::lock_guard lock(m_renderParam->m_threadControlMutex); + if (--m_renderParam->m_editCount == 0) { m_renderParam->RestartRender(); + } } PXR_NAMESPACE_CLOSE_SCOPE diff --git a/pxr/imaging/plugin/hdRpr/renderParam.h b/pxr/imaging/plugin/hdRpr/renderParam.h index a0bb81e46..52d4a574c 100644 --- a/pxr/imaging/plugin/hdRpr/renderParam.h +++ b/pxr/imaging/plugin/hdRpr/renderParam.h @@ -37,7 +37,7 @@ class RprApiSafeWrapper final RprApiSafeWrapper(const RprApiSafeWrapper& other) = delete; public: - RprApiSafeWrapper(RprApiSafeWrapper&& other); + RprApiSafeWrapper(RprApiSafeWrapper&& other) noexcept; public: virtual ~RprApiSafeWrapper(); @@ -50,8 +50,6 @@ class RprApiSafeWrapper final private: HdRprRenderParam* m_renderParam; HdRprApi* m_rprApi; - static size_t m_ptrCounter; - static std::mutex m_threadControlMutex; }; class HdRprRenderParam final : public HdRenderParam { @@ -85,6 +83,10 @@ class HdRprRenderParam final : public HdRenderParam { bool IsRenderShouldBeRestarted() { return m_restartRender.exchange(false); } private: + friend class RprApiSafeWrapper; + std::mutex m_threadControlMutex; + size_t m_editCount = 0; + HdRprApi* m_rprApi; HdRprRenderThread* m_renderThread; @@ -94,7 +96,7 @@ class HdRprRenderParam final : public HdRenderParam { std::mutex m_materialSubscriptionsMutex; std::map> m_materialSubscriptions; - std::atomic m_restartRender; + std::atomic m_restartRender {false}; }; PXR_NAMESPACE_CLOSE_SCOPE diff --git a/pxr/imaging/plugin/hdRpr/renderPass.cpp b/pxr/imaging/plugin/hdRpr/renderPass.cpp index 7cf6a303d..56f956126 100644 --- a/pxr/imaging/plugin/hdRpr/renderPass.cpp +++ b/pxr/imaging/plugin/hdRpr/renderPass.cpp @@ -58,10 +58,10 @@ void HdRprRenderPass::_Execute(HdRenderPassStateSharedPtr const& renderPassState // It could be solved in another way by using shared_mutex and // marking current write-lock as read-only after successful config->Sync // in such a way main and render threads would have read-only-locks that could coexist + auto renderDelegate = static_cast(GetRenderIndex()->GetRenderDelegate()); bool stopRender = false; { HdRprConfig* config; - auto renderDelegate = reinterpret_cast(GetRenderIndex()->GetRenderDelegate()); auto configInstanceLock = renderDelegate->LockConfigInstance(&config); config->Sync(renderDelegate); if (config->IsDirty(HdRprConfig::DirtyAll)) { @@ -80,16 +80,25 @@ void HdRprRenderPass::_Execute(HdRenderPassStateSharedPtr const& renderPassState m_renderParam->AcquireRprApiForEdit()->SetViewportSize(newViewportSize); } - if (rprApiConst->GetAovBindings() != renderPassState->GetAovBindings()) { + // Hydra may reallocate a buffer without replacing the binding or changing + // the viewport. Rebind to refresh both its CPU pointer and RPR AOV format. + std::vector aovBufferVersions; + for (auto const& binding : renderPassState->GetAovBindings()) { + auto buffer = static_cast(binding.renderBuffer); + aovBufferVersions.push_back(buffer ? buffer->GetAllocationVersion() : 0); + } + if (rprApiConst->GetAovBindings() != renderPassState->GetAovBindings() || + m_aovBufferVersions != aovBufferVersions) { m_renderParam->AcquireRprApiForEdit()->SetAovBindings(renderPassState->GetAovBindings()); + m_aovBufferVersions = std::move(aovBufferVersions); } if (rprApiConst->GetCamera() != renderPassState->GetCamera()) { m_renderParam->AcquireRprApiForEdit()->SetCamera(renderPassState->GetCamera()); } - if (m_renderParam->IsRenderShouldBeRestarted() || - rprApiConst->IsChanged()) { + if (!renderDelegate->IsStopped() && + (m_renderParam->IsRenderShouldBeRestarted() || rprApiConst->IsChanged())) { for (auto& aovBinding : renderPassState->GetAovBindings()) { if (aovBinding.renderBuffer) { auto rprRenderBuffer = static_cast(aovBinding.renderBuffer); diff --git a/pxr/imaging/plugin/hdRpr/renderPass.h b/pxr/imaging/plugin/hdRpr/renderPass.h index 0f0719192..0f87c5210 100644 --- a/pxr/imaging/plugin/hdRpr/renderPass.h +++ b/pxr/imaging/plugin/hdRpr/renderPass.h @@ -18,6 +18,9 @@ limitations under the License. #include "rprApi.h" +#include +#include + PXR_NAMESPACE_OPEN_SCOPE class HdRprRenderParam; @@ -37,6 +40,7 @@ class HdRprRenderPass final : public HdRenderPass { private: HdRprRenderParam* m_renderParam; + std::vector m_aovBufferVersions; }; PXR_NAMESPACE_CLOSE_SCOPE diff --git a/pxr/imaging/plugin/hdRpr/renderThread.cpp b/pxr/imaging/plugin/hdRpr/renderThread.cpp index 5d5eb9412..d4cce12cd 100644 --- a/pxr/imaging/plugin/hdRpr/renderThread.cpp +++ b/pxr/imaging/plugin/hdRpr/renderThread.cpp @@ -22,8 +22,8 @@ HdRprRenderThread::HdRprRenderThread() , m_renderCallback([]() { TF_CODING_ERROR("StartThread() called without a render callback set"); }) , m_shutdownCallback([]() {}) , m_requestedState(StateInitial) - , m_stopRequested(false) , m_pauseRender(false) + , m_stopRequested(false) , m_rendering(false) { } @@ -51,6 +51,7 @@ void HdRprRenderThread::SetShutdownCallback(std::function shutdownCallba } void HdRprRenderThread::StartThread() { + std::lock_guard controlLock(m_controlMutex); if (m_renderThread.joinable()) { TF_CODING_ERROR("StartThread() called while render thread is already running"); return; @@ -61,44 +62,68 @@ void HdRprRenderThread::StartThread() { } void HdRprRenderThread::StopThread() { + std::lock_guard controlLock(m_controlMutex); if (!m_renderThread.joinable()) { return; } + // Wake a paused callback and request SDK abort before waiting for the + // callback's state mutex. All delegate members are still alive here. + RequestStop(); { - m_enableRender.clear(); std::unique_lock lock(m_requestedStateMutex); m_requestedState = StateTerminated; - m_requestedStateCV.notify_one(); } + m_requestedStateCV.notify_one(); m_renderThread.join(); } bool HdRprRenderThread::IsThreadRunning() { + std::lock_guard controlLock(m_controlMutex); return m_renderThread.joinable(); } void HdRprRenderThread::StartRender() { - if (!IsRendering()) { - std::unique_lock lock(m_requestedStateMutex); - m_enableRender.test_and_set(); - m_requestedState = StateRendering; - m_rendering.store(true); - m_requestedStateCV.notify_one(); + std::lock_guard controlLock(m_controlMutex); + if (IsRendering()) { + return; + } + std::unique_lock lock(m_requestedStateMutex); + if (m_requestedState != StateIdle) { + return; } + { + std::lock_guard pauseLock(m_pauseWaitMutex); + m_stopRequested.store(false); + } + m_requestedState = StateRendering; + m_rendering.store(true); + m_requestedStateCV.notify_one(); } -void HdRprRenderThread::StopRender() { +void HdRprRenderThread::RequestStop() { + { + // The wait predicate and notification share this mutex, preventing a + // stop request from being lost between the pause check and wait. + std::lock_guard lock(m_pauseWaitMutex); + m_stopRequested.store(true); + } + m_pauseWaitCV.notify_all(); if (IsRendering()) { - m_enableRender.clear(); - if (m_pauseRender) { - // In case rendering thread was blocked by WaitUntilPaused, notify that stop is requested - m_pauseWaitCV.notify_one(); - } - // In case rendering thread currently inside of some sort of rendering task that could be stopped call stopCallback to speed up return from renderCallback m_stopCallback(); + } +} + +void HdRprRenderThread::StopRender(bool blocking) { + std::lock_guard controlLock(m_controlMutex); + RequestStop(); + if (blocking) { + // RenderLoop holds this mutex throughout the callback. Acquiring it + // guarantees that the callback has finished before scene edits begin. std::unique_lock lock(m_requestedStateMutex); - m_requestedState = StateIdle; + if (m_requestedState == StateRendering) { + m_requestedState = StateIdle; + } m_rendering.store(false); } } @@ -108,33 +133,32 @@ bool HdRprRenderThread::IsRendering() { } bool HdRprRenderThread::IsStopRequested() { - if (!m_enableRender.test_and_set()) { - m_stopRequested = true; - } - - return m_stopRequested; + return m_stopRequested.load(); } void HdRprRenderThread::PauseRender() { - std::unique_lock lock(m_pauseWaitMutex); + std::lock_guard lock(m_pauseWaitMutex); m_pauseRender = true; } void HdRprRenderThread::ResumeRender() { - std::unique_lock lock(m_pauseWaitMutex); - m_pauseRender = false; - m_pauseWaitCV.notify_one(); + { + std::lock_guard lock(m_pauseWaitMutex); + m_pauseRender = false; + } + m_pauseWaitCV.notify_all(); } -void HdRprRenderThread::WaitUntilPaused() { - if (!m_pauseRender || IsStopRequested()) { - return; - } +bool HdRprRenderThread::IsPaused() const { + std::lock_guard lock(m_pauseWaitMutex); + return m_pauseRender; +} +void HdRprRenderThread::WaitUntilPaused() { std::unique_lock lock(m_pauseWaitMutex); - while (m_pauseRender && !IsStopRequested()) { - m_pauseWaitCV.wait(lock); - } + m_pauseWaitCV.wait(lock, [this]() { + return !m_pauseRender || IsStopRequested(); + }); } void HdRprRenderThread::RenderLoop() { @@ -145,7 +169,6 @@ void HdRprRenderThread::RenderLoop() { }); if (m_requestedState == StateRendering) { m_renderCallback(); - m_stopRequested = false; m_rendering.store(false); m_requestedState = StateIdle; } else if (m_requestedState == StateTerminated) { diff --git a/pxr/imaging/plugin/hdRpr/renderThread.h b/pxr/imaging/plugin/hdRpr/renderThread.h index 1075aca3d..4884ecfd2 100644 --- a/pxr/imaging/plugin/hdRpr/renderThread.h +++ b/pxr/imaging/plugin/hdRpr/renderThread.h @@ -38,17 +38,19 @@ class HdRprRenderThread { bool IsThreadRunning(); void StartRender(); - void StopRender(); + void StopRender(bool blocking = true); bool IsStopRequested(); bool IsRendering(); void PauseRender(); void ResumeRender(); + bool IsPaused() const; void WaitUntilPaused(); private: void RenderLoop(); + void RequestStop(); std::function m_stopCallback; std::function m_renderCallback; @@ -61,16 +63,17 @@ class HdRprRenderThread { StateTerminated, }; + // Serialize control calls without holding a mutex needed by the worker. + std::mutex m_controlMutex; State m_requestedState; std::mutex m_requestedStateMutex; std::condition_variable m_requestedStateCV; - std::mutex m_pauseWaitMutex; + mutable std::mutex m_pauseWaitMutex; std::condition_variable m_pauseWaitCV; bool m_pauseRender; - std::atomic_flag m_enableRender; - bool m_stopRequested; + std::atomic m_stopRequested; std::atomic m_rendering; std::thread m_renderThread; diff --git a/pxr/imaging/plugin/hdRpr/rprApi.cpp b/pxr/imaging/plugin/hdRpr/rprApi.cpp index e09acca11..f511a99ac 100644 --- a/pxr/imaging/plugin/hdRpr/rprApi.cpp +++ b/pxr/imaging/plugin/hdRpr/rprApi.cpp @@ -613,9 +613,8 @@ class HdRprApiImpl { } if (numMeshSamples > 1) { - meshProperties[0] = (rpr_mesh_info)RPR_MESH_MOTION_DIMENSION; - meshProperties[1] = (rpr_mesh_info)numMeshSamples; - meshProperties[2] = (rpr_mesh_info)0; + meshProperties = {rpr_mesh_info(RPR_MESH_MOTION_DIMENSION), + rpr_mesh_info(numMeshSamples), rpr_mesh_info(0)}; mergedPoints = MergeSamples(&pointSamples, numMeshSamples, &pointsData, &numPoints); mergedNormals = MergeSamples(&normalSamples, numMeshSamples, &normalsData, &numNormals); @@ -1788,6 +1787,7 @@ class HdRprApiImpl { } void ResolveFramebuffers() { + if (m_state == kStateInvalid) return; auto startTime = std::chrono::high_resolution_clock::now(); m_colorAov->SetDenoisingSampleCount(m_numSamples); @@ -1803,7 +1803,11 @@ class HdRprApiImpl { for (auto& outRb : m_outputRenderBuffers) { if (outRb.mappedData && (m_isFirstSample || outRb.isMultiSampled)) { - outRb.rprAov->GetData(outRb.mappedData, outRb.mappedDataSize); + if (!outRb.rprAov->GetData(outRb.mappedData, outRb.mappedDataSize)) { + TF_RUNTIME_ERROR("Failed to resolve %s render buffer; context requires restart", outRb.aovName.GetText()); + m_state = kStateInvalid; + return; + } } } @@ -1873,7 +1877,9 @@ class HdRprApiImpl { auto newPlugin = GetPluginType(m_currentRenderQuality); auto activePlugin = m_rprContextMetadata.pluginType; bool backendChanged = activePlugin == kPluginNorthstar && m_rprContextMetadata.useOpenCL != config->GetCoreUseOpenCL(); - m_state = (newPlugin != activePlugin || backendChanged) ? kStateRestartRequired : kStateRender; + bool deviceChanged = RprUsdIsHybrid(activePlugin) && + m_rprContextMetadata.hybridGpuIndex != config->GetCoreHybridGpuIndex(); + m_state = (newPlugin != activePlugin || backendChanged || deviceChanged) ? kStateRestartRequired : kStateRender; } if (m_state == kStateRender && config->IsDirty(HdRprConfig::DirtyRenderQuality)) { @@ -2451,8 +2457,11 @@ class HdRprApiImpl { float sensorHeight; float focalLength; - float nearPlane; - float farPlane; + // Physical lens parameters can be present without clippingRange. + // Keep the same fallback range in that case instead of sending garbage + // to the SDK after a viewport/delegate camera is reconstructed. + float nearPlane = 0.01f; + float farPlane = 100000000.0f; GfVec2f apertureSize; GfVec2f apertureOffset(0.0f); @@ -3009,7 +3018,7 @@ class HdRprApiImpl { // active pixels as often as possible const bool isAdaptiveSamplingEnabled = IsAdaptiveSamplingEnabled(); - while (!IsConverged()) { + while (m_state == kStateRender && !IsConverged()) { if (renderThread->IsStopRequested()) { break; } @@ -3026,6 +3035,9 @@ class HdRprApiImpl { if (status != RPR_SUCCESS && status != RPR_ERROR_ABORTED) { RPR_ERROR_CHECK(status, "Failed to render", m_rprContext.get()); + // A failed context must not be submitted again on the next + // viewport update (in particular after Vulkan device loss). + m_state = kStateInvalid; break; } @@ -3072,7 +3084,9 @@ class HdRprApiImpl { } } - ResolveFramebuffers(); + if (m_state == kStateRender) { + ResolveFramebuffers(); + } } static void RenderUpdateCallback(float progress, void* dataPtr) { @@ -3128,7 +3142,7 @@ class HdRprApiImpl { cd.SetForTile(m_camera, m_hdCamera, windowNDC); } - while (!IsConverged()) { + while (m_state == kStateRender && !IsConverged()) { // In interactive mode, always render at least one frame, otherwise // disturbing full-screen-flickering will be visible or // viewport will not update because of fast exit due to abort @@ -3143,7 +3157,8 @@ class HdRprApiImpl { if (progressivelyIncreaseSamplesPerIter) { // 1, 1, 2, 4, 8, ... - int numSamplesPerIter = std::max(int(pow(2, int(log2(m_numSamples)))), 1); + int numSamplesPerIter = m_numSamples > 0 + ? std::max(int(pow(2, int(log2(m_numSamples)))), 1) : 1; // Make sure we will not oversample the image int numSamplesLeft = std::min(numSamplesPerIter, m_maxSamples - m_numSamples); @@ -3173,6 +3188,9 @@ class HdRprApiImpl { if (status != RPR_SUCCESS && status != RPR_ERROR_ABORTED) { RPR_ERROR_CHECK(status, "Failed to render", m_rprContext.get()); + // A failed context must not be submitted again on the next + // viewport update (in particular after Vulkan device loss). + m_state = kStateInvalid; break; } @@ -3183,6 +3201,9 @@ class HdRprApiImpl { if (m_resolveMode == kResolveAfterRender) { ResolveFramebuffers(); } + if (m_state != kStateRender) { + break; + } if (IsAdaptiveSamplingEnabled() && m_numSamples >= m_minSamples && RPR_ERROR_CHECK(m_rprContext->GetInfo(RPR_CONTEXT_ACTIVE_PIXEL_COUNT, sizeof(m_activePixels), &m_activePixels, NULL), "Failed to query active pixels")) { @@ -3243,7 +3264,7 @@ class HdRprApiImpl { #endif // HDRPR_ENABLE_VULKAN_INTEROP_SUPPORT void RenderFrame(HdRprRenderThread* renderThread) { - if (!m_rprContext) { + if (!m_rprContext || m_state == kStateInvalid) { return; } @@ -3251,6 +3272,7 @@ class HdRprApiImpl { Update(); } catch (std::runtime_error const& e) { TF_RUNTIME_ERROR("Failed to update: %s", e.what()); + m_state = kStateInvalid; m_dirtyFlags = ChangeTracker::Clean; if (m_hdCamera) { m_hdCamera->CleanDirtyBits(); @@ -3280,9 +3302,12 @@ class HdRprApiImpl { RenderImpl(renderThread); } } - SaveCryptomatte(); + if (m_state == kStateRender) { + SaveCryptomatte(); + } } catch (std::runtime_error const& e) { TF_RUNTIME_ERROR("Failed to render frame: %s", e.what()); + m_state = kStateInvalid; } } else if (m_state == kStateRestartRequired) { { @@ -3566,9 +3591,12 @@ Don't show this message again? m_startTime = std::chrono::high_resolution_clock::now(); RenderFrame(renderThread); + // Returning from the worker is not proof of convergence: it may have + // stopped for a camera/settings edit, a required restart, or an error. + const bool converged = m_state == kStateRender && IsConverged(); for (auto& aovBinding : m_aovBindings) { if (auto rb = static_cast(aovBinding.renderBuffer)) { - rb->SetConverged(true); + rb->SetConverged(converged); } } } @@ -3600,13 +3628,17 @@ Don't show this message again? : (m_rprContextMetadata.pluginType == kPluginHybridPro ? "HybridPro" : "Hybrid"); stats.denoisingMode = m_denoisingMode; stats.upscalingMode = m_upscalingMode; + stats.renderStatus = m_state == kStateInvalid ? "Error - restart required" + : m_state == kStateRestartRequired ? "Restart required" + : m_state == kStateUninitialized ? "Initializing" + : IsConverged() ? "Converged" : "Rendering"; // rprsExport has no progress callback if (!m_rprSceneExportPath.empty()) { return stats; } - double progress = double(m_numSamples) / m_maxSamples; + double progress = m_maxSamples > 0 ? double(m_numSamples) / m_maxSamples : 0.0; if (m_activePixels != -1) { int numPixels = m_viewportSize[0] * m_viewportSize[1]; progress = std::max(progress, double(numPixels - m_activePixels) / numPixels); @@ -3628,7 +3660,10 @@ Don't show this message again? stats.frameRenderTotalTime = (double)m_frameRenderTotalTime.count() / 1000000000.0; stats.frameResolveTotalTime = (double)m_frameResolveTotalTime.count() / 1000000000.0; - stats.totalRenderTime = (double)(std::chrono::high_resolution_clock::now().time_since_epoch() - m_startTime.time_since_epoch()).count() / 1000000000.0; + if (m_startTime != std::chrono::high_resolution_clock::time_point()) { + stats.totalRenderTime = std::chrono::duration( + std::chrono::high_resolution_clock::now() - m_startTime).count(); + } stats.syncTime = (double)m_syncTime.count() / 1000000000.0; stats.cacheCreationTime = (double)m_cacheCreationTime.count() / 1000000000.0; @@ -3661,7 +3696,8 @@ Don't show this message again? return m_numSamples == 1; } - return (m_numSamples >= m_maxSamples) || (m_activePixels == 0); + return (m_numSamples >= m_maxSamples) || + (IsAdaptiveSamplingEnabled() && m_numSamples >= m_minSamples && m_activePixels == 0); } bool IsAdaptiveSamplingEnabled() const { @@ -3762,8 +3798,10 @@ Don't show this message again? #endif // HDRPR_ENABLE_VULKAN_INTEROP_SUPPORT void Restart() { - if (m_colorAov) m_colorAov->ResetDenoising(); - m_numSamples = 0; + // Let UpdateAovs clear every SDK framebuffer and reset sample count, + // adaptive coverage, first-sample AOVs, iteration size and denoising as + // one operation, before the next accumulation starts. + m_dirtyFlags |= ChangeTracker::DirtyScene; } private: static RprUsdPluginType GetPluginType(TfToken const& renderQuality) { @@ -3865,6 +3903,7 @@ Don't show this message again? flipRequestedByRenderSetting = config->GetCoreFlipVertical(); useGmon = config->GetCoreUseGmon(); m_rprContextMetadata.useOpenCL = config->GetCoreUseOpenCL(); + m_rprContextMetadata.hybridGpuIndex = config->GetCoreHybridGpuIndex(); m_rprContextMetadata.pluginType = GetPluginType(m_currentRenderQuality); @@ -4710,15 +4749,15 @@ Don't show this message again? int m_minSamples = 0; float m_varianceThreshold = 0.0f; TfToken m_currentRenderQuality; - bool m_hybridDisplacement; + bool m_hybridDisplacement = false; using Duration = std::chrono::high_resolution_clock::duration; - Duration m_frameRenderTotalTime; - Duration m_frameResolveTotalTime; + Duration m_frameRenderTotalTime = {}; + Duration m_frameResolveTotalTime = {}; std::chrono::high_resolution_clock::time_point m_startTime = {}; std::chrono::high_resolution_clock::time_point m_syncStartTime = {}; - Duration m_syncTime; - Duration m_cacheCreationTime; + Duration m_syncTime = {}; + Duration m_cacheCreationTime = {}; bool m_cacheCreationRequired = false; struct RenderUpdateCallbackData { diff --git a/pxr/imaging/plugin/hdRpr/rprApi.h b/pxr/imaging/plugin/hdRpr/rprApi.h index 620c5bdaf..1e7c3b1ea 100644 --- a/pxr/imaging/plugin/hdRpr/rprApi.h +++ b/pxr/imaging/plugin/hdRpr/rprApi.h @@ -174,6 +174,7 @@ class HdRprApi final { std::string rendererBackend; std::string denoisingMode; std::string upscalingMode; + std::string renderStatus; double percentDone; double averageRenderTimePerSample; double averageResolveTimePerSample; diff --git a/pxr/imaging/plugin/hdRpr/testenv/README.md b/pxr/imaging/plugin/hdRpr/testenv/README.md new file mode 100644 index 000000000..6d39b008a --- /dev/null +++ b/pxr/imaging/plugin/hdRpr/testenv/README.md @@ -0,0 +1,37 @@ +These standalone Linux CPU tests compile the real `renderThread.cpp` against +Houdini's HDK headers and link only USD Tf/Arch. They do not load hdRpr, create +an RPR context, launch Houdini, or use a GPU. No plugin build/install is needed. + +From this directory, with a C++ compiler and GNU `timeout` installed: + +```bash +bash runRenderThreadTests.sh /path/to/hfs22.0.429 +ASAN_OPTIONS=detect_leaks=0 bash runRenderThreadTests.sh /path/to/hfs22.0.429 20 address,undefined +``` + +The second argument selects the C++ standard required by the HDK (20 for +Houdini 22; use 17 for a compatible older HDK). The optional third argument +passes a sanitizer list to the compiler. Leak detection is disabled in the +example because LeakSanitizer cannot run under the ptrace-based execution +sandbox used for this validation; address and undefined-behavior checks remain +active. Outside such a sandbox it can be left enabled. Temporary binaries are +removed when the script exits. + +`testHdRprPausedShutdown.cpp` is a small regression that also compiles with the +old thread interface. The worker enters a paused callback before its owner +calls `StopThread()`. With the pre-fix implementation, a 3-second timeout exits +124; with the fix, it exits 0. To reproduce the old failure, compile this test +against the pre-fix `renderThread.h` and `renderThread.cpp`, then run it with +`timeout 3s`. + +`testHdRprRenderThread.cpp` covers repeated idle shutdown/thread recreation, +100 paused shutdown attempts, abort callback delivery for in-flight work, +nonblocking then blocking stop, restart after cancellation while preserving +pause, 300 concurrent pause/start/stop cycles, and destruction before callback +state expires. Internal event deadlines and the process timeout turn hangs into +failures. The full suite requires the new `StopRender(bool)` and `IsPaused()` +methods. + +These tests exercise thread ownership and synchronization. Actual delegate +recreation, SDK device-loss cleanup, AOV allocation/rebinding, and rendering +need separate host integration checks. diff --git a/pxr/imaging/plugin/hdRpr/testenv/runRenderThreadTests.sh b/pxr/imaging/plugin/hdRpr/testenv/runRenderThreadTests.sh new file mode 100644 index 000000000..4987f8b25 --- /dev/null +++ b/pxr/imaging/plugin/hdRpr/testenv/runRenderThreadTests.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -lt 1 || $# -gt 3 ]]; then + echo "Usage: $0 /path/to/houdini [c++-standard=20] [sanitizers]" >&2 + exit 2 +fi + +hd_rpr_hfs=$1 +hd_rpr_standard=${2:-20} +hd_rpr_sanitizers=${3:-} +hd_rpr_cxx=${CXX:-c++} +hd_rpr_test_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +hd_rpr_source_dir=$(cd -- "$hd_rpr_test_dir/.." && pwd) +hd_rpr_build_dir=$(mktemp -d "${TMPDIR:-/tmp}/hdRpr-thread-tests.XXXXXX") +trap 'rm -rf -- "$hd_rpr_build_dir"' EXIT + +hd_rpr_flags=( + "-std=c++$hd_rpr_standard" -DNDEBUG -pthread -O1 -g -Wall -Wextra + -Wno-deprecated -Wno-deprecated-declarations + "-I$hd_rpr_hfs/toolkit/include" "-I$hd_rpr_source_dir" +) +# Tf diagnostics can include Houdini's Python headers indirectly. +for hd_rpr_python_include in "$hd_rpr_hfs"/toolkit/include/python3.*; do + [[ ! -d "$hd_rpr_python_include" ]] || hd_rpr_flags+=("-I$hd_rpr_python_include") +done +if [[ -n "$hd_rpr_sanitizers" ]]; then + hd_rpr_flags+=("-fsanitize=$hd_rpr_sanitizers" -fno-omit-frame-pointer) +fi + +for hd_rpr_test in testHdRprPausedShutdown testHdRprRenderThread; do + "$hd_rpr_cxx" "${hd_rpr_flags[@]}" \ + "$hd_rpr_source_dir/renderThread.cpp" "$hd_rpr_test_dir/$hd_rpr_test.cpp" \ + "-L$hd_rpr_hfs/dsolib" "-Wl,-rpath,$hd_rpr_hfs/dsolib" \ + -lpxr_tf -lpxr_arch -o "$hd_rpr_build_dir/$hd_rpr_test" + env LD_LIBRARY_PATH="$hd_rpr_hfs/dsolib:$hd_rpr_hfs/python/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \ + timeout 15s "$hd_rpr_build_dir/$hd_rpr_test" + echo "PASS: $hd_rpr_test" +done diff --git a/pxr/imaging/plugin/hdRpr/testenv/testHdRprPausedShutdown.cpp b/pxr/imaging/plugin/hdRpr/testenv/testHdRprPausedShutdown.cpp new file mode 100644 index 000000000..edace655d --- /dev/null +++ b/pxr/imaging/plugin/hdRpr/testenv/testHdRprPausedShutdown.cpp @@ -0,0 +1,26 @@ +/************************************************************************ +Copyright 2020 Advanced Micro Devices, Inc +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +************************************************************************/ + +#include "renderThread.h" +#include +#include +#include +int main() { + pxr::HdRprRenderThread thread; + std::atomic entered(false); + thread.SetRenderCallback([&] { entered = true; thread.WaitUntilPaused(); }); + thread.PauseRender(); thread.StartThread(); thread.StartRender(); + while (!entered) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + thread.StopThread(); +} diff --git a/pxr/imaging/plugin/hdRpr/testenv/testHdRprRenderThread.cpp b/pxr/imaging/plugin/hdRpr/testenv/testHdRprRenderThread.cpp new file mode 100644 index 000000000..74f9916db --- /dev/null +++ b/pxr/imaging/plugin/hdRpr/testenv/testHdRprRenderThread.cpp @@ -0,0 +1,111 @@ +/************************************************************************ +Copyright 2020 Advanced Micro Devices, Inc +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +************************************************************************/ + +#include "renderThread.h" +#include +#include +#include +#include +#include +#include +#include +#include +#define CHECK(x) do { if (!(x)) { std::fprintf(stderr, "FAIL line %d: %s\n", __LINE__, #x); std::abort(); } } while (false) +using pxr::HdRprRenderThread; +struct Event { + std::mutex mutex; + std::condition_variable cv; + bool ready = false; + void Set() { { std::lock_guard lock(mutex); ready = true; } cv.notify_all(); } + void Wait() { std::unique_lock lock(mutex); CHECK(cv.wait_for(lock, std::chrono::seconds(2), [this] { return ready; })); } +}; +void IdleShutdownAndRecreate() { + HdRprRenderThread thread; + std::atomic shutdowns(0); + thread.SetShutdownCallback([&] { ++shutdowns; }); + for (int i = 0; i < 30; ++i) { thread.StartThread(); CHECK(thread.IsThreadRunning()); thread.StopThread(); CHECK(!thread.IsThreadRunning()); thread.StopThread(); } + CHECK(shutdowns == 30); +} +void PausedShutdown() { + for (int i = 0; i < 100; ++i) { + HdRprRenderThread thread; + Event entered; + std::atomic exited(false); + thread.SetRenderCallback([&] { entered.Set(); thread.WaitUntilPaused(); CHECK(thread.IsStopRequested()); exited = true; }); + thread.PauseRender(); + thread.StartThread(); + thread.StartRender(); + entered.Wait(); + if (i == 0) std::this_thread::sleep_for(std::chrono::milliseconds(20)); + thread.StopThread(); + CHECK(exited); + } +} +void ShutdownAbortsInFlightWork() { + HdRprRenderThread thread; + Event entered, aborted; + std::atomic exited(false); + thread.SetRenderCallback([&] { entered.Set(); aborted.Wait(); CHECK(thread.IsStopRequested()); exited = true; }); + thread.SetStopCallback([&] { aborted.Set(); }); + thread.StartThread(); thread.StartRender(); entered.Wait(); thread.StopThread(); CHECK(exited); +} +void NonblockingThenBlockingStop() { + HdRprRenderThread thread; + Event entered, release, requested; + thread.SetRenderCallback([&] { entered.Set(); release.Wait(); CHECK(thread.IsStopRequested()); }); + thread.SetStopCallback([&] { requested.Set(); }); + thread.StartThread(); thread.StartRender(); entered.Wait(); + thread.StopRender(false); requested.Wait(); CHECK(thread.IsRendering()); + release.Set(); thread.StopRender(); CHECK(!thread.IsRendering()); thread.StopThread(); +} +void StopRestartPreservesPause() { + HdRprRenderThread thread; + std::atomic entered(0), completed(0); + thread.SetRenderCallback([&] { ++entered; thread.WaitUntilPaused(); if (!thread.IsStopRequested()) ++completed; }); + thread.PauseRender(); CHECK(thread.IsPaused()); thread.StartThread(); + for (int i = 0; i < 100; ++i) { thread.StartRender(); thread.StopRender(); CHECK(!thread.IsRendering()); CHECK(thread.IsStopRequested()); } + thread.StartRender(); CHECK(!thread.IsStopRequested()); + thread.ResumeRender(); CHECK(!thread.IsPaused()); + const auto until = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while (completed == 0 && std::chrono::steady_clock::now() < until) std::this_thread::yield(); + CHECK(completed == 1); thread.StopRender(); thread.StopThread(); +} +void ConcurrentPauseStop() { + HdRprRenderThread thread; + std::atomic finish(false); + thread.SetRenderCallback([&] { while (!thread.IsStopRequested()) { thread.WaitUntilPaused(); std::this_thread::yield(); } }); + thread.StartThread(); + std::thread pauser([&] { while (!finish) { thread.PauseRender(); thread.ResumeRender(); } }); + for (int i = 0; i < 300; ++i) { thread.StartRender(); thread.StopRender(); } + finish = true; pauser.join(); thread.StopThread(); +} +void CallbackCannotOutliveOwnerState() { + std::atomic alive(true), exited(false); + struct Owner { + HdRprRenderThread thread; + struct State { std::atomic& alive; ~State() { alive = false; } } state; + Event entered; + Owner(std::atomic& alive, std::atomic& exited) : state{alive} { + thread.SetRenderCallback([this, &exited] { entered.Set(); thread.WaitUntilPaused(); CHECK(state.alive); exited = true; }); + thread.PauseRender(); thread.StartThread(); thread.StartRender(); entered.Wait(); + } + ~Owner() { thread.StopThread(); } + }; + { Owner owner(alive, exited); } + CHECK(exited && !alive); +} +int main() { + IdleShutdownAndRecreate(); PausedShutdown(); ShutdownAbortsInFlightWork(); + NonblockingThenBlockingStop(); StopRestartPreservesPause(); ConcurrentPauseStop(); CallbackCannotOutliveOwnerState(); + std::puts("PASS: idle/paused/in-flight shutdown, nonblocking stop, restart, concurrent pause/stop, owner lifetime"); +} diff --git a/pxr/imaging/plugin/rprHoudini/UsdRenderers.json b/pxr/imaging/plugin/rprHoudini/UsdRenderers.json index 8aa4e5cba..31e702b4f 100644 --- a/pxr/imaging/plugin/rprHoudini/UsdRenderers.json +++ b/pxr/imaging/plugin/rprHoudini/UsdRenderers.json @@ -4,11 +4,14 @@ "menulabel": "RPR", "menupriority": 0, "restartrendersettings": [ - "rpr:core:renderQuality" + "rpr:core:renderQuality", + "rpr:core:useOpenCL", + "rpr:core:hybridGpuIndex" ], "pauseonupdate": false, "allowbackgroundupdate": true, "viewstats": [ + "Render state", "Render backend", "Render GPU", "CPU render threads", @@ -27,7 +30,8 @@ "Output resolution": "outputResolution", "RPR preview scale": "previewScale", "Render backend": "rendererBackend", - "Denoising": "denoisingMode" + "Denoising": "denoisingMode", + "Render state": "renderStatus" } } } diff --git a/pxr/imaging/rprUsd/contextHelpers.cpp b/pxr/imaging/rprUsd/contextHelpers.cpp index 28733bf32..0edeef92a 100644 --- a/pxr/imaging/rprUsd/contextHelpers.cpp +++ b/pxr/imaging/rprUsd/contextHelpers.cpp @@ -477,7 +477,20 @@ rpr::Context* RprUsdCreateContext(RprUsdContextMetadata* metadata) { return nullptr; } - RprUsdDevicesInfo devicesConfiguration = LoadDevicesConfiguration(metadata->pluginType, deviceConfigurationFilepath); + RprUsdDevicesInfo devicesConfiguration; + const bool explicitHybridGpu = RprUsdIsHybrid(metadata->pluginType) && metadata->hybridGpuIndex >= 0; + if (explicitHybridGpu) { + // A per-delegate selection is atomic with backend creation. Do not create + // temporary discovery contexts on every other GPU during a live switch. + const auto supported = GetHybridGpuIndices(metadata->pluginType); + if (RprUsdIsCpuOnly() || std::find(supported.begin(), supported.end(), metadata->hybridGpuIndex) == supported.end()) { + TF_RUNTIME_ERROR("Hybrid GPU index %d is unavailable", metadata->hybridGpuIndex); + return nullptr; + } + devicesConfiguration.gpus.push_back({metadata->hybridGpuIndex, {}}); + } else { + devicesConfiguration = LoadDevicesConfiguration(metadata->pluginType, deviceConfigurationFilepath); + } std::vector contextProperties; auto appendContextProperty = [&contextProperties](uint64_t propertyKey, void* propertyValue) { @@ -566,6 +579,10 @@ rpr::Context* RprUsdCreateContext(RprUsdContextMetadata* metadata) { RPR_ERROR_CHECK(context->SetParameter(RPR_CONTEXT_TEXTURE_CACHE_PATH, textureCachePath.c_str()), "Failed to set texture cache path"); + if (explicitHybridGpu) { + // Name queries use the active ordinal, even for physical GPU1+. + devicesConfiguration.gpus.front().name = RprUsdGetStringInfo(context, RPR_CONTEXT_GPU0_NAME); + } metadata->creationFlags = creationFlags; metadata->devicesActuallyUsed = devicesConfiguration; } else { diff --git a/pxr/imaging/rprUsd/contextMetadata.h b/pxr/imaging/rprUsd/contextMetadata.h index a197f86a5..3e73d014d 100644 --- a/pxr/imaging/rprUsd/contextMetadata.h +++ b/pxr/imaging/rprUsd/contextMetadata.h @@ -60,6 +60,8 @@ struct RprUsdContextMetadata { RprUsdPluginType pluginType = kPluginInvalid; bool isGlInteropEnabled = false; bool useOpenCL = false; + // -1 preserves the saved device configuration; nonnegative selects one Hybrid GPU. + int hybridGpuIndex = -1; void* interopInfo = nullptr; rpr::CreationFlags creationFlags = 0; // additional info about hardware actually used in render context creation From 2f722117eca179efa5161cb6cb4f5e9862091e38 Mon Sep 17 00:00:00 2001 From: Jason Batson Date: Sun, 6 Sep 2026 22:59:51 -0500 Subject: [PATCH 4/5] Add standalone HybridPro accumulation and lifecycle diagnostics --- BUILD_HOUDINI.md | 25 +- pxr/imaging/plugin/hdRpr/testenv/README.md | 4 + pxr/imaging/plugin/hdRpr/testenv/SDK_TESTS.md | 76 ++++ .../plugin/hdRpr/testenv/checkHybridSdk.py | 51 +++ .../plugin/hdRpr/testenv/testHybridSdk.cpp | 352 ++++++++++++++++++ 5 files changed, 505 insertions(+), 3 deletions(-) create mode 100644 pxr/imaging/plugin/hdRpr/testenv/SDK_TESTS.md create mode 100644 pxr/imaging/plugin/hdRpr/testenv/checkHybridSdk.py create mode 100644 pxr/imaging/plugin/hdRpr/testenv/testHybridSdk.cpp diff --git a/BUILD_HOUDINI.md b/BUILD_HOUDINI.md index 128e875d5..7fe2ba653 100644 --- a/BUILD_HOUDINI.md +++ b/BUILD_HOUDINI.md @@ -189,6 +189,25 @@ entry but wrote three, initialized light transforms/displacement state and timin counters, and supplied defined clipping defaults when a reconstructed camera has lens parameters without a clipping range. These corrections need live validation; they are not presented as a demonstrated fix for the HybridPro GPU resets. -A small direct-SDK accumulation/teardown reproduction has been compiled locally, -but has not yet run. No application restart, screenshot, or GPU test is needed -for the standalone CPU synchronization tests above. +The darkening now reproduces outside Houdini in the standalone HybridPro SDK +fixture, including after reboot. On the R9700/RADV 25.2.8 configuration, one +iteration per SDK render call gives mean RGB 0.00254655682 at 32 samples, +0.000636639204 at 128 samples (exactly one quarter), then zero after clear and +rerender. Native batching, reauthoring iterations and SVGF do not fix it. +The same fixture renders correctly on the RTX 2070 Super/595.84 driver. + +`RADV_DEBUG=syncshaders` corrects accumulation and reset in these SDK fixtures. +This Mesa diagnostic flag is not enabled by the plugin and does not establish +whether the missing synchronization is an SDK or driver defect. With this flag, +six fresh HybridPro contexts alternated AMD/NVIDIA in one process, rendered +metal/glass spheres, procedural texture and emissive lighting at 512x512/128 +samples, accepted light/camera edits, and completed all teardowns. Their mean +absolute RGB difference was 4.77e-6. These results do not validate Houdini +renderer switches, recovery from device loss, or production-scene stability. +An earlier synchronized geometry-edit test still lost the AMD device; later +immediate and deferred deletion controls both passed. No speculative deferred +release queue was added to the delegate. + +The standalone SDK fixture, image/teardown checker and reproduction instructions +are in `pxr/imaging/plugin/hdRpr/testenv/SDK_TESTS.md`. They require no Houdini +engine or window. CPU synchronization tests remain separate and use no GPU. diff --git a/pxr/imaging/plugin/hdRpr/testenv/README.md b/pxr/imaging/plugin/hdRpr/testenv/README.md index 6d39b008a..e4ef3dea7 100644 --- a/pxr/imaging/plugin/hdRpr/testenv/README.md +++ b/pxr/imaging/plugin/hdRpr/testenv/README.md @@ -35,3 +35,7 @@ methods. These tests exercise thread ownership and synchronization. Actual delegate recreation, SDK device-loss cleanup, AOV allocation/rebinding, and rendering need separate host integration checks. + +Standalone GPU accumulation, image-reset and context-lifecycle diagnostics are +documented separately in [SDK_TESTS.md](SDK_TESTS.md). They link only the RPR +SDK and are not run by the CPU test script. diff --git a/pxr/imaging/plugin/hdRpr/testenv/SDK_TESTS.md b/pxr/imaging/plugin/hdRpr/testenv/SDK_TESTS.md new file mode 100644 index 000000000..c92f5fee7 --- /dev/null +++ b/pxr/imaging/plugin/hdRpr/testenv/SDK_TESTS.md @@ -0,0 +1,76 @@ +# Standalone HybridPro diagnostics + +`testHybridSdk.cpp` links only the RPR SDK. It does not use Hydra, Houdini, or a +window. Run it explicitly on a selected GPU; it is not part of the CPU test +suite. A driver or SDK failure can reset that GPU. It does not attempt to +recover a lost context, and a process timeout bounds SDK shutdown hangs. + +Build using the headers and libraries from the same SDK version: + +```bash +RPR_TEST_SDK=/absolute/path/to/RadeonProRenderSDK/RadeonProRender +RPR_TEST_LIB="$RPR_TEST_SDK/binUbuntu20" +c++ -std=c++17 -O2 -I"$RPR_TEST_SDK/inc" testHybridSdk.cpp \ + -L"$RPR_TEST_LIB" -lRadeonProRender64 -Wl,-rpath,"$RPR_TEST_LIB" \ + -o /tmp/testHybridSdk +mkdir -p /tmp/hybrid-results +timeout -k 3s 30s /tmp/testHybridSdk "$RPR_TEST_LIB/HybridPro.so" \ + /tmp/hybrid-results/default steady 0 > /tmp/hybrid-results/default.log 2>&1 +python3 checkHybridSdk.py /tmp/hybrid-results/default.log +``` + +The positional arguments are the HybridPro library, output prefix, variant +(default `steady`), and the machine-local SDK GPU index (default 0; range 0–15). +The program prints the actual selected GPU name before rendering. Set +`RPR_TEST_EXPECT_GPU` to require a substring in that name. This selection does +not create probe contexts on other devices. + +`RPR_TEST_SIZE` selects a square image, default 128 (32–2048). +`RPR_TEST_CYCLES` selects fresh context cycles, default 3 (1–20). +`RPR_TEST_ALTERNATE_GPU=1` alternates GPU0 and GPU1 between cycles, starting at +the supplied index; it requires both devices. Leave it unset for one device. +Output directories must already exist. Each snapshot is native float32 RGBA; +the log reports mean linear RGB. Timings include a completed framebuffer +readback and exclude context creation and writing files. + +Every variant renders 32 samples, continues to 128 without clearing, then +clears and renders 128 again. The fixed illuminated scene should keep similar +radiance across those images. Successful SDK calls alone are **not** a passing +image test: `checkHybridSdk.py` checks that static brightness stays within 5%, +the light-off image is zero where present, and every started context closes. +The checker uses only Python's standard library. Inspect the raw images too; +mean radiance cannot catch every spatial artifact. The fixture and tolerance +are diagnostic, not a general image-comparison metric. + +| Variant | Additional behavior | +| --- | --- | +| `steady` | One SDK iteration per render call; diffuse floor and an emissive quad. | +| `batch` | Submit native batches of 32, 96 and 128 iterations. | +| `set-iterations` | Reauthor one iteration before every render call. | +| `camera` | Turn the emitter off/on, move the camera, then restore it. | +| `materials` | Add metal and solid glass spheres plus a procedural checker floor; run the camera/light edits at a square camera aspect ratio. | +| `edits` | Also detach/delete/recreate the floor immediately. | +| `defer-delete` | Keep the detached floor alive through replacement rendering/readback, delete it, then render again. | +| `keep-detached` | Keep the detached floor alive until final teardown. | +| `churn` / `churn-immediate` | Replace floor, emitter and their materials 16 times per context; release old objects after one replacement render/readback, or immediately. | +| `svgf` | Use the SDK's SVGF denoiser. | +| `framecount` | Diagnostic unsupported-API probe; tested HybridPro returns -23. | +| `null-scene` | Deliberate reproduction of HybridPro crashing on `rprContextSetScene(context, nullptr)` during cleanup. Normal variants follow the SDK tutorial's direct scene deletion. | + +On the tested R9700/RADV 25.2.8 configuration, the default path produces +exactly quarter brightness after increasing 32→128 samples, then black after +clearing. It reproduces after reboot. `RADV_DEBUG=syncshaders` removes this +failure in the standalone fixtures. Mesa documents this flag as synchronizing +after each draw/dispatch: +[RADV environment variables](https://docs.mesa3d.org/envvars.html#radv-driver-environment-variables). +It is a diagnostic workaround, not enabled by this program or the plugin. + +With SDK v3.1.6.patch1, a 512×512 metal/glass fixture alternated six fresh +AMD/NVIDIA contexts with synchronization enabled, completed all teardowns, +passed the radiance/reset/light-off checks, and produced closely matching +images. This does not validate Houdini delegate switching or device-loss +recovery. An earlier AMD geometry-edit run with synchronization still lost the +device; subsequent immediate and deferred deletion controls both passed. +Deferred deletion is therefore **not** a demonstrated fix and was not added +to the delegate. Denoising, FSR2, and production-scene behavior need separate +checks. diff --git a/pxr/imaging/plugin/hdRpr/testenv/checkHybridSdk.py b/pxr/imaging/plugin/hdRpr/testenv/checkHybridSdk.py new file mode 100644 index 000000000..bc14f9141 --- /dev/null +++ b/pxr/imaging/plugin/hdRpr/testenv/checkHybridSdk.py @@ -0,0 +1,51 @@ +"""Check the static fixture's radiance, reset and teardown, not just sample counts.""" +import argparse +import json +import math +import re +import sys +from pathlib import Path + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument("log", type=Path) +args = parser.parse_args() +cycles = [] +current = None +deleted = 0 +errors = [] +for line in args.log.read_text(errors="replace").splitlines(): + match = re.match(r"cycle (\d+) GPU (.+)", line) + if match: + current = {"cycle": int(match[1]), "gpu": match[2], "means": {}} + cycles.append(current) + if line == "context deleted": + deleted += 1 + match = re.search(r"-(32|128|reset128|lightoff32)\.rgba32f RGB mean (\S+)$", line) + # Churn snapshots have additional edit numbers and are not the static scene. + if match and current is not None and not re.search(r"-edit\d+-", line): + current["means"][match[1]] = float(match[2]) +if not cycles or deleted != len(cycles): + errors.append("Missing completed context teardown(s)") +for cycle in cycles: + means = cycle["means"] + label = f"cycle {cycle['cycle']}" + if not all(key in means for key in ("32", "128", "reset128")): + errors.append(f"{label}: missing baseline or reset image") + continue + if not all(math.isfinite(value) for value in means.values()): + errors.append(f"{label}: nonfinite radiance") + continue + if means["32"] <= 0: + errors.append(f"{label}: illuminated fixture is black") + continue + # This fixed, area-lit fixture changes far less than 5% through sampling. + # The large margin catches the 1/N fade without requiring bit-identical GPUs. + for key in ("128", "reset128"): + ratio = means[key] / means["32"] + if not 0.95 <= ratio <= 1.05: + errors.append(f"{label}: {key}/32 radiance ratio {ratio:.6g}") + if "lightoff32" in means and means["lightoff32"] != 0: + errors.append(f"{label}: light-off fixture still has radiance") +print(json.dumps({"passed": not errors, "errors": errors, "contexts_deleted": deleted, + "cycles": cycles}, indent=2)) +sys.exit(bool(errors)) diff --git a/pxr/imaging/plugin/hdRpr/testenv/testHybridSdk.cpp b/pxr/imaging/plugin/hdRpr/testenv/testHybridSdk.cpp new file mode 100644 index 000000000..190907870 --- /dev/null +++ b/pxr/imaging/plugin/hdRpr/testenv/testHybridSdk.cpp @@ -0,0 +1,352 @@ +// Isolate HybridPro accumulation and teardown without Houdini or Hydra. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define CHECK(call) do { auto status = (call); if (status != RPR_SUCCESS) { \ + std::fprintf(stderr, "%s returned %d at line %d\n", #call, int(status), __LINE__); \ + throw std::runtime_error(#call); } } while (false) + +static unsigned resolution = 128; + +static int integer(const char* text, int minimum, int maximum) { + char* end = nullptr; + errno = 0; + const long value = std::strtol(text, &end, 10); + if (errno || end == text || *end || value < minimum || value > maximum) + throw std::runtime_error("integer argument out of range"); + return int(value); +} + +static rpr_shape quad(rpr_context context, const float* vertices, float normalY) { + const float normals[] = {0, normalY, 0}; + const float uv[] = {0, 0, 1, 0, 1, 1, 0, 1}; + const int indices[] = {0, 1, 2, 0, 2, 3}; + const int normalIndices[] = {0, 0, 0, 0, 0, 0}; + const int faces[] = {3, 3}; + rpr_shape shape = nullptr; + CHECK(rprContextCreateMesh(context, vertices, 4, 12, normals, 1, 12, + uv, 4, 8, indices, 4, normalIndices, 4, indices, 4, faces, 2, &shape)); + return shape; +} + +static rpr_shape sphere(rpr_context context, float x) { + constexpr int segments = 64, rings = 32; + constexpr double pi = 3.14159265358979323846; + std::vector vertices, normals, uv; + std::vector indices; + for (int row = 0; row <= rings; ++row) { + const double theta = pi * row / rings; + for (int col = 0; col <= segments; ++col) { + const double phi = 2 * pi * col / segments; + const float nx = std::sin(theta) * std::cos(phi); + const float ny = std::cos(theta); + const float nz = std::sin(theta) * std::sin(phi); + vertices.insert(vertices.end(), {x + .85f * nx, .86f + .85f * ny, .85f * nz}); + normals.insert(normals.end(), {nx, ny, nz}); + uv.insert(uv.end(), {float(col) / segments, float(row) / rings}); + } + } + for (int row = 0; row < rings; ++row) { + for (int col = 0; col < segments; ++col) { + const int a = row * (segments + 1) + col, b = a + segments + 1; + if (row != 0) indices.insert(indices.end(), {a, a + 1, b}); + if (row != rings - 1) indices.insert(indices.end(), {a + 1, b + 1, b}); + } + } + const std::vector faces(indices.size() / 3, 3); + rpr_shape shape = nullptr; + CHECK(rprContextCreateMesh(context, vertices.data(), vertices.size() / 3, 12, + normals.data(), normals.size() / 3, 12, uv.data(), uv.size() / 2, 8, + indices.data(), 4, indices.data(), 4, indices.data(), 4, + faces.data(), faces.size(), &shape)); + return shape; +} + +static void save(rpr_framebuffer fb, const std::string& path) { + std::vector values(size_t(resolution) * resolution * 4); + CHECK(rprFrameBufferGetInfo(fb, RPR_FRAMEBUFFER_DATA, + values.size() * sizeof(float), values.data(), nullptr)); + double sum = 0; + for (size_t i = 0; i < values.size(); ++i) { + if (!std::isfinite(values[i])) throw std::runtime_error("nonfinite output"); + if (i % 4 != 3) sum += values[i]; + } + std::ofstream file(path, std::ios::binary); + file.write(reinterpret_cast(values.data()), values.size() * sizeof(float)); + if (!file) throw std::runtime_error("write failed"); + std::printf("%s RGB mean %.9g\n", path.c_str(), sum / (resolution * resolution * 3)); + std::fflush(stdout); +} + +int main(int argc, char** argv) { + if (argc < 3 || argc > 5) { + std::fprintf(stderr, "Usage: %s /path/HybridPro.so output-prefix [variant] [GPU index 0..15]\n", argv[0]); + return 2; + } + try { + const std::string variant = argc >= 4 ? argv[3] : "steady"; + const std::vector variants = {"steady", "batch", "set-iterations", "framecount", + "svgf", "edits", "camera", "defer-delete", "keep-detached", "churn", "churn-immediate", + "materials", "null-scene"}; + if (std::find(variants.begin(), variants.end(), variant) == variants.end()) + throw std::runtime_error("unknown variant"); + const int initialGpuIndex = argc == 5 ? integer(argv[4], 0, 15) : 0; + if (const auto size = std::getenv("RPR_TEST_SIZE")) resolution = integer(size, 32, 2048); + const auto cyclesText = std::getenv("RPR_TEST_CYCLES"); + const int cycles = cyclesText ? integer(cyclesText, 1, 20) : 3; + const auto alternateText = std::getenv("RPR_TEST_ALTERNATE_GPU"); + const bool alternate = alternateText && integer(alternateText, 0, 1); + if (alternate && initialGpuIndex > 1) + throw std::runtime_error("alternation selects only GPU0/GPU1"); + const rpr_creation_flags gpuFlags[] = { + RPR_CREATION_FLAGS_ENABLE_GPU0, RPR_CREATION_FLAGS_ENABLE_GPU1, + RPR_CREATION_FLAGS_ENABLE_GPU2, RPR_CREATION_FLAGS_ENABLE_GPU3, + RPR_CREATION_FLAGS_ENABLE_GPU4, RPR_CREATION_FLAGS_ENABLE_GPU5, + RPR_CREATION_FLAGS_ENABLE_GPU6, RPR_CREATION_FLAGS_ENABLE_GPU7, + RPR_CREATION_FLAGS_ENABLE_GPU8, RPR_CREATION_FLAGS_ENABLE_GPU9, + RPR_CREATION_FLAGS_ENABLE_GPU10, RPR_CREATION_FLAGS_ENABLE_GPU11, + RPR_CREATION_FLAGS_ENABLE_GPU12, RPR_CREATION_FLAGS_ENABLE_GPU13, + RPR_CREATION_FLAGS_ENABLE_GPU14, RPR_CREATION_FLAGS_ENABLE_GPU15}; + const int plugin = rprRegisterPlugin(argv[1]); + if (plugin < 0) throw std::runtime_error("register failed"); + for (int cycle = 0; cycle < cycles; ++cycle) { + const int gpuIndex = alternate + ? (initialGpuIndex + cycle) % 2 : initialGpuIndex; + rpr_context context = nullptr; + CHECK(rprCreateContext(RPR_API_VERSION, &plugin, 1, + gpuFlags[gpuIndex], + nullptr, "/tmp", &context)); + CHECK(rprContextSetActivePlugin(context, plugin)); + char name[1024] = {}; + CHECK(rprContextGetInfo(context, RPR_CONTEXT_GPU0_NAME, sizeof(name), name, nullptr)); + std::printf("cycle %d GPU %s\n", cycle, name); std::fflush(stdout); + const auto expectedGpu = std::getenv("RPR_TEST_EXPECT_GPU"); + if (expectedGpu && !std::strstr(name, expectedGpu)) + throw std::runtime_error("unexpected device"); + CHECK(rprContextSetParameterByKey1u(context, RPR_CONTEXT_ITERATIONS, 1)); + CHECK(rprContextSetParameterByKey1u(context, RPR_CONTEXT_PT_DENOISER, + variant == "svgf" ? RPR_DENOISER_SVGF : RPR_DENOISER_NONE)); + CHECK(rprContextSetParameterByKey1u(context, RPR_CONTEXT_UPSCALER, RPR_UPSCALER_NONE)); + CHECK(rprContextSetParameterByKey1u(context, RPR_CONTEXT_TONE_MAPPING, RPR_TONE_MAPPING_NONE)); + CHECK(rprContextSetParameterByKey1f(context, RPR_CONTEXT_DISPLAY_GAMMA, 1)); + rpr_scene scene = nullptr; + CHECK(rprContextCreateScene(context, &scene)); + CHECK(rprContextSetScene(context, scene)); + rpr_camera camera = nullptr; + CHECK(rprContextCreateCamera(context, &camera)); + if (variant == "materials") CHECK(rprCameraSetSensorSize(camera, 36, 36)); + CHECK(rprCameraLookAt(camera, 0, 2, 7, 0, 1, 0, 0, 1, 0)); + CHECK(rprSceneSetCamera(scene, camera)); + rpr_material_system system = nullptr; + CHECK(rprContextCreateMaterialSystem(context, 0, &system)); + rpr_material_node diffuse = nullptr, emission = nullptr; + CHECK(rprMaterialSystemCreateNode(system, RPR_MATERIAL_NODE_UBERV2, &diffuse)); + CHECK(rprMaterialNodeSetInputFByKey(diffuse, RPR_MATERIAL_INPUT_UBER_DIFFUSE_WEIGHT, 1, 1, 1, 1)); + CHECK(rprMaterialNodeSetInputFByKey(diffuse, RPR_MATERIAL_INPUT_UBER_DIFFUSE_COLOR, .18, .18, .18, 1)); + CHECK(rprMaterialSystemCreateNode(system, RPR_MATERIAL_NODE_UBERV2, &emission)); + CHECK(rprMaterialNodeSetInputFByKey(emission, RPR_MATERIAL_INPUT_UBER_DIFFUSE_WEIGHT, 0, 0, 0, 0)); + CHECK(rprMaterialNodeSetInputFByKey(emission, RPR_MATERIAL_INPUT_UBER_EMISSION_WEIGHT, 1, 1, 1, 1)); + CHECK(rprMaterialNodeSetInputFByKey(emission, RPR_MATERIAL_INPUT_UBER_EMISSION_COLOR, 30, 30, 30, 1)); + const float floorVertices[] = {-3, 0, -3, -3, 0, 3, 3, 0, 3, 3, 0, -3}; + const float lightVertices[] = {-2, 4, -.5, 2, 4, -.5, 2, 4, .5, -2, 4, .5}; + auto floor = quad(context, floorVertices, 1); + auto light = quad(context, lightVertices, -1); + CHECK(rprShapeSetMaterial(floor, diffuse)); + CHECK(rprShapeSetMaterial(light, emission)); + CHECK(rprSceneAttachShape(scene, floor)); + CHECK(rprSceneAttachShape(scene, light)); + std::vector materialShapes; + std::vector materialNodes; + if (variant == "materials") { + auto value = [](rpr_material_node node, rpr_material_node_input key, float v) { + CHECK(rprMaterialNodeSetInputFByKey(node, key, v, v, v, v)); + }; + rpr_material_node checker = nullptr, uvLookup = nullptr, scale = nullptr; + CHECK(rprMaterialSystemCreateNode(system, RPR_MATERIAL_NODE_INPUT_LOOKUP, &uvLookup)); + CHECK(rprMaterialNodeSetInputUByKey(uvLookup, RPR_MATERIAL_INPUT_VALUE, RPR_MATERIAL_NODE_LOOKUP_UV)); + CHECK(rprMaterialSystemCreateNode(system, RPR_MATERIAL_NODE_ARITHMETIC, &scale)); + CHECK(rprMaterialNodeSetInputUByKey(scale, RPR_MATERIAL_INPUT_OP, RPR_MATERIAL_NODE_OP_MUL)); + CHECK(rprMaterialNodeSetInputNByKey(scale, RPR_MATERIAL_INPUT_COLOR0, uvLookup)); + CHECK(rprMaterialNodeSetInputFByKey(scale, RPR_MATERIAL_INPUT_COLOR1, 8, 8, 1, 1)); + CHECK(rprMaterialSystemCreateNode(system, RPR_MATERIAL_NODE_CHECKER_TEXTURE, &checker)); + CHECK(rprMaterialNodeSetInputNByKey(checker, RPR_MATERIAL_INPUT_UV, scale)); + CHECK(rprMaterialNodeSetInputNByKey(diffuse, RPR_MATERIAL_INPUT_UBER_DIFFUSE_COLOR, checker)); + for (int i = 0; i < 2; ++i) { + rpr_material_node mat = nullptr; + CHECK(rprMaterialSystemCreateNode(system, RPR_MATERIAL_NODE_UBERV2, &mat)); + value(mat, RPR_MATERIAL_INPUT_UBER_DIFFUSE_WEIGHT, 0); + value(mat, RPR_MATERIAL_INPUT_UBER_REFLECTION_WEIGHT, 1); + value(mat, RPR_MATERIAL_INPUT_UBER_REFLECTION_ROUGHNESS, i ? .01 : .12); + if (i == 0) { + CHECK(rprMaterialNodeSetInputUByKey(mat, RPR_MATERIAL_INPUT_UBER_REFLECTION_MODE, RPR_UBER_MATERIAL_IOR_MODE_METALNESS)); + value(mat, RPR_MATERIAL_INPUT_UBER_REFLECTION_METALNESS, 1); + CHECK(rprMaterialNodeSetInputFByKey(mat, RPR_MATERIAL_INPUT_UBER_REFLECTION_COLOR, .95, .65, .2, 1)); + } else { + CHECK(rprMaterialNodeSetInputUByKey(mat, RPR_MATERIAL_INPUT_UBER_REFLECTION_MODE, RPR_UBER_MATERIAL_IOR_MODE_PBR)); + value(mat, RPR_MATERIAL_INPUT_UBER_REFLECTION_IOR, 1.5); + value(mat, RPR_MATERIAL_INPUT_UBER_REFLECTION_COLOR, 1); + value(mat, RPR_MATERIAL_INPUT_UBER_REFRACTION_WEIGHT, 1); + value(mat, RPR_MATERIAL_INPUT_UBER_REFRACTION_COLOR, 1); + value(mat, RPR_MATERIAL_INPUT_UBER_REFRACTION_IOR, 1.5); + value(mat, RPR_MATERIAL_INPUT_UBER_REFRACTION_ROUGHNESS, 0); + CHECK(rprMaterialNodeSetInputUByKey(mat, RPR_MATERIAL_INPUT_UBER_REFRACTION_THIN_SURFACE, 0)); + } + auto shape = sphere(context, i ? 1 : -1); + CHECK(rprShapeSetMaterial(shape, mat)); + CHECK(rprSceneAttachShape(scene, shape)); + materialShapes.push_back(shape); + materialNodes.push_back(mat); + } + // Dependency owners precede inputs in explicit cleanup order. + materialNodes.insert(materialNodes.end(), {checker, scale, uvLookup}); + } + rpr_framebuffer fb = nullptr; + const rpr_framebuffer_format format{4, RPR_COMPONENT_TYPE_FLOAT32}; + const rpr_framebuffer_desc dimensions{resolution, resolution}; + CHECK(rprContextCreateFrameBuffer(context, format, &dimensions, &fb)); + CHECK(rprContextSetAOV(context, RPR_AOV_COLOR, fb)); + CHECK(rprFrameBufferClear(fb)); + const std::string prefix = std::string(argv[2]) + "-" + std::to_string(cycle); + auto render = [&](int count) { + const auto start = std::chrono::steady_clock::now(); + if (variant == "batch") { + CHECK(rprContextSetParameterByKey1u(context, RPR_CONTEXT_ITERATIONS, count)); + CHECK(rprContextRender(context)); + } else { + for (int sample = 0; sample < count; ++sample) { + if (variant == "set-iterations") + CHECK(rprContextSetParameterByKey1u(context, RPR_CONTEXT_ITERATIONS, 1)); + if (variant == "framecount") + CHECK(rprContextSetParameterByKey1u(context, RPR_CONTEXT_FRAMECOUNT, sample)); + CHECK(rprContextRender(context)); + } + } + const double submitSeconds = std::chrono::duration( + std::chrono::steady_clock::now() - start).count(); + std::vector completed(size_t(resolution) * resolution * 4); + CHECK(rprFrameBufferGetInfo(fb, RPR_FRAMEBUFFER_DATA, + completed.size() * sizeof(float), completed.data(), nullptr)); + std::printf("render %d samples at %ux%u: submit %.6f s; with readback %.6f s\n", + count, resolution, resolution, submitSeconds, + std::chrono::duration(std::chrono::steady_clock::now() - start).count()); + }; + render(32); + save(fb, prefix + "-32.rgba32f"); + render(96); + save(fb, prefix + "-128.rgba32f"); + CHECK(rprFrameBufferClear(fb)); + render(128); + save(fb, prefix + "-reset128.rgba32f"); + rpr_shape retiredFloor = nullptr; + if (variant == "edits" || variant == "camera" || variant == "defer-delete" || variant == "keep-detached" || variant == "materials") { + CHECK(rprMaterialNodeSetInputFByKey(emission, RPR_MATERIAL_INPUT_UBER_EMISSION_COLOR, 0, 0, 0, 1)); + CHECK(rprFrameBufferClear(fb)); + render(32); + save(fb, prefix + "-lightoff32.rgba32f"); + if (variant != "camera" && variant != "materials") { + CHECK(rprSceneDetachShape(scene, floor)); + if (variant == "defer-delete" || variant == "keep-detached") retiredFloor = floor; + else CHECK(rprObjectDelete(floor)); + floor = quad(context, floorVertices, 1); + CHECK(rprShapeSetMaterial(floor, diffuse)); + CHECK(rprSceneAttachShape(scene, floor)); + } + CHECK(rprMaterialNodeSetInputFByKey(emission, RPR_MATERIAL_INPUT_UBER_EMISSION_COLOR, 30, 30, 30, 1)); + CHECK(rprCameraLookAt(camera, 1, 2, 7, 0, 1, 0, 0, 1, 0)); + CHECK(rprFrameBufferClear(fb)); + render(128); + save(fb, prefix + "-moved128.rgba32f"); + if (variant == "defer-delete") { + CHECK(rprObjectDelete(retiredFloor)); + retiredFloor = nullptr; + } + CHECK(rprCameraLookAt(camera, 0, 2, 7, 0, 1, 0, 0, 1, 0)); + CHECK(rprFrameBufferClear(fb)); + render(128); + save(fb, prefix + "-restored128.rgba32f"); + } + if (variant == "churn" || variant == "churn-immediate") { + // Rebuild emissive and diffuse geometry repeatedly in ONE live + // scene. Releasing after one render/readback checks the actual + // retirement boundary, not just retention until final teardown. + for (int edit = 0; edit < 16; ++edit) { + auto oldFloor = floor; + auto oldLight = light; + auto oldDiffuse = diffuse; + auto oldEmission = emission; + CHECK(rprSceneDetachShape(scene, oldFloor)); + CHECK(rprSceneDetachShape(scene, oldLight)); + auto retire = [&]() { + CHECK(rprObjectDelete(oldFloor)); + CHECK(rprObjectDelete(oldLight)); + CHECK(rprObjectDelete(oldDiffuse)); + CHECK(rprObjectDelete(oldEmission)); + }; + if (variant == "churn-immediate") retire(); + CHECK(rprMaterialSystemCreateNode(system, RPR_MATERIAL_NODE_UBERV2, &diffuse)); + CHECK(rprMaterialNodeSetInputFByKey(diffuse, RPR_MATERIAL_INPUT_UBER_DIFFUSE_WEIGHT, 1, 1, 1, 1)); + CHECK(rprMaterialNodeSetInputFByKey(diffuse, RPR_MATERIAL_INPUT_UBER_DIFFUSE_COLOR, .18, .18, .18, 1)); + CHECK(rprMaterialSystemCreateNode(system, RPR_MATERIAL_NODE_UBERV2, &emission)); + CHECK(rprMaterialNodeSetInputFByKey(emission, RPR_MATERIAL_INPUT_UBER_DIFFUSE_WEIGHT, 0, 0, 0, 0)); + CHECK(rprMaterialNodeSetInputFByKey(emission, RPR_MATERIAL_INPUT_UBER_EMISSION_WEIGHT, 1, 1, 1, 1)); + CHECK(rprMaterialNodeSetInputFByKey(emission, RPR_MATERIAL_INPUT_UBER_EMISSION_COLOR, 30, 30, 30, 1)); + floor = quad(context, floorVertices, 1); + light = quad(context, lightVertices, -1); + CHECK(rprShapeSetMaterial(floor, diffuse)); + CHECK(rprShapeSetMaterial(light, emission)); + CHECK(rprSceneAttachShape(scene, floor)); + CHECK(rprSceneAttachShape(scene, light)); + CHECK(rprCameraLookAt(camera, edit % 2, 2, 7, 0, 1, 0, 0, 1, 0)); + CHECK(rprFrameBufferClear(fb)); + render(1); + save(fb, prefix + "-edit" + std::to_string(edit) + "-1.rgba32f"); + if (variant == "churn") retire(); + render(31); + save(fb, prefix + "-edit" + std::to_string(edit) + "-32.rgba32f"); + } + } + std::puts("detach and delete"); std::fflush(stdout); + CHECK(rprContextSetAOV(context, RPR_AOV_COLOR, nullptr)); + for (auto shape : materialShapes) { + CHECK(rprSceneDetachShape(scene, shape)); + CHECK(rprObjectDelete(shape)); + } + CHECK(rprSceneDetachShape(scene, light)); + CHECK(rprSceneDetachShape(scene, floor)); + CHECK(rprShapeSetMaterial(light, nullptr)); + CHECK(rprShapeSetMaterial(floor, nullptr)); + CHECK(rprObjectDelete(light)); + CHECK(rprObjectDelete(floor)); + if (retiredFloor) CHECK(rprObjectDelete(retiredFloor)); + CHECK(rprObjectDelete(emission)); + CHECK(rprObjectDelete(diffuse)); + for (auto node : materialNodes) CHECK(rprObjectDelete(node)); + CHECK(rprObjectDelete(system)); + CHECK(rprObjectDelete(fb)); + CHECK(rprSceneSetCamera(scene, nullptr)); + CHECK(rprObjectDelete(camera)); + // Current HybridPro dereferences null in rprContextSetScene. Its + // bundled tutorial deletes the scene directly instead. + if (variant == "null-scene") CHECK(rprContextSetScene(context, nullptr)); + CHECK(rprObjectDelete(scene)); + CHECK(rprObjectDelete(context)); + std::puts("context deleted"); std::fflush(stdout); + } + return 0; + } catch (const std::exception& error) { + // A failed GPU context is left to process cleanup; do not cascade SDK calls. + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} From c546e463a332d50103675b8f5dcf5bf2b5174e92 Mon Sep 17 00:00:00 2001 From: Jason Batson Date: Sun, 6 Sep 2026 23:42:57 -0500 Subject: [PATCH 5/5] Document synchronized AMD renderer-switch validation --- BUILD_HOUDINI.md | 52 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/BUILD_HOUDINI.md b/BUILD_HOUDINI.md index 7fe2ba653..9d121f2a4 100644 --- a/BUILD_HOUDINI.md +++ b/BUILD_HOUDINI.md @@ -164,7 +164,7 @@ frames and expose an error status. `renderContextId` identifies delegate recreation for integration checks. CPU-only regression tests are documented in `pxr/imaging/plugin/hdRpr/testenv/README.md`. -Live validation remains incomplete. A later NVIDIA HybridPro test on the RTX +Earlier live validation failed. An NVIDIA HybridPro test on the RTX 2070 Super/595.84 driver stopped after two successful samples with a Vulkan device loss; the kernel recorded Xid 109 (`CTX SWITCH TIMEOUT`). Attempting to switch from that lost context crashed inside the SDK's HybridPro context @@ -173,7 +173,7 @@ establish that this SDK can recover from device loss. Earlier successful captures must not be taken as proof of switching stability on either GPU. -The latest fixed-view sequence is **not accepted image/stability validation**. +The earlier build23 fixed-view sequence is **not accepted image/stability validation**. AMD HybridPro completed 32 samples, switched to Northstar and back, and later reported 128 samples; its floor pixels after increasing the sample cap were exactly one quarter of the previous 32-sample values. Another return from Karma @@ -187,8 +187,8 @@ The initiating cause remains unisolated; neither SDK nor driver blame is proven. Source inspection also corrected motion-mesh property storage that allocated one entry but wrote three, initialized light transforms/displacement state and timing counters, and supplied defined clipping defaults when a reconstructed camera has -lens parameters without a clipping range. These corrections need live validation; -they are not presented as a demonstrated fix for the HybridPro GPU resets. +lens parameters without a clipping range. The bounded build25 check below includes +these corrections; it does not establish a fix for recovery after GPU resets. The darkening now reproduces outside Houdini in the standalone HybridPro SDK fixture, including after reboot. On the R9700/RADV 25.2.8 configuration, one iteration per SDK render call gives mean RGB 0.00254655682 at 32 samples, @@ -202,8 +202,9 @@ whether the missing synchronization is an SDK or driver defect. With this flag, six fresh HybridPro contexts alternated AMD/NVIDIA in one process, rendered metal/glass spheres, procedural texture and emissive lighting at 512x512/128 samples, accepted light/camera edits, and completed all teardowns. Their mean -absolute RGB difference was 4.77e-6. These results do not validate Houdini -renderer switches, recovery from device loss, or production-scene stability. +absolute RGB difference was 4.77e-6. These SDK-only results do not validate +recovery from device loss or production-scene stability; the separate Houdini +check is described below. An earlier synchronized geometry-edit test still lost the AMD device; later immediate and deferred deletion controls both passed. No speculative deferred release queue was added to the delegate. @@ -211,3 +212,42 @@ release queue was added to the delegate. The standalone SDK fixture, image/teardown checker and reproduction instructions are in `pxr/imaging/plugin/hdRpr/testenv/SDK_TESTS.md`. They require no Houdini engine or window. CPU synchronization tests remain separate and use no GPU. + + +## Bounded GUI validation with the synchronization workaround + +Installed build25 (renderer source commit `8367d5b`) was tested in one existing +H22.0.429 GUI launched with `RADV_DEBUG=syncshaders`, on the R9700/RADV 25.2.8. +The isolated scene contains chrome and glass spheres, a procedural striped +material and emissive geometry. Raw color AOVs were read directly through Hydra; +these checks did not use screen captures or additional Houdini engines. + +At a fixed 935x1099 free-camera view, AMD HybridPro completed 32 then 128 samples +in the same context. Mean RGB changed from 0.187160 to 0.196604 (ratio 1.0505), +rather than the earlier quarter-brightness error. Switching to Northstar and +back created fresh RPR delegates and preserved the image. Switching to Karma CPU +and back also recreated HybridPro successfully: its mean RGB was 0.196844, with +no opaque white bands. Karma's selection/unpaused state was checked, but its +pixels were not inspected. No automatic pause, crash or new kernel GPU fault +was observed during this sequence. Denoising, upscaling and adaptive stopping +were disabled for the HybridPro checks. + +A native Solaris camera translation and restore each generated new 128-sample +images without recreating the delegate. H22 HOM `setDefaultCamera()` and live +camera setters changed the reported viewport transform but left Hydra's free +camera and output unchanged, even with `draw()` and a redraw fence. Those probes +were rejected as evidence of camera-render progress. The successful test used +`viewtransform desktop.pane.solaris.viewport txyz ( x y z )`; `viewls -n -v` +identifies the actual Solaris viewport. The separate `.world.` view is not the +LOP camera. This is a host-notification observation, not an SDK camera fix. + +The restored 128-sample frame accumulated 0.119 s in SDK render calls versus +1.422 s in resolve/readback. HUD samples/second excludes resolve, while +`totalRenderTime` continues growing after convergence; neither should be used +as complete frame latency. No resolve-throttling optimization was introduced. + +This is a bounded material-scene pass with a diagnostic launch flag, not general +stability certification. Continuous navigation, GUI GPU changes, long playback, +ASVGF/FSR2, production fluid geometry and recovery from device loss remain +unvalidated with the workaround. The plugin does not enable `syncshaders` +automatically or require it on other driver/SDK configurations.