diff --git a/lib/mayaUsd/listeners/stageNoticeListener.cpp b/lib/mayaUsd/listeners/stageNoticeListener.cpp index 2dadc8a457..35b439c67c 100644 --- a/lib/mayaUsd/listeners/stageNoticeListener.cpp +++ b/lib/mayaUsd/listeners/stageNoticeListener.cpp @@ -17,8 +17,12 @@ #include #include +#include +#include #include #include +#include +#include PXR_NAMESPACE_OPEN_SCOPE @@ -168,4 +172,90 @@ void UsdMayaStageNoticeListener::_OnStageEditTargetChanged( } } +namespace { +bool _IsUiSchemaPrepend(const VtValue& v) +{ + static std::set UiSchemaPrependHashes; + std::once_flag hasHashes; + std::call_once(hasHashes, [&]() { + SdfTokenListOp op; + op.SetPrependedItems(TfTokenVector { TfToken("NodeGraphNodeAPI") }); + UiSchemaPrependHashes.insert(hash_value(op)); + }); + + if (v.IsHolding()) { + const size_t hash = hash_value(v.UncheckedGet()); + if (UiSchemaPrependHashes.count(hash)) { + return true; + } + } + return false; +} +} // namespace + +UsdMayaStageNoticeListener::ChangeType +UsdMayaStageNoticeListener::ClassifyObjectsChanged(UsdNotice::ObjectsChanged const& notice) +{ + using PathRange = UsdNotice::ObjectsChanged::PathRange; + + auto range = notice.GetResyncedPaths(); + if (!range.empty()) { + size_t ignoredCount = 0; + size_t resyncCount = 0; + for (auto it = range.begin(); it != range.end(); ++it) { + if (it->IsPropertyPath()) { + // We have a bunch of UI properties to ignore. Especially anything that comes from + // UI schemas. + if (it->GetName().rfind("ui:", 0) == 0) { + ++ignoredCount; + continue; + } + } + for (const SdfChangeList::Entry* entry : it.base()->second) { + for (auto&& infoChanged : entry->infoChanged) { + if (infoChanged.first == UsdTokens->apiSchemas + && _IsUiSchemaPrepend(infoChanged.second.second)) { + ++ignoredCount; + } else { + ++resyncCount; + } + } + } + } + + if (ignoredCount) { + return resyncCount ? ChangeType::kResync : ChangeType::kIgnored; + } else { + return ChangeType::kResync; + } + } + + auto retVal = ChangeType::kIgnored; + + const PathRange pathsToUpdate = notice.GetChangedInfoOnlyPaths(); + for (PathRange::const_iterator it = pathsToUpdate.begin(); it != pathsToUpdate.end(); ++it) { + if (it->IsAbsoluteRootOrPrimPath()) { + const TfTokenVector changedFields = it.GetChangedFields(); + if (!changedFields.empty()) { + retVal = ChangeType::kUpdate; + } + } else if (it->IsPropertyPath()) { + // We have a bunch of UI properties to ignore. Especially anything that comes from UI + // schemas. + if (it->GetName().rfind("ui:", 0) == 0) { + continue; + } + retVal = ChangeType::kUpdate; + for (const auto& entry : it.base()->second) { + if (entry->flags.didChangeAttributeConnection) { + retVal = ChangeType::kResync; + break; + } + } + } + } + + return retVal; +} + PXR_NAMESPACE_CLOSE_SCOPE diff --git a/lib/mayaUsd/listeners/stageNoticeListener.h b/lib/mayaUsd/listeners/stageNoticeListener.h index 552ad51dd2..9f8cb48ae5 100644 --- a/lib/mayaUsd/listeners/stageNoticeListener.h +++ b/lib/mayaUsd/listeners/stageNoticeListener.h @@ -77,6 +77,33 @@ class UsdMayaStageNoticeListener : public TfWeakBase MAYAUSD_CORE_PUBLIC void SetStageEditTargetChangedCallback(const StageEditTargetChangedCallback& callback); + /// We have incoming changes that USD will consider either requiring an + /// update (meaning the render delegate needs to refresh and redraw) or + /// a resync (meaning the scene delegate needs to fetch new datum). We + /// want external clients to be aware of these classes of updates in case + /// they do not use the Hydra system for refreshing and drawing the scene. + enum class ChangeType + { + kIgnored, // Change does not require redraw: UI change, metadata change. + kUpdate, // Change requires redraw after refreshing parameter values + kResync // Change requires refreshing cached buffers + }; + + /// This is a stripped down copy of UsdImagingDelegate::_OnUsdObjectsChanged which is the main + /// USD notification handler where paths to refresh and paths to update are compiled for the + /// next Hydra refresh. We do not gather paths as there is no simple way to know when to flush + /// these maps. + /// + /// This needs to stay as quick as possible since it is stuck in the middle of the notification + /// code path. + /// + /// This is a work in progress. Some improvements might be necessary in the future. The + /// following potential issues are already visible: + /// + /// - Changing a parameter value for the first time creates the attribute, which is a kResync + MAYAUSD_CORE_PUBLIC + static ChangeType ClassifyObjectsChanged(UsdNotice::ObjectsChanged const& notice); + private: UsdMayaStageNoticeListener(const UsdMayaStageNoticeListener&) = delete; UsdMayaStageNoticeListener& operator=(const UsdMayaStageNoticeListener&) = delete; diff --git a/lib/mayaUsd/nodes/CMakeLists.txt b/lib/mayaUsd/nodes/CMakeLists.txt index cdfe07ce6e..86f2fa0fa9 100644 --- a/lib/mayaUsd/nodes/CMakeLists.txt +++ b/lib/mayaUsd/nodes/CMakeLists.txt @@ -9,7 +9,7 @@ target_sources(${PROJECT_NAME} proxyShapeBase.cpp proxyShapePlugin.cpp proxyShapeStageExtraData.cpp - proxyShapeUpdateManager.cpp + proxyShapeListenerBase.cpp stageData.cpp stageNode.cpp usdPrimProvider.cpp @@ -23,7 +23,7 @@ set(HEADERS proxyShapePlugin.h proxyStageProvider.h proxyShapeStageExtraData.h - proxyShapeUpdateManager.h + proxyShapeListenerBase.h stageData.h stageNode.h usdPrimProvider.h diff --git a/lib/mayaUsd/nodes/proxyShapeBase.cpp b/lib/mayaUsd/nodes/proxyShapeBase.cpp index dd4d2af26f..646c2ed1a0 100644 --- a/lib/mayaUsd/nodes/proxyShapeBase.cpp +++ b/lib/mayaUsd/nodes/proxyShapeBase.cpp @@ -157,9 +157,6 @@ MObject MayaUsdProxyShapeBase::drawGuidePurposeAttr; MObject MayaUsdProxyShapeBase::sessionLayerNameAttr; MObject MayaUsdProxyShapeBase::rootLayerNameAttr; MObject MayaUsdProxyShapeBase::mutedLayersAttr; -// Change counter attributes -MObject MayaUsdProxyShapeBase::updateCounterAttr; -MObject MayaUsdProxyShapeBase::resyncCounterAttr; // Output attributes MObject MayaUsdProxyShapeBase::outTimeAttr; MObject MayaUsdProxyShapeBase::outStageDataAttr; @@ -403,26 +400,6 @@ MStatus MayaUsdProxyShapeBase::initialize() retValue = addAttribute(outStageDataAttr); CHECK_MSTATUS_AND_RETURN_IT(retValue); - updateCounterAttr - = numericAttrFn.create("updateId", "upid", MFnNumericData::kInt64, -1, &retValue); - CHECK_MSTATUS_AND_RETURN_IT(retValue); - numericAttrFn.setStorable(false); - numericAttrFn.setWritable(false); - numericAttrFn.setHidden(true); - numericAttrFn.setInternal(true); - retValue = addAttribute(updateCounterAttr); - CHECK_MSTATUS_AND_RETURN_IT(retValue); - - resyncCounterAttr - = numericAttrFn.create("resyncId", "rsid", MFnNumericData::kInt64, -1, &retValue); - CHECK_MSTATUS_AND_RETURN_IT(retValue); - numericAttrFn.setStorable(false); - numericAttrFn.setWritable(false); - numericAttrFn.setHidden(true); - numericAttrFn.setInternal(true); - retValue = addAttribute(resyncCounterAttr); - CHECK_MSTATUS_AND_RETURN_IT(retValue); - outStageCacheIdAttr = numericAttrFn.create("outStageCacheId", "ostcid", MFnNumericData::kInt, -1, &retValue); CHECK_MSTATUS_AND_RETURN_IT(retValue); @@ -589,22 +566,6 @@ void MayaUsdProxyShapeBase::postConstructor() } } -/* virtual */ -bool MayaUsdProxyShapeBase::getInternalValue(const MPlug& plug, MDataHandle& handle) -{ - bool retVal = true; - - if (plug == updateCounterAttr) { - handle.set(_shapeUpdateManager.GetUpdateCount()); - } else if (plug == resyncCounterAttr) { - handle.set(_shapeUpdateManager.GetResyncCount()); - } else { - retVal = MPxSurfaceShape::getInternalValue(plug, handle); - } - - return retVal; -} - /* virtual */ MStatus MayaUsdProxyShapeBase::compute(const MPlug& plug, MDataBlock& dataBlock) { @@ -2025,6 +1986,11 @@ void MayaUsdProxyShapeBase::_OnStageObjectsChanged(const UsdNotice::ObjectsChang MProfilingScope profilingScope( _shapeBaseProfilerCategory, MProfiler::kColorB_L1, "Process USD objects changed"); + if (UsdMayaStageNoticeListener::ClassifyObjectsChanged(notice) + == UsdMayaStageNoticeListener::ChangeType::kIgnored) { + return; + } + // This will definitely force a BBox recomputation on "Frame All" or when framing a selected // stage. Computing bounds in USD is expensive, so if it pops up in other frequently used // scenarios we will have to investigate ways to make this cache clearing less expensive. @@ -2033,11 +1999,6 @@ void MayaUsdProxyShapeBase::_OnStageObjectsChanged(const UsdNotice::ObjectsChang ProxyAccessor::stageChanged(_usdAccessor, thisMObject(), notice); MayaUsdProxyStageObjectsChangedNotice(*this, notice).Send(); - // Also keeps track of the notification counters: - if (_shapeUpdateManager.CanIgnoreObjectsChanged(notice)) { - return; - } - // Recompute the extents of any UsdGeomBoundable that has authored extents const auto& stage = notice.GetStage(); if (stage != getUsdStage()) { diff --git a/lib/mayaUsd/nodes/proxyShapeBase.h b/lib/mayaUsd/nodes/proxyShapeBase.h index 91394f490d..a65460c480 100644 --- a/lib/mayaUsd/nodes/proxyShapeBase.h +++ b/lib/mayaUsd/nodes/proxyShapeBase.h @@ -55,7 +55,6 @@ constexpr char USD_UFE_SEPARATOR = '/'; #include #include #include -#include #include #include @@ -127,12 +126,6 @@ class MayaUsdProxyShapeBase MAYAUSD_CORE_PUBLIC static MObject mutedLayersAttr; - // Change counter attributes - MAYAUSD_CORE_PUBLIC - static MObject updateCounterAttr; - MAYAUSD_CORE_PUBLIC - static MObject resyncCounterAttr; - // Output attributes MAYAUSD_CORE_PUBLIC static MObject outTimeAttr; @@ -179,8 +172,6 @@ class MayaUsdProxyShapeBase MAYAUSD_CORE_PUBLIC void postConstructor() override; MAYAUSD_CORE_PUBLIC - bool getInternalValue(const MPlug&, MDataHandle&) override; - MAYAUSD_CORE_PUBLIC MStatus compute(const MPlug& plug, MDataBlock& dataBlock) override; MAYAUSD_CORE_PUBLIC bool isBounded() const override; @@ -415,9 +406,6 @@ class MayaUsdProxyShapeBase size_t _excludePrimPathsVersion { 1 }; size_t _UsdStageVersion { 1 }; - // This helper class keeps track of the notification counters: - MayaUsdProxyShapeUpdateManager _shapeUpdateManager; - MayaUsd::ProxyAccessor::Owner _usdAccessor; static ClosestPointDelegate _sharedClosestPointDelegate; diff --git a/lib/mayaUsd/nodes/proxyShapeListenerBase.cpp b/lib/mayaUsd/nodes/proxyShapeListenerBase.cpp new file mode 100644 index 0000000000..a0ce6ecbac --- /dev/null +++ b/lib/mayaUsd/nodes/proxyShapeListenerBase.cpp @@ -0,0 +1,227 @@ +// +// Copyright 2023 Autodesk +// +// 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 "proxyShapeListenerBase.h" + +#include + +#include +#include +#include + +#include + +using namespace MAYAUSD_NS_DEF; + +PXR_NAMESPACE_OPEN_SCOPE + +TF_DEFINE_PUBLIC_TOKENS( + MayaUsdProxyShapeListenerBaseTokens, + MAYAUSD_PROXY_SHAPE_LISTENER_BASE_TOKENS); + +const MTypeId MayaUsdProxyShapeListenerBase::typeId(0x5800009A); +const MString MayaUsdProxyShapeListenerBase::typeName( + MayaUsdProxyShapeListenerBaseTokens->MayaTypeName.GetText()); + +MObject MayaUsdProxyShapeListenerBase::updateCounterAttr; +MObject MayaUsdProxyShapeListenerBase::resyncCounterAttr; +MObject MayaUsdProxyShapeListenerBase::stageCacheIdAttr; +MObject MayaUsdProxyShapeListenerBase::outStageCacheIdAttr; + +MayaUsdProxyShapeListenerBase::~MayaUsdProxyShapeListenerBase() +{ + _stageNoticeListener.SetStage(UsdStageWeakPtr()); + _stageNoticeListener.SetStageObjectsChangedCallback(nullptr); +} + +void MayaUsdProxyShapeListenerBase::_ReInit() +{ + _lastKnownStageCacheId = -1; + _stageNoticeListener.SetStage(UsdStageWeakPtr()); + _stageNoticeListener.SetStageObjectsChangedCallback(nullptr); + _IncrementCounter(resyncCounterAttr); + _IncrementCounter(updateCounterAttr); +} + +void MayaUsdProxyShapeListenerBase::_OnStageObjectsChanged(const UsdNotice::ObjectsChanged& notice) +{ + switch (UsdMayaStageNoticeListener::ClassifyObjectsChanged(notice)) { + case UsdMayaStageNoticeListener::ChangeType::kIgnored: return; + case UsdMayaStageNoticeListener::ChangeType::kResync: _IncrementCounter(resyncCounterAttr); + // [[fallthrough]]; // We want that fallthrough to have the update always triggered. + case UsdMayaStageNoticeListener::ChangeType::kUpdate: + _IncrementCounter(updateCounterAttr); + break; + } +} + +void MayaUsdProxyShapeListenerBase::_IncrementCounter(MObject attribute) +{ + auto plug = MPlug(thisMObject(), attribute); + plug.setInt64(plug.asInt64() + 1); +} + +/* static */ +void* MayaUsdProxyShapeListenerBase::creator() { return new MayaUsdProxyShapeListenerBase(); } + +/* static */ +MStatus MayaUsdProxyShapeListenerBase::initialize() +{ + MStatus retValue = MS::kSuccess; + + // + // create attr factories + // + MFnNumericAttribute numericAttrFn; + + stageCacheIdAttr + = numericAttrFn.create("stageCacheId", "stcid", MFnNumericData::kInt, -1, &retValue); + CHECK_MSTATUS_AND_RETURN_IT(retValue); + numericAttrFn.setStorable(false); + numericAttrFn.setConnectable(false); + numericAttrFn.setReadable(false); + retValue = addAttribute(stageCacheIdAttr); + CHECK_MSTATUS_AND_RETURN_IT(retValue); + + outStageCacheIdAttr + = numericAttrFn.create("outStageCacheId", "ostcid", MFnNumericData::kInt, -1, &retValue); + CHECK_MSTATUS_AND_RETURN_IT(retValue); + numericAttrFn.setStorable(false); + numericAttrFn.setConnectable(true); + numericAttrFn.setWritable(false); + retValue = addAttribute(outStageCacheIdAttr); + CHECK_MSTATUS_AND_RETURN_IT(retValue); + + // + // Smart signaling attributes: + // + updateCounterAttr + = numericAttrFn.create("updateId", "upid", MFnNumericData::kInt64, 0, &retValue); + CHECK_MSTATUS_AND_RETURN_IT(retValue); + numericAttrFn.setStorable(false); + numericAttrFn.setHidden(true); + numericAttrFn.setIndeterminant(true); + numericAttrFn.setWritable(false); + numericAttrFn.setCached(false); + numericAttrFn.setAffectsAppearance(false); + retValue = addAttribute(updateCounterAttr); + CHECK_MSTATUS_AND_RETURN_IT(retValue); + + resyncCounterAttr + = numericAttrFn.create("resyncId", "rsid", MFnNumericData::kInt64, 0, &retValue); + CHECK_MSTATUS_AND_RETURN_IT(retValue); + numericAttrFn.setStorable(false); + numericAttrFn.setHidden(true); + numericAttrFn.setIndeterminant(true); + numericAttrFn.setWritable(false); + numericAttrFn.setCached(false); + numericAttrFn.setAffectsAppearance(false); + retValue = addAttribute(resyncCounterAttr); + CHECK_MSTATUS_AND_RETURN_IT(retValue); + + // + // add attribute dependencies + // + retValue = attributeAffects(stageCacheIdAttr, outStageCacheIdAttr); + CHECK_MSTATUS_AND_RETURN_IT(retValue); + + return retValue; +} + +void MayaUsdProxyShapeListenerBase::postConstructor() { setExistWithoutInConnections(false); } + +MStatus MayaUsdProxyShapeListenerBase::compute(const MPlug& plug, MDataBlock& dataBlock) +{ + if (plug == outStageCacheIdAttr) { + MStatus retValue = MS::kSuccess; + + MDataHandle inHandle = dataBlock.inputValue(stageCacheIdAttr, &retValue); + CHECK_MSTATUS_AND_RETURN_IT(retValue); + + int cacheIdNum = inHandle.asInt(); + + if (cacheIdNum != _lastKnownStageCacheId) { + const auto cacheId = UsdStageCache::Id::FromLongInt(cacheIdNum); + const auto stageCached + = cacheId.IsValid() && UsdUtilsStageCache::Get().Contains(cacheId); + if (stageCached) { + auto usdStage = UsdUtilsStageCache::Get().Find(cacheId); + _stageNoticeListener.SetStage(usdStage); + _stageNoticeListener.SetStageObjectsChangedCallback( + [this](const UsdNotice::ObjectsChanged& notice) { + return _OnStageObjectsChanged(notice); + }); + } else { + _stageNoticeListener.SetStage(nullptr); + _stageNoticeListener.SetStageObjectsChangedCallback(nullptr); + } + auto increment = [](MDataBlock& dataBlock, MObject attr) -> MStatus { + MStatus retStatus = MS::kSuccess; + MDataHandle inHandle = dataBlock.inputValue(attr, &retStatus); + CHECK_MSTATUS_AND_RETURN_IT(retStatus); + MDataHandle outHandle = dataBlock.outputValue(attr, &retStatus); + CHECK_MSTATUS_AND_RETURN_IT(retStatus); + outHandle.setInt64(inHandle.asInt64() + 1); + outHandle.setClean(); + return retStatus; + }; + + retValue = increment(dataBlock, updateCounterAttr); + CHECK_MSTATUS_AND_RETURN_IT(retValue); + retValue = increment(dataBlock, resyncCounterAttr); + CHECK_MSTATUS_AND_RETURN_IT(retValue); + + _lastKnownStageCacheId = cacheIdNum; + } + + MDataHandle outCacheIdHandle = dataBlock.outputValue(outStageCacheIdAttr, &retValue); + CHECK_MSTATUS_AND_RETURN_IT(retValue); + + outCacheIdHandle.set(cacheIdNum); + outCacheIdHandle.setClean(); + + return retValue; + } + + return MS::kUnknownParameter; +} + +MStatus +MayaUsdProxyShapeListenerBase::connectionMade(const MPlug& plug1, const MPlug& plug2, bool asSrc) +{ + if (plug1 == stageCacheIdAttr || plug2 == stageCacheIdAttr) { + _ReInit(); + } + return MPxNode::connectionMade(plug1, plug2, asSrc); +} + +MStatus +MayaUsdProxyShapeListenerBase::connectionBroken(const MPlug& plug1, const MPlug& plug2, bool asSrc) +{ + if (plug1 == stageCacheIdAttr || plug2 == stageCacheIdAttr) { + _ReInit(); + } + return MPxNode::connectionMade(plug1, plug2, asSrc); +} + +MStatus MayaUsdProxyShapeListenerBase::setDependentsDirty(const MPlug& plug, MPlugArray& plugArray) +{ + if (plug == stageCacheIdAttr) { + _ReInit(); + } + return MPxNode::setDependentsDirty(plug, plugArray); +} + +PXR_NAMESPACE_CLOSE_SCOPE diff --git a/lib/mayaUsd/nodes/proxyShapeListenerBase.h b/lib/mayaUsd/nodes/proxyShapeListenerBase.h new file mode 100644 index 0000000000..061e5e2c8b --- /dev/null +++ b/lib/mayaUsd/nodes/proxyShapeListenerBase.h @@ -0,0 +1,137 @@ +// +// Copyright 2023 Autodesk +// +// 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. +// +#ifndef PXRUSDMAYA_PROXY_SHAPE_LISTENER_BASE_H +#define PXRUSDMAYA_PROXY_SHAPE_LISTENER_BASE_H + +#include +#include + +#include +#include + +#include +#include +#include + +PXR_NAMESPACE_OPEN_SCOPE + +// clang-format off +#define MAYAUSD_PROXY_SHAPE_LISTENER_BASE_TOKENS \ + ((MayaTypeName, "mayaUsdProxyShapeListenerBase")) +// clang-format on + +TF_DECLARE_PUBLIC_TOKENS( + MayaUsdProxyShapeListenerBaseTokens, + MAYAUSD_CORE_PUBLIC, + MAYAUSD_PROXY_SHAPE_LISTENER_BASE_TOKENS); + +/// \class MayaUsdProxyShapeListenerBase +/// \brief This class allows listening to a Maya USD proxy for USD stage notifications. +/// +/// Goals: +/// +/// 1 - Allow a client to know when a stage is changed +/// 2 - Filter out changes that do not affect the rendering of the stage +/// 3 - C++ clients of this code do not need to link with USD +/// 4 - The class should not affect the stage in any negative way +/// +/// Implementation: +/// +/// We add a new Maya node that can connect to the "outStageCacheId" of the proxy shape and provide +/// update counters that will increment if the view needs to be refreshed or when the data needs to +/// be resynced. Using an external node prevents affecting the USD stage and requires only Maya APIs +/// for C++ clients. +/// +/// The "updateId" counter will increment every time a scene refresh is required due to a value +/// changing (equivalent to a Hydra "change"). The "resyncId" counter will increment every time a +/// scene reparse is needed due to major topological changes (equivalent to a Hydra "resync"). +/// +/// Usage: +/// +/// Clients wanting to listen on a proxyShape for USD changes need to instantiate a +/// "mayaUsdProxyShapeListener" node and connect its "stageCacheId" input to the "outStageCacheId" +/// output of the proxy shape. The new node will start listening on the proxy shape as soon as its +/// "outStageCacheId" gets pulled. So, once the proxy to listener connection is done, you can +/// either: +/// +/// - connect your Maya node to the listener "outStageCacheId" and one of the two counters to get +/// dirtied/evaluated via regular Maya process +/// +/// - Use MNodeMessage.addNodeDirtyPlugCallback() or MNodeMessage.addAttributeChangedCallback() to +/// have your C++ code receive notifications when the stage has changed and needs to be redrawn. +/// When processing the scene for updates, you need to fetch the latest USD cache ID from the +/// listener as this will allow the listener to start listening anew whenever the proxy starts +/// handling a new stage. An implementation of this workflow can be found in +/// test/testUtils/mayaUtils.py, look for TestProxyShapeUpdateHandler and its uses in unit tests. +/// + +class MayaUsdProxyShapeListenerBase : public MPxNode +{ +public: + MAYAUSD_CORE_PUBLIC + ~MayaUsdProxyShapeListenerBase(); + + MAYAUSD_CORE_PUBLIC + static const MTypeId typeId; + MAYAUSD_CORE_PUBLIC + static const MString typeName; + + // Change counter attributes + MAYAUSD_CORE_PUBLIC + static MObject updateCounterAttr; + MAYAUSD_CORE_PUBLIC + static MObject resyncCounterAttr; + + // Input attributes + MAYAUSD_CORE_PUBLIC + static MObject stageCacheIdAttr; + + // Output attributes + MAYAUSD_CORE_PUBLIC + static MObject outStageCacheIdAttr; + + MAYAUSD_CORE_PUBLIC + static void* creator(); + + MAYAUSD_CORE_PUBLIC + static MStatus initialize(); + + MAYAUSD_CORE_PUBLIC + void postConstructor() override; + MAYAUSD_CORE_PUBLIC + MStatus compute(const MPlug& plug, MDataBlock& dataBlock) override; + + MAYAUSD_CORE_PUBLIC + MStatus connectionMade(const MPlug&, const MPlug&, bool asSrc) override; + MAYAUSD_CORE_PUBLIC + MStatus connectionBroken(const MPlug&, const MPlug&, bool asSrc) override; + MAYAUSD_CORE_PUBLIC + MStatus setDependentsDirty(const MPlug& plug, MPlugArray& plugArray) override; + +private: + UsdMayaStageNoticeListener _stageNoticeListener; + void _OnStageContentsChanged(const UsdNotice::StageContentsChanged& notice); + void _OnStageObjectsChanged(const UsdNotice::ObjectsChanged& notice); + + int _lastKnownStageCacheId { -1 }; + + void _IncrementCounter(MObject attribute); + void _ReInit(); +}; + +PXR_NAMESPACE_CLOSE_SCOPE + +#endif diff --git a/lib/mayaUsd/nodes/proxyShapePlugin.cpp b/lib/mayaUsd/nodes/proxyShapePlugin.cpp index 0bcffaed59..c24ee85c04 100644 --- a/lib/mayaUsd/nodes/proxyShapePlugin.cpp +++ b/lib/mayaUsd/nodes/proxyShapePlugin.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -117,6 +118,13 @@ MStatus MayaUsdProxyShapePlugin::initialize(MFnPlugin& plugin) CHECK_MSTATUS(status); #endif + status = plugin.registerNode( + MayaUsdProxyShapeListenerBase::typeName, + MayaUsdProxyShapeListenerBase::typeId, + MayaUsdProxyShapeListenerBase::creator, + MayaUsdProxyShapeListenerBase::initialize); + CHECK_MSTATUS(status); + // Hybrid Hydra / VP2 rendering uses a draw override to draw the proxy // shape. The Pixar and MayaUsd plugins use the UsdMayaProxyDrawOverride, // so register it here. Native USD VP2 rendering uses a sub-scene override. @@ -212,6 +220,9 @@ MStatus MayaUsdProxyShapePlugin::finalize(MFnPlugin& plugin) status = plugin.deregisterNode(UsdMayaPointBasedDeformerNode::typeId); CHECK_MSTATUS(status); + status = plugin.deregisterNode(MayaUsdProxyShapeListenerBase::typeId); + CHECK_MSTATUS(status); + #if defined(WANT_UFE_BUILD) status = plugin.deregisterNode(MayaUsd::LayerManager::typeId); CHECK_MSTATUS(status); diff --git a/lib/mayaUsd/nodes/proxyShapeUpdateManager.cpp b/lib/mayaUsd/nodes/proxyShapeUpdateManager.cpp deleted file mode 100644 index 73cf21ec5c..0000000000 --- a/lib/mayaUsd/nodes/proxyShapeUpdateManager.cpp +++ /dev/null @@ -1,158 +0,0 @@ -// -// Copyright 2023 Autodesk -// -// 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 "proxyShapeUpdateManager.h" - -#include -#include -#include -#include - -#include - -using namespace MAYAUSD_NS_DEF; - -PXR_NAMESPACE_OPEN_SCOPE - -namespace { - -// We have incoming changes that USD will consider either requiring an -// update (meaning the render delegate needs to refresh and redraw) or -// a resync (meaning the scene delegate needs to fetch new datum). We -// want external clients to be aware of these classes of updates in case -// they do not use the Hydra system for refreshing and drawing the scene. -enum class _UsdChangeType -{ - kIgnored, // Change does not require redraw: UI change, metadata change. - kUpdate, // Change requires redraw after refreshing parameter values - kResync // Change requires refreshing cached buffers -}; - -// If the notification is about prepending a UI schema, we don't want a refresh. These structures -// are quite large to inspect, but they hash easily, so let's compare known hashes. -bool _IsUiSchemaPrepend(const VtValue& v) -{ - static std::set UiSchemaPrependHashes; - std::once_flag hasHashes; - std::call_once(hasHashes, [&]() { - SdfTokenListOp op; - op.SetPrependedItems(TfTokenVector { TfToken("NodeGraphNodeAPI") }); - UiSchemaPrependHashes.insert(hash_value(op)); - }); - - if (v.IsHolding()) { - const size_t hash = hash_value(v.UncheckedGet()); - if (UiSchemaPrependHashes.count(hash)) { - return true; - } - } - return false; -} - -// This is a stripped down copy of UsdImagingDelegate::_OnUsdObjectsChanged which is the main USD -// notification handler where paths to refresh and paths to update are compiled for the next Hydra -// refresh. We do not gather paths as there is no simple way to know when to flush these maps. -// -// This needs to stay as quick as possible since it is stuck in the middle of the notification code -// path. -// -// This is a work in progress. Some improvements might be necessary in the future. The following -// potential issues are already visible: -// -// - Changing a parameter value for the first time creates the attribute, which is a kResync -_UsdChangeType _ClassifyUsdObjectsChanged(UsdNotice::ObjectsChanged const& notice) -{ - using PathRange = UsdNotice::ObjectsChanged::PathRange; - - auto range = notice.GetResyncedPaths(); - if (!range.empty()) { - size_t ignoredCount = 0; - size_t resyncCount = 0; - for (auto it = range.begin(); it != range.end(); ++it) { - if (it->IsPropertyPath()) { - // We have a bunch of UI properties to ignore. Especially anything that comes from - // UI schemas. - if (it->GetName().rfind("ui:", 0) == 0) { - ++ignoredCount; - continue; - } - } - for (const SdfChangeList::Entry* entry : it.base()->second) { - for (auto&& infoChanged : entry->infoChanged) { - if (infoChanged.first == UsdTokens->apiSchemas - && _IsUiSchemaPrepend(infoChanged.second.second)) { - ++ignoredCount; - } else { - ++resyncCount; - } - } - } - } - - if (ignoredCount) { - return resyncCount ? _UsdChangeType::kResync : _UsdChangeType::kIgnored; - } else { - return _UsdChangeType::kResync; - } - } - - auto retVal = _UsdChangeType::kIgnored; - - const PathRange pathsToUpdate = notice.GetChangedInfoOnlyPaths(); - for (PathRange::const_iterator it = pathsToUpdate.begin(); it != pathsToUpdate.end(); ++it) { - if (it->IsAbsoluteRootOrPrimPath()) { - const TfTokenVector changedFields = it.GetChangedFields(); - if (!changedFields.empty()) { - retVal = _UsdChangeType::kUpdate; - } - } else if (it->IsPropertyPath()) { - // We have a bunch of UI properties to ignore. Especially anything that comes from UI - // schemas. - if (it->GetName().rfind("ui:", 0) == 0) { - continue; - } - retVal = _UsdChangeType::kUpdate; - for (const auto& entry : it.base()->second) { - if (entry->flags.didChangeAttributeConnection) { - retVal = _UsdChangeType::kResync; - break; - } - } - } - } - - return retVal; -} - -} // namespace - -bool MayaUsdProxyShapeUpdateManager::CanIgnoreObjectsChanged( - const UsdNotice::ObjectsChanged& notice) -{ - switch (_ClassifyUsdObjectsChanged(notice)) { - case _UsdChangeType::kIgnored: return true; - case _UsdChangeType::kResync: ++_UsdStageResyncCounter; - // [[fallthrough]]; // We want that fallthrough to have the update always triggered. - case _UsdChangeType::kUpdate: ++_UsdStageUpdateCounter; break; - } - - return false; -} - -MInt64 MayaUsdProxyShapeUpdateManager::GetUpdateCount() { return _UsdStageUpdateCounter; } - -MInt64 MayaUsdProxyShapeUpdateManager::GetResyncCount() { return _UsdStageResyncCounter; } - -PXR_NAMESPACE_CLOSE_SCOPE diff --git a/lib/mayaUsd/nodes/proxyShapeUpdateManager.h b/lib/mayaUsd/nodes/proxyShapeUpdateManager.h deleted file mode 100644 index 7cb378fe56..0000000000 --- a/lib/mayaUsd/nodes/proxyShapeUpdateManager.h +++ /dev/null @@ -1,47 +0,0 @@ -// -// Copyright 2023 Autodesk -// -// 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. -// -#ifndef PXRUSDMAYA_PROXY_SHAPE_UPDATE_MANAGER_H -#define PXRUSDMAYA_PROXY_SHAPE_UPDATE_MANAGER_H - -#include - -#include -#include - -#include - -PXR_NAMESPACE_OPEN_SCOPE - -class MayaUsdProxyShapeUpdateManager -{ -public: - MAYAUSD_CORE_PUBLIC - bool CanIgnoreObjectsChanged(const UsdNotice::ObjectsChanged& notice); - - MAYAUSD_CORE_PUBLIC - MInt64 GetUpdateCount(); - - MAYAUSD_CORE_PUBLIC - MInt64 GetResyncCount(); - -private: - MInt64 _UsdStageUpdateCounter { 1 }; - MInt64 _UsdStageResyncCounter { 1 }; -}; - -PXR_NAMESPACE_CLOSE_SCOPE - -#endif diff --git a/lib/mayaUsd/ufe/UsdAttribute.cpp b/lib/mayaUsd/ufe/UsdAttribute.cpp index 4c9375b523..238d83266d 100644 --- a/lib/mayaUsd/ufe/UsdAttribute.cpp +++ b/lib/mayaUsd/ufe/UsdAttribute.cpp @@ -263,7 +263,7 @@ class SetUndoableCommand : public MayaUsd::ufe::UsdUndoableCommand::undo(); } - + void redo() override { MayaUsd::ufe::InSetAttribute inSetAttr; diff --git a/plugin/adsk/plugin/CMakeLists.txt b/plugin/adsk/plugin/CMakeLists.txt index 2353ba7d68..fea8ab2895 100644 --- a/plugin/adsk/plugin/CMakeLists.txt +++ b/plugin/adsk/plugin/CMakeLists.txt @@ -32,6 +32,7 @@ target_sources(${TARGET_NAME} importTranslator.cpp exportTranslator.cpp ProxyShape.cpp + ProxyShapeListener.cpp ) if (CMAKE_UFE_V3_FEATURES_AVAILABLE) diff --git a/plugin/adsk/plugin/ProxyShapeListener.cpp b/plugin/adsk/plugin/ProxyShapeListener.cpp new file mode 100644 index 0000000000..e78d031481 --- /dev/null +++ b/plugin/adsk/plugin/ProxyShapeListener.cpp @@ -0,0 +1,52 @@ +// +// Copyright 2023 Autodesk +// +// 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 "ProxyShapeListener.h" + +namespace MAYAUSD_NS_DEF { + +// ======================================================== + +const MTypeId MAYAUSD_PROXYSHAPELISTENER_ID(0x5800009B); + +const MTypeId ProxyShapeListener::typeId(MayaUsd::MAYAUSD_PROXYSHAPELISTENER_ID); +const MString ProxyShapeListener::typeName("mayaUsdProxyShapeListener"); + +/* static */ +void* ProxyShapeListener::creator() { return new ProxyShapeListener(); } + +/* static */ +MStatus ProxyShapeListener::initialize() +{ + MStatus retValue = inheritAttributesFrom(MayaUsdProxyShapeListenerBase::typeName); + CHECK_MSTATUS_AND_RETURN_IT(retValue); + + return retValue; +} + +ProxyShapeListener::ProxyShapeListener() + : MayaUsdProxyShapeListenerBase() +{ +} + +/* virtual */ +ProxyShapeListener::~ProxyShapeListener() +{ + // + // empty + // +} + +} // namespace MAYAUSD_NS_DEF diff --git a/plugin/adsk/plugin/ProxyShapeListener.h b/plugin/adsk/plugin/ProxyShapeListener.h new file mode 100644 index 0000000000..af79068491 --- /dev/null +++ b/plugin/adsk/plugin/ProxyShapeListener.h @@ -0,0 +1,53 @@ +// +// Copyright 2023 Autodesk +// +// 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. +// +#pragma once + +#include "base/api.h" + +#include +#include + +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace MAYAUSD_NS_DEF { + +class ProxyShapeListener : public MayaUsdProxyShapeListenerBase +{ +public: + typedef MayaUsdProxyShapeListenerBase ParentClass; + + MAYAUSD_PLUGIN_PUBLIC + static const MTypeId typeId; + MAYAUSD_PLUGIN_PUBLIC + static const MString typeName; + + MAYAUSD_PLUGIN_PUBLIC + static void* creator(); + + MAYAUSD_PLUGIN_PUBLIC + static MStatus initialize(); + +private: + ProxyShapeListener(); + + ProxyShapeListener(const ProxyShapeListener&); + ~ProxyShapeListener() override; + ProxyShapeListener& operator=(const ProxyShapeListener&); +}; + +} // namespace MAYAUSD_NS_DEF diff --git a/plugin/adsk/plugin/plugin.cpp b/plugin/adsk/plugin/plugin.cpp index 24d70ed803..914836255d 100644 --- a/plugin/adsk/plugin/plugin.cpp +++ b/plugin/adsk/plugin/plugin.cpp @@ -14,6 +14,7 @@ // limitations under the License. // #include "ProxyShape.h" +#include "ProxyShapeListener.h" #include "adskExportCommand.h" #include "adskImportCommand.h" #include "adskListJobContextsCommand.h" @@ -277,6 +278,13 @@ MStatus initializePlugin(MObject obj) MayaUsdProxyShapePlugin::getProxyShapeClassification()); CHECK_MSTATUS(status); + status = plugin.registerNode( + MayaUsd::ProxyShapeListener::typeName, + MayaUsd::ProxyShapeListener::typeId, + MayaUsd::ProxyShapeListener::creator, + MayaUsd::ProxyShapeListener::initialize); + CHECK_MSTATUS(status); + status = plugin.registerNode( MayaUsd::MayaUsdGeomNode::typeName, MayaUsd::MayaUsdGeomNode::typeId, @@ -420,6 +428,9 @@ MStatus uninitializePlugin(MObject obj) deregisterCommandCheck(plugin); #endif + status = plugin.deregisterNode(MayaUsd::ProxyShapeListener::typeId); + CHECK_MSTATUS(status); + status = plugin.deregisterNode(MayaUsd::ProxyShape::typeId); CHECK_MSTATUS(status); diff --git a/test/lib/ufe/testContextOps.py b/test/lib/ufe/testContextOps.py index 7ad4ce17e7..e90b2cb268 100644 --- a/test/lib/ufe/testContextOps.py +++ b/test/lib/ufe/testContextOps.py @@ -34,6 +34,8 @@ from maya import standalone from maya.internal.ufeSupport import ufeCmdWrapper as ufeCmd +import maya.api.OpenMaya as om + import ufe import os @@ -1474,6 +1476,13 @@ def testGeomCoponentAssignment(self): cubeXForm, _ = cmds.polyCube(name='MyCube') psPathStr = mayaUsd_createStageWithNewLayer.createStageWithNewLayer() + sl = om.MSelectionList() + sl.add(psPathStr) + dagPath = sl.getDagPath(0) + dagPath.extendToShape() + + proxyShapeNode = dagPath.node() + mayaUsd.lib.PrimUpdaterManager.duplicate(cmds.ls(cubeXForm, long=True)[0], psPathStr) topPath = ufe.PathString.path(psPathStr + ',/' + cubeXForm + "/" + "top") @@ -1483,24 +1492,8 @@ def testGeomCoponentAssignment(self): self.assertEqual(topSubset.GetFamilyNameAttr().Get(), "componentTag") self.assertFalse(topSubset.GetPrim().HasAPI(UsdShade.MaterialBindingAPI)) - counters= { "resync": cmds.getAttr(psPathStr + '.resyncId'), - "update" : cmds.getAttr(psPathStr + '.upid')} - - def assertIsOnlyUpdate(self, counters, shapePathStr): - resyncCounter = cmds.getAttr(shapePathStr + '.resyncId') - updateCounter = cmds.getAttr(shapePathStr + '.updateId') - self.assertEqual(resyncCounter, counters["resync"]) - self.assertGreater(updateCounter, counters["update"]) - counters["resync"] = resyncCounter - counters["update"] = updateCounter - - def assertIsResync(self, counters, shapePathStr): - resyncCounter = cmds.getAttr(shapePathStr + '.resyncId') - updateCounter = cmds.getAttr(shapePathStr + '.updateId') - self.assertGreater(resyncCounter, counters["resync"]) - self.assertGreater(updateCounter, counters["update"]) - counters["resync"] = resyncCounter - counters["update"] = updateCounter + messageHandler = mayaUtils.TestProxyShapeUpdateHandler(psPathStr) + messageHandler.snapshot() contextOps = ufe.ContextOps.contextOps(topItem) cmd = contextOps.doOpCmd(['Assign New Material', 'USD', 'UsdPreviewSurface']) @@ -1514,41 +1507,65 @@ def assertIsResync(self, counters, shapePathStr): self.assertTrue(topSubset.GetPrim().HasAPI(UsdShade.MaterialBindingAPI)) # We expect a resync after this assignment: - assertIsResync(self, counters, psPathStr) + self.assertTrue(messageHandler.isResync()) # setting a value the first time is a resync due to the creation of the attribute: attrs = ufe.Attributes.attributes(shaderItem) metallicAttr = attrs.attribute("inputs:metallic") ufeCmd.execute(metallicAttr.setCmd(0.5)) - assertIsResync(self, counters, psPathStr) + self.assertTrue(messageHandler.isResync()) # Subsequent changes are updates: ufeCmd.execute(metallicAttr.setCmd(0.7)) - assertIsOnlyUpdate(self, counters, psPathStr) + self.assertTrue(messageHandler.isUpdate()) # First undo is an update: cmds.undo() - assertIsOnlyUpdate(self, counters, psPathStr) + self.assertTrue(messageHandler.isUpdate()) # Second undo is a resync: cmds.undo() - assertIsResync(self, counters, psPathStr) + self.assertTrue(messageHandler.isResync()) # Third undo is also resync: cmds.undo() - assertIsResync(self, counters, psPathStr) + self.assertTrue(messageHandler.isResync()) # First redo is resync: cmds.redo() - assertIsResync(self, counters, psPathStr) + self.assertTrue(messageHandler.isResync()) # Second redo is resync: cmds.redo() - assertIsResync(self, counters, psPathStr) + self.assertTrue(messageHandler.isResync()) # Third redo is update: cmds.redo() - assertIsOnlyUpdate(self, counters, psPathStr) + self.assertTrue(messageHandler.isUpdate()) + currentCacheId = messageHandler.getStageCacheId() + + # Changing the whole stage is a resync: + testFile = testUtils.getTestScene("MaterialX", "MtlxValueTypes.usda") + cmds.setAttr('{}.filePath'.format(psPathStr), testFile, type='string') + + self.assertTrue(messageHandler.isResync()) + + # But that will be the last resync: + testFile = testUtils.getTestScene("MaterialX", "sin_compound.usda") + cmds.setAttr('{}.filePath'.format(psPathStr), testFile, type='string') + + self.assertTrue(messageHandler.isUnchanged()) + + # Until we pull on the node to get the current stage cache id, which resets + # the stage listener to the new stage: + self.assertNotEqual(messageHandler.getStageCacheId(), currentCacheId) + + testFile = testUtils.getTestScene("MaterialX", "MtlxUVStreamTest.usda") + cmds.setAttr('{}.filePath'.format(psPathStr), testFile, type='string') + + self.assertTrue(messageHandler.isResync()) + + messageHandler.terminate() if __name__ == '__main__': diff --git a/test/lib/ufe/testUINodeGraphNode.py b/test/lib/ufe/testUINodeGraphNode.py index 3773b6f47c..82e1d015bb 100644 --- a/test/lib/ufe/testUINodeGraphNode.py +++ b/test/lib/ufe/testUINodeGraphNode.py @@ -18,6 +18,7 @@ import fixturesUtils import mayaUtils +import testUtils from maya import cmds from maya import standalone @@ -27,7 +28,6 @@ import os import unittest - class UINodeGraphNodeTestCase(unittest.TestCase): '''Verify the UINodeGraphNode USD implementation. ''' @@ -53,14 +53,18 @@ def setUp(self): # Open ballset.ma scene in testSamples mayaUtils.openGroupBallsScene() + self.messageHandler = mayaUtils.TestProxyShapeUpdateHandler('|transform1|proxyShape1') + # Clear selection to start off cmds.select(clear=True) + def tearDown(self): + self.messageHandler.terminate() + # Helper to avoid copy-pasting the entire test def doPosAndSizeTests(self, hasFunc, setFunc, getFunc, cmdFunc): - initialUpdateCount = cmds.getAttr('|transform1|proxyShape1.updateId') - initialResyncCount = cmds.getAttr('|transform1|proxyShape1.resyncId') - + self.messageHandler.snapshot() + # Test hasFunc and getFunc self.assertFalse(hasFunc()) pos0 = getFunc() @@ -92,13 +96,12 @@ def doPosAndSizeTests(self, hasFunc, setFunc, getFunc, cmdFunc): self.assertEqual(pos4.y(), pos5.y()) # None of these changes should force a render refresh: - self.assertEqual(initialUpdateCount, cmds.getAttr('|transform1|proxyShape1.updateId')) - self.assertEqual(initialResyncCount, cmds.getAttr('|transform1|proxyShape1.resyncId')) + self.assertTrue(self.messageHandler.isUnchanged()) def testPosition(self): ball3Path = ufe.PathString.path('|transform1|proxyShape1,/Ball_set/Props/Ball_3') ball3SceneItem = ufe.Hierarchy.createItem(ball3Path) - + uiNodeGraphNode = ufe.UINodeGraphNode.uiNodeGraphNode(ball3SceneItem) self.doPosAndSizeTests(uiNodeGraphNode.hasPosition, uiNodeGraphNode.setPosition, uiNodeGraphNode.getPosition, uiNodeGraphNode.setPositionCmd) @@ -113,7 +116,7 @@ def testSize(self): uiNodeGraphNode = ufe.UINodeGraphNode_v4_1.uiNodeGraphNode(ball3SceneItem) else: uiNodeGraphNode = ufe.UINodeGraphNode.uiNodeGraphNode(ball3SceneItem) - + self.doPosAndSizeTests(uiNodeGraphNode.hasSize, uiNodeGraphNode.setSize, uiNodeGraphNode.getSize, uiNodeGraphNode.setSizeCmd) diff --git a/test/testUtils/mayaUtils.py b/test/testUtils/mayaUtils.py index 61bb435202..d5b0b42e70 100644 --- a/test/testUtils/mayaUtils.py +++ b/test/testUtils/mayaUtils.py @@ -315,3 +315,80 @@ def activeModelPanel(): for panel in cmds.getPanel(type="modelPanel"): if cmds.modelEditor(panel, q=1, av=1): return panel + +class TestProxyShapeUpdateHandler: + def __init__(self, proxyShapeName): + sl = om.MSelectionList() + sl.add(proxyShapeName) + dagPath = sl.getDagPath(0) + dagPath.extendToShape() + mod = om.MDGModifier() + listener = mod.createNode("mayaUsdProxyShapeListener") + proxyDepNode = om.MFnDependencyNode(dagPath.node()) + listenerDepNode = om.MFnDependencyNode(listener) + mod.connect(proxyDepNode.findPlug("outStageCacheId", True), + listenerDepNode.findPlug("stageCacheId", True)) + mod.doIt() + listenerDepNode.findPlug("outStageCacheId", True).asInt() + + self.nodeDirtyCbId = om.MNodeMessage.addNodeDirtyPlugCallback(listener, self.nodeDirty, None) + self.updateIdDirty = 0 + self.resyncIdDirty = 0 + self.attributeChangedCbId = om.MNodeMessage.addAttributeChangedCallback(listener, self.attributeChanged, None) + self.updateIdChanged = 0 + self.resyncIdChanged = 0 + self.listenerNode = listener + + def terminate(self): + om.MMessage.removeCallback(self.nodeDirtyCbId) + om.MMessage.removeCallback(self.attributeChangedCbId) + del self.listenerNode + + def snapshot(self): + self.updateIdDirtySaved = self.updateIdDirty + self.resyncIdDirtySaved = self.resyncIdDirty + self.updateIdChangedSaved = self.updateIdChanged + self.resyncIdChangedSaved = self.resyncIdChanged + + def isUnchanged(self): + retVal = (self.updateIdDirtySaved == self.updateIdDirty and + self.updateIdChangedSaved == self.updateIdChanged and + self.resyncIdDirtySaved == self.resyncIdDirty and + self.resyncIdChangedSaved == self.resyncIdChanged) + self.snapshot() + return retVal + + def isUpdate(self): + retVal = (self.updateIdDirtySaved < self.updateIdDirty and + self.updateIdChangedSaved < self.updateIdChanged and + self.resyncIdDirtySaved == self.resyncIdDirty and + self.resyncIdChangedSaved == self.resyncIdChanged) + self.snapshot() + return retVal + + def isResync(self): + retVal = (self.updateIdDirtySaved < self.updateIdDirty and + self.updateIdChangedSaved < self.updateIdChanged and + self.resyncIdDirtySaved < self.resyncIdDirty and + self.resyncIdChangedSaved < self.resyncIdChanged) + self.snapshot() + return retVal + + def getStageCacheId(self): + listenerDepNode = om.MFnDependencyNode(self.listenerNode) + return listenerDepNode.findPlug("outStageCacheId", True).asInt() + + def nodeDirty(self, node, plug, listeners): + name = plug.partialName(False, False, False, False, False, True) + if name == "updateId": + self.updateIdDirty += 1 + elif name == "resyncId": + self.resyncIdDirty += 1 + + def attributeChanged(self, message, plug, otherPlug, clientData): + if message & om.MNodeMessage.kAttributeSet: + name = plug.partialName(False, False, False, False, False, True) + if name == "updateId": + self.updateIdChanged += 1 + elif name == "resyncId": + self.resyncIdChanged += 1