Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/mayaUsd/render/vp2RenderDelegate/proxyRenderDelegate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1902,6 +1902,11 @@ void ProxyRenderDelegate::_PopulateSelection()
return;
}

// Clear the producer's selection state before rebuilding it.
if (_producer) {
_producer->ClearSelection();
}

_leadSelection.reset(new HdSelection);
_activeSelection.reset(new HdSelection);

Expand Down
162 changes: 158 additions & 4 deletions lib/mayaUsd/render/vp2RenderDelegate/sceneIndexProducer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@
#include <pxr/base/tf/getenv.h>
#include <pxr/imaging/hd/dependencyForwardingSceneIndex.h>
#include <pxr/imaging/hd/dirtyBitsTranslator.h>
#include <pxr/imaging/hd/instancedBySchema.h>
#include <pxr/imaging/hd/instancerTopologySchema.h>
#include <pxr/imaging/hd/materialBindingsSchema.h>
#include <pxr/imaging/hd/renderDelegate.h>
#include <pxr/imaging/hd/renderIndex.h>
#include <pxr/imaging/hd/retainedDataSource.h>
#include <pxr/imaging/hd/sceneIndexPrimView.h>
#include <pxr/imaging/hd/tokens.h>
#include <pxr/imaging/hdsi/implicitSurfaceSceneIndex.h>
#include <pxr/imaging/hdsi/materialBindingResolvingSceneIndex.h>
Expand All @@ -32,6 +35,7 @@
#include <pxr/usd/usd/stage.h>
#include <pxr/usdImaging/usdImaging/sceneIndices.h>

#include <numeric>
#include <vector>

PXR_NAMESPACE_OPEN_SCOPE
Expand Down Expand Up @@ -481,6 +485,128 @@ SdfPathVector HdVP2SceneIndexProducer::GetScenePrimPaths(
return scenePaths;
}

/* Collects the rprims a point instancer draws, so a single point instance can

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this strictly for point instancers or also scene graph instancing? I thought through hydra it was one and the same

be selected without UsdImagingSelectionSceneIndex::AddSelection, which takes
only a path and would select every instance.

The walk starts at the instancer's prototype roots. A nested point
instancer's prototypes are propagated below its USD prototypes
(<prototype>/ForInstancer<hash>), outside the root being walked, so they are
walked too. Every rprim found reaches the
picked instancer through instancedBy, possibly via native or nested point
instancers; each of those levels multiplies the instances drawn per point
instance, as HdVP2Instancer nests them and HdxPrimOriginInfo decodes them.
*/
const HdVP2SceneIndexProducer::_PointInstanceRprims*
HdVP2SceneIndexProducer::_PointInstanceRprimsOf(const SdfPath& instancer)
{
auto [cached, inserted] = _pointInstanceRprims.try_emplace(instancer);
if (!inserted) {
return cached->second ? &*cached->second : nullptr;
}

// A top-level UsdGeomPointInstancer keeps its scene path through the
// propagating scene indices, so the USD path doubles as its chain path and
// the chain draws it as an instancer. One the chain does not draw as an

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

YOu use ""chain path" in many comment, to be sure, that's essentially the usd path prefixed with the scene index root thing?

// instancer - inside a native instance, inside another point instancer's
// prototype, or above the proxy's primPath - is left to AddSelection.
const HdSceneIndexPrim prim = _selectionSceneIndex->GetPrim(instancer);
const HdPathArrayDataSourceHandle prototypes
= HdInstancerTopologySchema::GetFromParent(prim.dataSource).GetPrototypes();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if GetPrototype is always fast? Sometimes some hydra getters ar not that fast if they resolve stuff, because you could skip it if rim.primType != HdPrimTypeTokens->instancer

if (prim.primType != HdPrimTypeTokens->instancer || !prototypes) {
return nullptr;
}

const auto first = [](const HdPathArrayDataSourceHandle& paths) -> SdfPath {
if (paths) {
const VtArray<SdfPath> values = paths->GetTypedValue(0.0f);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the 0.0?

if (!values.empty()) {
return values[0];
}
}
return SdfPath();
};

_PointInstanceRprims& rprims = cached->second.emplace();
const VtArray<SdfPath> instancerRoots = prototypes->GetTypedValue(0.0f);

@deboisj deboisj Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a timecode? If so maybe query at the detalut timecode to be explicit

SdfPathVector roots(instancerRoots.begin(), instancerRoots.end());
for (size_t index = 0; index < roots.size(); ++index) {
const SdfPath root = roots[index];
for (const SdfPath& path : HdSceneIndexPrimView(_selectionSceneIndex, root)) {
const HdSceneIndexPrim prim = _selectionSceneIndex->GetPrim(path);

if (prim.primType == HdPrimTypeTokens->instancer) {
// Native instancing keeps its prototypes below the instancer.
if (const HdPathArrayDataSourceHandle nested
= HdInstancerTopologySchema::GetFromParent(prim.dataSource).GetPrototypes()) {
const VtArray<SdfPath> nestedRoots = nested->GetTypedValue(0.0f);
for (const SdfPath& nestedRoot : nestedRoots) {
if (!nestedRoot.HasPrefix(root)) {
roots.push_back(nestedRoot);
}
}
}
continue;
}

// Materials, among others, carry instancedBy too; only rprims are
// highlighted.
const SdfPath indexPath = ToIndexPath(path);
if (!_renderIndex->HasRprim(indexPath)) {
continue;
}

int stride = 1;
HdContainerDataSourceHandle dataSource = prim.dataSource;
for (;;) {
const HdInstancedBySchema instancedBy
= HdInstancedBySchema::GetFromParent(dataSource);
const SdfPath parent = first(instancedBy.GetPaths());
const SdfPath parentRoot = first(instancedBy.GetPrototypeRoots());
if (parent.IsEmpty()) {
break;
}
if (parent == instancer) {
// A level with no visible instances draws nothing.
if (stride > 0) {
rprims.push_back({ indexPath, parentRoot, stride });
}
break;
}
stride *= static_cast<int>(_InstanceIds(parent, parentRoot).size());
dataSource = _selectionSceneIndex->GetPrim(parent).dataSource;
}
}
}

return &rprims;
}

/* ComputeInstanceIndicesForProto is the call the emulation delegate's
GetInstanceIndices makes for HdVP2Instancer, so its positions are the ids
the rprims draw with. It also applies the instancer's mask, which the raw
instancerTopology/instanceIndices array does not.
*/
const std::unordered_map<int, int>&
HdVP2SceneIndexProducer::_InstanceIds(const SdfPath& instancer, const SdfPath& prototypeRoot)
{
// Keyed by prototype root alone: every propagated prototype root belongs to
// one instancer.
auto [ids, inserted] = _instanceIds.try_emplace(prototypeRoot);
if (inserted) {
// Not const: ComputeInstanceIndicesForProto is non-const before USD
// 26.08. Make this const once the minimum reaches 26.08.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe add a TODO so it can be found easily

HdInstancerTopologySchema topology = HdInstancerTopologySchema::GetFromParent(
_selectionSceneIndex->GetPrim(instancer).dataSource);
const VtIntArray indices = topology.ComputeInstanceIndicesForProto(prototypeRoot);
ids->second.reserve(indices.size());
for (size_t id = 0; id < indices.size(); ++id) {
ids->second.emplace(indices[id], static_cast<int>(id));
}
}
return ids->second;
}

/* Populates the HdSelection object VP2 highlights from, via
ProxyRenderDelegate::GetSelectionStatus.

Expand All @@ -503,12 +629,12 @@ SdfPathVector HdVP2SceneIndexProducer::GetScenePrimPaths(
the size of the stage. After that, the cost tracks what is selected.

AddSelection takes only a path, so a single point instance - what the
"Instances" pick mode produces - highlights every instance of its point
instancer for now.
"Instances" pick mode produces - is resolved by _PointInstanceRprimsOf
instead.
*/
void HdVP2SceneIndexProducer::PopulateSelection(
const SdfPath& usdPath,
int /* instanceIndex */,
const SdfPath& usdPath,
int instanceIndex,
const HdSelectionSharedPtr& result)
{
// An expired UFE item has no path. AddSelection would treat the empty path
Expand All @@ -517,6 +643,24 @@ void HdVP2SceneIndexProducer::PopulateSelection(
return;
}

if (instanceIndex >= 0) {
if (const _PointInstanceRprims* const rprims = _PointInstanceRprimsOf(usdPath)) {
for (const _PointInstanceRprim& rprim : *rprims) {
const std::unordered_map<int, int>& ids
= _InstanceIds(usdPath, rprim.prototypeRoot);
const auto id = ids.find(instanceIndex);
if (id == ids.end()) {
// This prototype does not instantiate the picked index.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an error scenario?

continue;
}
VtIntArray instanceIds(rprim.stride);
std::iota(instanceIds.begin(), instanceIds.end(), id->second * rprim.stride);
result->AddInstance(HdSelection::HighlightModeSelect, rprim.indexPath, instanceIds);
}
return;
}
}

_selectionSceneIndex->AddSelection(usdPath);

if (const HdSelectionSharedPtr resolved = _selectionObserver.GetSelection()) {
Expand Down Expand Up @@ -551,6 +695,16 @@ void HdVP2SceneIndexProducer::PopulateSelection(
_selectionSceneIndex->ClearSelection();
}

void HdVP2SceneIndexProducer::ClearSelection()
{
// ProxyRenderDelegate::_PopulateSelection calls this once before rebuilding
// the selection, which makes it the point at which the point instance caches
// may safely go stale. The selection scene index needs no clearing here:
// PopulateSelection clears it after every item.
_pointInstanceRprims.clear();
_instanceIds.clear();
}

/* Emits the invalidation from inside our own chain rather than through
HdChangeTracker, which cannot reach prims contributed by
HdRenderIndex::InsertSceneIndex - see HdVP2DirtyingSceneIndex.
Expand Down
59 changes: 59 additions & 0 deletions lib/mayaUsd/render/vp2RenderDelegate/sceneIndexProducer.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
#include <pxr/usdImaging/usdImaging/selectionSceneIndex.h>
#include <pxr/usdImaging/usdImaging/stageSceneIndex.h>

#include <map>
#include <optional>
#include <unordered_map>
#include <vector>

PXR_NAMESPACE_OPEN_SCOPE
Expand Down Expand Up @@ -89,6 +92,12 @@ class HdVP2DirtyingSceneIndex final : public HdNoticeBatchingSceneIndex
an HdSceneDelegate*. The render delegate, render index, task controller and
Hydra engine are unchanged.

Selecting a single point instance highlights that instance only, including
what its prototypes instance again: native instances and nested point
instancers. A point instancer the chain does not draw as an instancer, which
picking never yields as a point instance, highlights all of its instances.
See PopulateSelection.

\class HdVP2SceneIndexProducer
*/
class HdVP2SceneIndexProducer final : public HdVP2UsdProducer
Expand Down Expand Up @@ -134,13 +143,57 @@ class HdVP2SceneIndexProducer final : public HdVP2UsdProducer
int instanceIndex,
const HdSelectionSharedPtr& result) override;

void ClearSelection() override;

void MarkRprimDirty(const SdfPath& indexPath, HdDirtyBits bits) override;
void MarkSprimDirty(const SdfPath& indexPath, HdDirtyBits bits) override;

HdSceneDelegate* GetSceneDelegate() const override;
UsdImagingDelegate* GetUsdImagingDelegate() const override;

private:
/*! \brief An rprim a point instancer draws, as point instance selection
needs it.

Two index spaces meet in point instance selection. An *instance index*
indexes UsdGeomPointInstancer's protoIndices and positions; that is what
UFE carries in a point instance path and what reaches
PopulateSelection. An *instance id* is a position in the array of
instances an rprim is drawn with; that is what HdSelection::AddInstance
takes, and what HdVP2Mesh and HdVP2Instancer index with.

The point instance with instance index i has id k, the position of i in
HdInstancerTopologySchema::ComputeInstanceIndicesForProto(prototypeRoot).
An rprim instanced again below the point instancer - by a native
instance or a nested point instancer in the prototype - is drawn stride
times per point instance, with ids [k * stride, (k + 1) * stride).
*/
struct _PointInstanceRprim
{
SdfPath indexPath; //!< The rprim.
SdfPath prototypeRoot; //!< The point instancer's prototype root above it.
int stride; //!< Instances drawn per point instance.
};

using _PointInstanceRprims = std::vector<_PointInstanceRprim>;

/*! \brief The rprims a point instancer draws, cached until ClearSelection.

\return nullptr when the chain does not draw instancer as an instancer,
which tells PopulateSelection to resolve the item with
AddSelection instead.
*/
const _PointInstanceRprims* _PointInstanceRprimsOf(const SdfPath& instancer);

/*! \brief Instance index to instance id, for the instances of
prototypeRoot, cached until ClearSelection.

A map rather than a search over the array, because one selection pass
can ask about tens of thousands of instances of one instancer.
*/
const std::unordered_map<int, int>&
_InstanceIds(const SdfPath& instancer, const SdfPath& prototypeRoot);

/*! \brief Converts a render index path back into a path in this chain.

The exact inverse of ToIndexPath, and a different question from the one
Expand Down Expand Up @@ -204,6 +257,12 @@ class HdVP2SceneIndexProducer final : public HdVP2UsdProducer
//! back into an HdSelection.
HdxSelectionSceneIndexObserver _selectionObserver;

//! Caches for point instance selection, valid for one selection pass. An
//! empty optional records an instancer that cannot be resolved, so it is
//! probed only once per pass.
std::map<SdfPath, std::optional<_PointInstanceRprims>> _pointInstanceRprims;
std::map<SdfPath, std::unordered_map<int, int>> _instanceIds;

//! HdsiLegacyDisplayStyleOverrideSceneIndex is set-only, so the current
//! fallback is tracked here. 0 matches UsdImagingDelegate's default.
int _refineLevelFallback { 0 };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,12 @@ void HdVP2UsdImagingDelegateProducer::PopulateSelection(
_delegate->PopulateSelection(HdSelection::HighlightModeSelect, path, instanceIndex, result);
}

void HdVP2UsdImagingDelegateProducer::ClearSelection()
{
// The Hydra 1.0 delegate manages selection internally through PopulateSelection.
// No explicit clearing needed.
}

// UsdImagingDelegate is a legacy scene delegate, so its prims are in the render
// index's HdLegacyPrimSceneIndex whether or not emulation is on. The change
// tracker reaches them either way.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ class HdVP2UsdImagingDelegateProducer final : public HdVP2UsdProducer
int instanceIndex,
const HdSelectionSharedPtr& result) override;

void ClearSelection() override;

void MarkRprimDirty(const SdfPath& indexPath, HdDirtyBits bits) override;
void MarkSprimDirty(const SdfPath& indexPath, HdDirtyBits bits) override;

Expand Down
4 changes: 4 additions & 0 deletions lib/mayaUsd/render/vp2RenderDelegate/usdProducer.h
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ class HdVP2UsdProducer
PopulateSelection(const SdfPath& usdPath, int instanceIndex, const HdSelectionSharedPtr& result)
= 0;

//! \brief Clear the current selection state. Called before a new selection
//! update cycle begins, prior to PopulateSelection calls.
virtual void ClearSelection() = 0;

/*! \brief Invalidates a prim this producer contributed.

VP2 dirties prims imperatively for selection highlight, display mode,
Expand Down
Loading