Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
227 changes: 203 additions & 24 deletions lib/adskHydraSceneBrowser/test/adskHydraSceneBrowserTestFixture.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@
#include <pxr/base/gf/vec2f.h>
#include <pxr/base/gf/vec3d.h>
#include <pxr/base/gf/vec3f.h>
#include <pxr/base/tf/stringUtils.h>
#include <pxr/base/vt/array.h>
#include <pxr/base/vt/value.h>
#include <pxr/imaging/hd/materialSchema.h>
#include <pxr/imaging/hd/materialBindingsSchema.h>
#include <pxr/imaging/hd/primOriginSchema.h>

Comment thread
lanierd-adsk marked this conversation as resolved.
#include <gtest/gtest.h>

Expand All @@ -37,9 +39,69 @@
#include <QVBoxLayout>
#include <iostream>
#include <regex>
#include <set>
#include <stack>
#include <vector>

namespace {

#if PXR_VERSION >= 2603
// Matches HduiDataSourceTreeWidget's name ordering (USD 26.03+).
std::vector<PXR_NS::TfToken>
GetSortedContainerChildNames(const PXR_NS::HdContainerDataSourceHandle& container)
{
const PXR_NS::TfTokenVector names = container->GetNames();
const std::set<PXR_NS::TfToken, PXR_NS::TfDictionaryLessThan> sortedNames(
names.begin(), names.end());
return std::vector<PXR_NS::TfToken>(sortedNames.begin(), sortedNames.end());
}

void PushSortedContainerChildrenOnStack(
const PXR_NS::HdContainerDataSourceHandle& container,
const PXR_NS::HdDataSourceLocator& parentLocator,
std::stack<DataSourceEntry>& dataSourceStack)
{
const std::vector<PXR_NS::TfToken> sortedChildNames = GetSortedContainerChildNames(container);
for (auto itChildNames = sortedChildNames.rbegin(); itChildNames != sortedChildNames.rend();
++itChildNames) {
const PXR_NS::TfToken& childName = *itChildNames;
if (PXR_NS::HdDataSourceBaseHandle childDataSource = container->Get(childName)) {
dataSourceStack.push(
{ childName, childDataSource, parentLocator.Append(childName) });
}
}
}
#endif

std::stack<DataSourceEntry> BuildInitialDataSourceStack(
const PXR_NS::SdfPath& primPath, const PXR_NS::HdSceneIndexPrim& prim)
{
std::stack<DataSourceEntry> dataSourceStack;

#if PXR_VERSION >= 2603
// From USD 26.03, HduiDataSourceTreeWidget::SetPrimDataSource lists sorted
// container children as top-level items instead of the prim data source
// container itself (introduced by OpenUSD commit 6be1d6ec75).
if (PXR_NS::HdContainerDataSourceHandle container
= PXR_NS::HdContainerDataSource::Cast(prim.dataSource)) {
PushSortedContainerChildrenOnStack(
container, PXR_NS::HdDataSourceLocator(), dataSourceStack);
} else if (prim.dataSource) {
dataSourceStack.push(
{ primPath.GetNameToken(), prim.dataSource, PXR_NS::HdDataSourceLocator() });
}
#else
// USD < 26.03: SetPrimDataSource shows the prim data source container
// itself as the single top-level item, with text = primPath.GetNameToken().
dataSourceStack.push(
{ primPath.GetNameToken(), prim.dataSource, PXR_NS::HdDataSourceLocator() });
#endif

return dataSourceStack;
}

} // namespace

template <class ChildType> ChildType* FindFirstChild(QObject* qObject)
{
for (QObject* child : qObject->children()) {
Expand All @@ -51,14 +113,44 @@ template <class ChildType> ChildType* FindFirstChild(QObject* qObject)
return nullptr;
}

int CountTreeItems(QTreeWidget* treeWidget)
{
int count = 0;
for (QTreeWidgetItemIterator it(treeWidget); *it; ++it) {
++count;
}
return count;
}

QTreeWidgetItemIterator GetIteratorForTree(QTreeWidget* treeWidget)
{
// Expand all items so the iterator can traverse them
treeWidget->expandAll();
// Immediately process queued events, otherwise some events might linger and lead to a crash
// trying to access since-deleted items once the Qt event loop resumes and processes the events.
// (e.g. without this there is a crash involving a setExpanded() call)
QApplication::processEvents(QEventLoop::ProcessEventsFlag::EventLoopExec);
// Hdui_DataSourceTreeWidgetItem builds children lazily (on first expand).
// A static _expandedSet persists across prim selections: items whose
// locator is in the set schedule their expansion via QTimer::singleShot(0).
// processEvents() fires those timers and creates a new generation of child
// items — but expandAll() has already returned, so those new children are
// never expanded themselves. Loop until the item count stabilises so that
// every generation of lazily-built children is fully expanded before the
// iterator is created.
static constexpr int kMaxExpansionIterations = 20;
int prevCount = -1;
int currCount = 0;
int iterations = 0;
while (currCount != prevCount) {
EXPECT_LT(iterations, kMaxExpansionIterations)
<< "Data source tree did not stabilise after " << kMaxExpansionIterations
<< " expansion iterations — possible infinite loop in lazy item creation.";
if (iterations >= kMaxExpansionIterations) {
break;
}
prevCount = currCount;
treeWidget->expandAll();
// Process queued events: fires deferred QTimer::singleShot expansions
// and avoids crashes with since-deleted items (see original comment).
QApplication::processEvents(QEventLoop::ProcessEventsFlag::EventLoopExec);
currCount = CountTreeItems(treeWidget);
++iterations;
}
Comment on lines +139 to +156
return QTreeWidgetItemIterator(treeWidget);
}

Expand Down Expand Up @@ -132,38 +224,65 @@ void AdskHydraSceneBrowserTestFixture::ComparePrimHierarchy(
// Compare data source
if (compareDataSourceHierarchy) {
_primHierarchyWidget->setCurrentItem(primQtItem);
CompareDataSourceHierarchy( primPath,
{ primPath.GetNameToken(), prim.dataSource, PXR_NS::HdDataSourceLocator() }, compareDataSourceValues);
CompareDataSourceHierarchy(primPath, BuildInitialDataSourceStack(primPath, prim),
compareDataSourceValues);
}

// Prepare next step (need to pop the stack before pushing the next elements)
itPrimsTreeWidget++;
primPathsStack.pop();

// Push child paths on the stack
PXR_NS::SdfPathVector childPaths = sceneIndex->GetChildPrimPaths(primPath);
for (auto itChildPaths = childPaths.rbegin(); itChildPaths != childPaths.rend();
itChildPaths++) {
// Push child paths on the stack in the same order used by
// HduiSceneIndexTreeWidget.
const PXR_NS::SdfPathVector childPathVec = sceneIndex->GetChildPrimPaths(primPath);
#if PXR_VERSION >= 2603
// USD 26.03+: children are listed in sorted order (see OpenUSD 6be1d6ec75).
const PXR_NS::SdfPathSet sortedChildPaths(childPathVec.begin(), childPathVec.end());
for (auto itChildPaths = sortedChildPaths.rbegin(); itChildPaths != sortedChildPaths.rend();
++itChildPaths) {
primPathsStack.push(*itChildPaths);
}
#else
// USD < 26.03: children follow GetChildPrimPaths() order; push in
// reversed order so stack pops in forward (matching) order.
for (auto itChildPaths = childPathVec.rbegin(); itChildPaths != childPathVec.rend();
++itChildPaths) {
primPathsStack.push(*itChildPaths);
}
#endif
}

// Ensure both sides are fully exhausted — if one has remaining items the
// traversal would have silently stopped short.
EXPECT_FALSE(*itPrimsTreeWidget) << "Qt prim tree has more items than expected by the scene index";
EXPECT_TRUE(primPathsStack.empty()) << "Scene index has more prims than present in the Qt prim tree";
}

void AdskHydraSceneBrowserTestFixture::CompareDataSourceHierarchy(
const PXR_NS::SdfPath& primPath,
DataSourceEntry rootDataSourceEntry,
bool compareValues)
const PXR_NS::SdfPath& primPath,
std::stack<DataSourceEntry> initialDataSourceStack,
bool compareValues)
{
// Setup traversal data structures (depth-first search)
QTreeWidgetItemIterator itDataSourceTreeWidget = GetIteratorForTree(_dataSourceHierarchyWidget);
std::stack<DataSourceEntry> dataSourceStack({ rootDataSourceEntry });
std::stack<DataSourceEntry> dataSourceStack = std::move(initialDataSourceStack);

// Track traversal statistics for diagnostics.
int qtItemsVisited = 0;
std::string lastQtText;
std::string lastStackLocator;

// Traverse hierarchy and compare (depth-first search)
while (*itDataSourceTreeWidget && !dataSourceStack.empty()) {
// Get the objects for the current step
QTreeWidgetItem* dataSourceQtItem = *itDataSourceTreeWidget;
DataSourceEntry dataSourceEntry = dataSourceStack.top();

lastQtText = dataSourceQtItem->text(0).toStdString();
lastStackLocator = dataSourceEntry.locator.IsEmpty()
? std::string("<root>")
: dataSourceEntry.locator.GetString();

// Compare data source name
CompareDataSourceName(primPath, dataSourceQtItem, dataSourceEntry);

Expand All @@ -179,19 +298,26 @@ void AdskHydraSceneBrowserTestFixture::CompareDataSourceHierarchy(
// Prepare next step (need to pop the stack before pushing the next elements)
itDataSourceTreeWidget++;
dataSourceStack.pop();
++qtItemsVisited;

// Push child data sources on the stack
if (auto containerDataSource
= PXR_NS::HdContainerDataSource::Cast(dataSourceEntry.dataSource)) {
#if PXR_VERSION >= 2603
// USD 26.03+: _BuildChildren uses sorted order (see 6be1d6ec75).
PushSortedContainerChildrenOnStack(
containerDataSource, dataSourceEntry.locator, dataSourceStack);
#else
// USD < 26.03: _BuildChildren uses GetNames() forward order; push
// in reversed order so stack pops in forward (matching) order.
PXR_NS::TfTokenVector childNames = containerDataSource->GetNames();
for (auto itChildNames = childNames.rbegin(); itChildNames != childNames.rend();
itChildNames++) {
PXR_NS::TfToken dataSourceName = *itChildNames;
PXR_NS::HdDataSourceBaseHandle dataSource = containerDataSource->Get(dataSourceName);
if (dataSource) {
dataSourceStack.push({ dataSourceName, dataSource, dataSourceEntry.locator.Append(dataSourceName) });
for (auto it = childNames.rbegin(); it != childNames.rend(); ++it) {
if (PXR_NS::HdDataSourceBaseHandle ds = containerDataSource->Get(*it)) {
dataSourceStack.push(
{ *it, ds, dataSourceEntry.locator.Append(*it) });
}
}
#endif
} else if (
auto vectorDataSource = PXR_NS::HdVectorDataSource::Cast(dataSourceEntry.dataSource)) {
for (size_t iElement = 0; iElement < vectorDataSource->GetNumElements(); iElement++) {
Comment thread
lanierd-adsk marked this conversation as resolved.
Expand All @@ -205,6 +331,39 @@ void AdskHydraSceneBrowserTestFixture::CompareDataSourceHierarchy(
}
}
}

// Ensure both sides are fully exhausted — if one has remaining items the
// traversal would have silently stopped short (e.g. when the initial stack
// is empty on USD 26.03+ and the UI still has stale entries).
EXPECT_FALSE(*itDataSourceTreeWidget)
<< "Qt data source tree has more items than expected for prim " << primPath.GetText();
if (!dataSourceStack.empty()) {
// Collect remaining stack locators (up to a reasonable limit) for diagnostics.
std::string remaining;
int shown = 0;
std::stack<DataSourceEntry> tmp = dataSourceStack;
while (!tmp.empty() && shown < 10) {
const std::string loc = tmp.top().locator.IsEmpty()
? std::string("<root>")
: tmp.top().locator.GetString();
remaining += "\n [" + std::to_string(shown) + "] " + loc;
tmp.pop();
++shown;
}
if (!tmp.empty()) {
remaining += "\n ... (" + std::to_string(tmp.size()) + " more)";
}
ADD_FAILURE() << "Expected more data source items than present in the Qt tree for prim "
<< primPath.GetText()
<< "\nItems matched before exhaustion: " << qtItemsVisited
<< "\nLast Qt item text seen: \"" << lastQtText << "\""
<< "\nLast stack locator: " << lastStackLocator
<< "\nFirst missing locator: "
<< (dataSourceStack.top().locator.IsEmpty()
? std::string("<root>")
: dataSourceStack.top().locator.GetString())
<< "\nRemaining expected items (stack, top-first):" << remaining;
}
}

void AdskHydraSceneBrowserTestFixture::CompareDataSourceName(
Expand Down Expand Up @@ -298,8 +457,28 @@ void AdskHydraSceneBrowserTestFixture::CompareValueContent(const PXR_NS::VtValue
for (PXR_NS::SdfPath const& path : paths) {
valueStream << path << "\n";
}
}
else {
} else if (value.IsHolding<PXR_NS::HdPrimOriginSchema::OriginPath>()) {
// Mirror HduiDataSourceValueTreeView: when OriginPath is a registered
// Vt type, operator<< outputs "HdPrimOriginSchema::OriginPath(<path>)"
// and the widget displays that same string. When OriginPath is not yet
// a registered Vt type, the widget calls .GetPath() directly. Detect
// at runtime so this works across USD builds regardless of what
// PXR_VERSION says.
//
// We cannot use MatchesFallbackTextOutput here: its regex excludes ':'
// so the unregistered fallback "<'HdPrimOriginSchema::OriginPath' @
// 0x...>" would not be detected. Instead check the prefix directly.
std::ostringstream probeStream;
probeStream << value;
const std::string probeOutput = probeStream.str();
if (probeOutput.rfind("HdPrimOriginSchema::OriginPath(", 0) == 0) {
// OriginPath is registered: widget uses VtValue streaming.
valueStream << probeOutput;
} else {
// OriginPath is not registered: widget calls .GetPath().
valueStream << value.UncheckedGet<PXR_NS::HdPrimOriginSchema::OriginPath>().GetPath();
}
} else {
valueStream << value;
}
#endif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@

#include <gtest/gtest.h>

#include <stack>

#include <dataSourceTreeWidget.h>
#include <dataSourceValueTreeView.h>
#include <sceneIndexDebuggerWidget.h>
Expand Down Expand Up @@ -54,8 +56,10 @@ class AdskHydraSceneBrowserTestFixture : public ::testing::Test
bool compareDataSourceHierarchy = false,
bool compareDataSourceValues = false);

void
CompareDataSourceHierarchy(const PXR_NS::SdfPath& primPath, DataSourceEntry rootDataSourceEntry, bool compareValues = false);
void CompareDataSourceHierarchy(
const PXR_NS::SdfPath& primPath,
std::stack<DataSourceEntry> initialDataSourceStack,
bool compareValues = false);

void
CompareDataSourceName(const PXR_NS::SdfPath& primPath, const QTreeWidgetItem* dataSourceQtItem, const DataSourceEntry& dataSourceEntry);
Expand Down
1 change: 1 addition & 0 deletions lib/mayaHydra/mayaPlugin/renderOverride.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1798,6 +1798,7 @@ void MtohRenderOverride::_CreateSceneIndicesChainAfterMergingSceneIndex(const MH
const bool pruneTextures = !(drawContext.getDisplayStyle() & MHWRender::MFrameContext::kTextured);
_lastFilteringSceneIndexBeforeCustomFiltering = _pruneTexturesSceneIndex =
Fvp::PruneTexturesSceneIndex::New(_lastFilteringSceneIndexBeforeCustomFiltering, pruneTextures);
_currentlyTextured = !pruneTextures;

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.

Nice, I had this fix locally for materialX testing. Thanks for pushing this


// Add default material scene index
_lastFilteringSceneIndexBeforeCustomFiltering = _defaultMaterialSceneIndex = Fvp::DefaultMaterialSceneIndex::New(_lastFilteringSceneIndexBeforeCustomFiltering,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# USD 26.05+ Baselines — ArnoldLightsTest

| Image | Source |
|-------|--------|
| `allLights.png` | **New in USD 26.05** — updated baseline from failing preflight |

## Why unchanged images are copied here instead of relying on usd25.11/

The test framework's `resolveRefImage` builds the baseline path as
`<inputDir>/<imageVersion>/<imageName>` with no fallback to the parent folder.
If `imageVersion` is set to `"usd26.05+"` and a file is missing from this folder,
the test will fail with a missing file error rather than falling back to `usd25.11/`.
Therefore all images that this test compares with `imageVersion` must be present here,
even those whose pixel content is identical to the `usd25.11/` version.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# USD 26.05+ Baselines — TexturedModeTest

| Image | Source |
|-------|--------|
| `untextured.png` | **New in USD 26.05** — updated baseline from failing preflight |
| `textured.png` | Copied from `TexturedModeTest/` root — no visual change in USD 26.05 |

## Why unchanged images are copied here instead of relying on the root folder

The test framework's `resolveRefImage` builds the baseline path as
`<inputDir>/<imageVersion>/<imageName>` with no fallback to the parent folder.
If `imageVersion` is set to `"usd26.05+"` and a file is missing from this folder,
the test will fail with a missing file error rather than falling back to the root
`TexturedModeTest/` directory.
Therefore all images that this test compares with `imageVersion` must be present here,
even those whose pixel content is identical to the root version.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# USD 26.05+ Baselines — USDLightsTest

| Image | Source |
|-------|--------|
| `allLights.png` | **New in USD 26.05** — updated baseline from failing preflight |
| `domeLight.png` | Copied from `usd25.11/` — no visual change in USD 26.05 |
| `flatLight.png` | Copied from `usd25.11/` — no visual change in USD 26.05 |

## Why unchanged images are copied here instead of relying on usd25.11/

The test framework's `resolveRefImage` builds the baseline path as
`<inputDir>/<imageVersion>/<imageName>` with no fallback to the parent folder.
If `imageVersion` is set to `"usd26.05+"` and a file is missing from this folder,
the test will fail with a missing file error rather than falling back to `usd25.11/`.
Therefore all images that this test compares with `imageVersion` must be present here,
even those whose pixel content is identical to the `usd25.11/` version.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading