Skip to content

rtx: Allow multiple MDL-capable ANARI devices in one process - #384

Merged
jeffamstutz merged 2 commits into
NVIDIA:next_releasefrom
tarcila:mdl-shared-neuray-instance
Oct 29, 2026
Merged

jeffamstutz merged 2 commits into
NVIDIA:next_releasefrom
tarcila:mdl-shared-neuray-instance

Conversation

@tarcila

@tarcila tarcila commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator

mi_factory yields at most one live INeuray per loaded MDL SDK image. Each
libmdl::Core asked for its own, so the second one to ask got nullptr and
reported Failed initializing VisRTX MDL support.

Case Before Now
One VisRTX device works works
Two VisRTX devices, same library fails automatic
Several anariLoadLibrary("visrtx"), a device each fails automatic
VisRTX + another vendor's MDL device fails app hands the instance over
Two different VisRTX binaries in one process fails app hands the instance over

How. A process-wide refcounted SharedNeuray owns the image and the
instance; the last Core out shuts it down and unloads. That covers everything
sharing one VisRTX binary. Separate binaries can't see each other's statics, so
there the application reads a new ineuray device property and sets it on the
existing ineuray parameter — both now declared in VISRTX_MATERIAL_MDL.

devices/rtx/docs/mdl-instance-sharing.md describes the MDL neuray instance sharing/hand off process.

Second commit: search paths belong to the INeuray, not the device, and the
SDK only offers clear-and-rebuild — so one device setting its roots dropped
every other device's. Now unioned per owner.

mi_factory hands out at most one live INeuray per loaded library image, and
dlopen of the same path returns the same refcounted handle. Each libmdl::Core
acquired its own instance, so creating a second VisRTX device while a first was
alive got a null INeuray, threw, and reported "Failed initializing VisRTX MDL
support."

Introduce detail::SharedNeuray, owning the library image and the INeuray, held
by every Core through a shared_ptr handed out via a static weak_ptr and mutex.
The SDK is shut down and unloaded only when the last Core dies, so destroying
every device and creating a new one still reacquires a fresh instance.
Teardown is serialized against a replacement: the weak_ptr expires before the
destructor has unloaded the image, and a concurrent acquire that reloaded it
meanwhile would overlap loadLibrary/freeLibrary. The destructor cannot simply
take the mutex, because the Core constructor's scope_fail destroys the object
while already holding it.

The SDK accepts a single receiving logger per instance, so a shared instance
has one master logger: the first Core in installs the sink it was given and
later ones leave it alone. SDK-internal diagnostics reach that master whichever
device provoked them. Messages VisRTX logs itself never pass through the SDK's
logger, so they still reach the device they belong to.

Attributing SDK diagnostics per device is possible -- route through a shim
consulting a thread-local "active device" -- and an earlier revision did, but
it cost a routing logger, a scope guard on all sixteen SDK entry points and
self-reference guards for instances handed back in, to redirect a trickle of
compiler warnings that no application can act on programmatically. An
application that needs precise attribution configures its own INeuray and
hands it over, which makes the question moot. Recorded in the doc as a
deliberate trade rather than an oversight.

Devices from another vendor in the same process cannot see VisRTX's internals,
so sharing with them is mediated by the application. Expose the instance as an
`ineuray` device property to pair with the existing `ineuray` parameter, and
describe both -- plus mdlSearchPaths and mdlResourceSearchPaths -- in
VISRTX_MATERIAL_MDL so the contract is discoverable rather than folklore.
docs/mdl-instance-sharing.md covers the handoff and why an automatic
rendezvous was prototyped and rejected.

The ANARI_DEVICE entry in the extension JSON deliberately carries no "name":
a named subtype makes the query generator emit a subtype-dispatching
ANARI_DEVICE_info, and callers pass NULL for the device subtype
(visrtx_extensions.h), which then crashes in subtype_hash. Properties are not
introspectable at all -- the generator's info vocabulary has no "property"
entry, which is why the core spec's own geometryMaxIndex is absent too -- so
TestMdlIneurayIntrospect verifies the property by reading it.

Tests: TestMdlMultiDevice (overlapping devices, sequential create/destroy,
concurrent creation), TestMdlIneurayHandoff (end-to-end property-to-parameter
handoff) and TestMdlIneurayIntrospect, plus TestMdlLogging pinning both halves of the
logging contract. The first three were confirmed to fail without the fix.

Review follow-ups in this commit: the teardown announcement moved from the
destructor into the shared_ptr's deleter, because the weak_ptr expires at the
last strong decrement and a flag set inside the destructor leaves a window in
which a concurrent acquire sees an expired weak_ptr, no announced teardown,
and reloads the image while the old instance is still alive (reproduced
197/200 in a model of the ordering); a failed INeuray::start() is no longer
published as a successful acquisition; and the self-reference hazard in the
adopt path disappeared with the router itself.
MDL search paths are a property of the INeuray instance, and every device in a
process shares one. IMdl_configuration offers only add_mdl_path and
clear_mdl_paths -- no way to withdraw a single root or to add one on behalf of
a particular owner -- so a device applying its own `mdlSearchPaths` had to
clear first and silently dropped every other device's roots. A device
updating its own parameter does want its previous roots retired, so neither
"always clear" nor "never clear" is correct.

detail::SearchPathRegistry keeps each Core's contribution separately, keyed by
instance and then by owner, and rebuilds the instance's list as their union on
every change. Setting one device's roots leaves the others' in place;
re-setting the same device's parameter replaces only that device's
contribution; destroying a device withdraws it. Duplicates are added once, and
the system and user paths are re-appended after every rebuild since the clear
removes those too.

Keyed on the INeuray rather than on VisRTX's shared-instance object, so it
equally covers devices that adopted an application-supplied instance through
the `ineuray` parameter -- previously those bypassed the union entirely and
cleared the shared list outright.

Withdrawing a root does not unload a module: one already loaded stays in the
MDL database and keeps resolving by name whatever the search paths later say
(verified against the SDK). TestMdlSearchPaths therefore asserts that new
roots start resolving rather than that retired ones stop. It writes a module
per directory, each emitting on a different channel, and loads them by module
name so resolution has to come off the search path; the rendered pixel
identifies which module was found and a black pixel means the root was lost.
It covers three concurrent devices with disjoint roots, devices leaving, a
device updating its own roots, and a device updating them while another is
live. The last case fails without this change.

docs/mdl-search-paths.md records the constraint and the module-caching
caveat.

Review follow-ups in this commit: forget() rebuilds before erasing the last
owner's entry, so a departed owner's roots do not stay installed on an
instance that outlives it; both path kinds stringify with generic_string() so
de-duplication is consistent on Windows; and the docs and JSON now say the
union covers participating VisRTX devices rather than every user of a shared
instance, and that precedence between conflicting roots is unspecified.
@tarcila
tarcila requested a review from jeffamstutz October 28, 2026 23:27
@jeffamstutz
jeffamstutz merged commit b7ed1b8 into NVIDIA:next_release Oct 29, 2026
8 checks passed
@tarcila
tarcila deleted the mdl-shared-neuray-instance branch October 29, 2026 00:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants