From 5681c15f74e270fdede65c95fdc063f0c4bdde14 Mon Sep 17 00:00:00 2001 From: Pierre Tremblay Date: Mon, 2 Dec 2024 13:01:27 -0500 Subject: [PATCH 1/2] Loading mayaHydra should not set scene as modified. --- lib/mayaHydra/mayaPlugin/plugin.cpp | 41 +++++++++++++ .../mayaUsd/render/mayaToHydra/CMakeLists.txt | 1 + .../mayaToHydra/testPolygonPrimitives.py | 2 + .../render/mayaToHydra/testSceneModified.py | 32 +++++++++++ test/testUtils/fixturesUtils.py | 9 +-- test/testUtils/mayaUtils.py | 57 +------------------ test/testUtils/mtohUtils.py | 18 +++--- 7 files changed, 91 insertions(+), 69 deletions(-) create mode 100644 test/lib/mayaUsd/render/mayaToHydra/testSceneModified.py diff --git a/lib/mayaHydra/mayaPlugin/plugin.cpp b/lib/mayaHydra/mayaPlugin/plugin.cpp index 54019b98ea..9b38d00099 100644 --- a/lib/mayaHydra/mayaPlugin/plugin.cpp +++ b/lib/mayaHydra/mayaPlugin/plugin.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include @@ -78,6 +79,44 @@ namespace { setenv(name.c_str(), value.c_str(), 1); #endif } + +class SceneModifiedGuard +{ +public: + + // Read modified state before + SceneModifiedGuard() : _wasModified(sceneModified()) + {} + + ~SceneModifiedGuard() + { + // If modified flag became true, clear it. + if (sceneModified() && !_wasModified) { + MGlobal::executeCommand("file -modified 0"); + } + } + +private: + + bool _wasModified{false}; + + // Scene modified query. + static bool sceneModified() + { + MCommandResult result; + auto status = MGlobal::executeCommand("file -query -modified", result); + if (status != MStatus::kSuccess) { + throw std::runtime_error("File modified query error in mayaHydra plugin load."); + } + int typedResult{0}; + status = result.getResult(typedResult); + if (status != MStatus::kSuccess) { + throw std::runtime_error("Command result access error in mayaHydra plugin load."); + } + return (typedResult != 0); + } +}; + } void initialize() @@ -132,6 +171,8 @@ void beforePluginUnloadCallback( const MStringArray& strs, void* clientData ) PLUGIN_EXPORT MStatus initializePlugin(MObject obj) { + SceneModifiedGuard guard; + MString experimental("mayaHydra is experimental."); MGlobal::displayWarning(experimental); diff --git a/test/lib/mayaUsd/render/mayaToHydra/CMakeLists.txt b/test/lib/mayaUsd/render/mayaToHydra/CMakeLists.txt index 24e63f9288..a64cd06cd5 100644 --- a/test/lib/mayaUsd/render/mayaToHydra/CMakeLists.txt +++ b/test/lib/mayaUsd/render/mayaToHydra/CMakeLists.txt @@ -56,6 +56,7 @@ set(INTERACTIVE_TEST_SCRIPT_FILES testCustomShadersNode.py testMayaDefaultMaterial.py testMayaLightingModes.py + testSceneModified.py cpp/testColorPreferences.py cpp/testCppFramework.py cpp/testDataProducerExample.py diff --git a/test/lib/mayaUsd/render/mayaToHydra/testPolygonPrimitives.py b/test/lib/mayaUsd/render/mayaToHydra/testPolygonPrimitives.py index b2212db936..235093eb94 100644 --- a/test/lib/mayaUsd/render/mayaToHydra/testPolygonPrimitives.py +++ b/test/lib/mayaUsd/render/mayaToHydra/testPolygonPrimitives.py @@ -26,6 +26,8 @@ class TestPolygonPrimitives(mtohUtils.MayaHydraBaseTestCase): IMAGE_DIFF_FAIL_THRESHOLD = 0.05 IMAGE_DIFF_FAIL_PERCENT = 1.5 + _requiredPlugins = ['modelingToolkit'] + def compareSnapshot(self, referenceFilename, cameraDistance=15, imageVersion=None): self.setBasicCam(cameraDistance) cmds.refresh() diff --git a/test/lib/mayaUsd/render/mayaToHydra/testSceneModified.py b/test/lib/mayaUsd/render/mayaToHydra/testSceneModified.py new file mode 100644 index 0000000000..5b1920b2e2 --- /dev/null +++ b/test/lib/mayaUsd/render/mayaToHydra/testSceneModified.py @@ -0,0 +1,32 @@ +# Copyright 2024 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. +# +import maya.cmds as cmds +import fixturesUtils +import mtohUtils + +class TestSceneModified(mtohUtils.MayaHydraBaseTestCase): + _file = __file__ + + # Base class setUp() defines HdStorm as the renderer. + + def test_sceneModified(self): + # Though it modifies node 'defaultRenderGlobals' (as reported by + # 'ls -modified'), loading the mayaHydra plugin should not cause + # a scene modified warning, which causes spurious dialogs on file + # new or open to save what is essentially an empty file. + self.assertFalse(cmds.file(query=True, modified=True)) + +if __name__ == '__main__': + fixturesUtils.runTests(globals()) diff --git a/test/testUtils/fixturesUtils.py b/test/testUtils/fixturesUtils.py index ab0dd39195..567fd52ba3 100644 --- a/test/testUtils/fixturesUtils.py +++ b/test/testUtils/fixturesUtils.py @@ -19,9 +19,6 @@ import sys import unittest -# Plugins that are bundled and loaded by default in a Maya installation -DEFAULT_PLUGINS = ['ArubaTessellator', 'modelingToolkit'] - def _setUpClass(modulePathName, pluginName, initializeStandalone): ''' Common code for setUpClass() and readOnlySetUpClass() @@ -32,10 +29,10 @@ def _setUpClass(modulePathName, pluginName, initializeStandalone): if pluginName: import maya.cmds as cmds + wasModified = cmds.file(query=True, modified=True) cmds.loadPlugin(pluginName, quiet=True) - - for defaultPlugin in DEFAULT_PLUGINS: - cmds.loadPlugin(defaultPlugin, quiet=True) + isModified = cmds.file(query=True, modified=True) + assert isModified == wasModified, ('Loading plugin %s modified the scene' % pluginName) realPath = os.path.realpath(modulePathName) return os.path.split(realPath) diff --git a/test/testUtils/mayaUtils.py b/test/testUtils/mayaUtils.py index e5ee67781a..231ff501ce 100644 --- a/test/testUtils/mayaUtils.py +++ b/test/testUtils/mayaUtils.py @@ -43,61 +43,6 @@ HD_STORM = "HdStormRendererPlugin" HD_STORM_OVERRIDE = "mayaHydraRenderOverride_" + HD_STORM -def loadPlugin(pluginName): - """ - Load all given plugins created or needed by maya-ufe-plugin - Args: - pluginName (str): The plugin name to load - Returns: - True if all plugins are loaded. False if a plugin failed to load - """ - try: - if not isPluginLoaded(pluginName): - cmds.loadPlugin( pluginName, quiet = True ) - return True - except: - print(sys.exc_info()[1]) - print("Unable to load %s" % pluginName) - return False - -def isPluginLoaded(pluginName): - """ - Verifies that the given plugin is loaded - Args: - pluginName (str): The plugin name to verify - Returns: - True if the plugin is loaded. False if a plugin failed to load - """ - return cmds.pluginInfo( pluginName, loaded=True, query=True) - -def isMayaUsdPluginLoaded(): - """ - Load plugins needed by UFE tests. - Returns: - True if plugins loaded successfully. False if a plugin failed to load - """ - # Load the mayaUsdPlugin first. - if not loadPlugin("mayaUsdPlugin"): - return False - - # Load the UFE support plugin, for ufeSelectCmd support. If this plugin - # isn't included in the distribution of Maya (e.g. Maya 2019 or 2020), use - # fallback test plugin. - if not (loadPlugin("ufeSupport") or loadPlugin("ufeTestCmdsPlugin")): - return False - - # The renderSetup Python plugin registers a file new callback to Maya. On - # test application exit (in TbaseApp::cleanUp()), a file new is done and - # thus the file new callback is invoked. Unfortunately, this occurs after - # the Python interpreter has been finalized, which causes a crash. Since - # renderSetup is not needed for mayaUsd tests, unload it. - rs = 'renderSetup' - if cmds.pluginInfo(rs, q=True, loaded=True): - unloaded = cmds.unloadPlugin(rs) - return (unloaded[0] == rs) - - return True - def createUfePathSegment(mayaPath): """ Create a UFE path from a given maya path and return the first segment. @@ -152,6 +97,8 @@ def openNewScene(useTestSettings=True): cmds.file(new=True, force=True) if useTestSettings: applyTestSettings() + # As this conceptually opens a new scene, set the file to unmodified. + cmds.file(modified=False) def openTestScene(*args, useTestSettings=True): filePath = testUtils.getTestScene(*args) diff --git a/test/testUtils/mtohUtils.py b/test/testUtils/mtohUtils.py index b6631d8ddd..5946f945bc 100644 --- a/test/testUtils/mtohUtils.py +++ b/test/testUtils/mtohUtils.py @@ -35,13 +35,6 @@ HD_STORM_OVERRIDE = "mayaHydraRenderOverride_" + HD_STORM MAYAUSD_PLUGIN_NAME = 'mayaUsdPlugin' -def checkForPlugin(pluginName: str): - try: - cmds.loadPlugin(pluginName) - except: - return False - return True - class MayaHydraBaseTestCase(unittest.TestCase, ImageDiffingTestCase): '''Base class for mayaHydra unit tests.''' @@ -60,7 +53,9 @@ class MayaHydraBaseTestCase(unittest.TestCase, ImageDiffingTestCase): # Unloading mayaHydraFlowViewportAPILocator crashes Maya (HYDRA-1304). # Unloading mtoa succeeds on Linux, but fails on Windows and macOS # with "cannot be unloaded because it is still in use" error. - _pluginsCantUnload = ['mayaHydraFlowViewportAPILocator', 'mtoa'] + # Unloading modelingToolkit fails with a + # "Dynamic unloading is not currently supported." error + _pluginsCantUnload = ['mayaHydraFlowViewportAPILocator', 'mtoa', 'modelingToolkit'] @classmethod def setUpClass(cls): @@ -106,8 +101,15 @@ def setUp(self): # so open a new file before each test to minimize leftovers # from previous tests. mayaUtils.openNewScene() + modified = cmds.file(query=True, modified=True) + assert not modified, 'Internal test framework error: scene left as modified by mayaUtils.openNewScene()' + self.setHdStormRenderer() + # We've just opened a new scene, so we should not be modified. Setting + # Storm should conceptually not change that status. + cmds.file(modified=False) + @classmethod def tearDownClass(cls): # Clean out the scene to allow all plugins to unload cleanly. From 7e7268b1c4d964b4decc1b422d84dc02f6129928 Mon Sep 17 00:00:00 2001 From: Pierre Tremblay Date: Tue, 3 Dec 2024 15:02:43 -0500 Subject: [PATCH 2/2] Addressed code review feedback. --- test/testUtils/mtohUtils.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/testUtils/mtohUtils.py b/test/testUtils/mtohUtils.py index 5946f945bc..002c1e099d 100644 --- a/test/testUtils/mtohUtils.py +++ b/test/testUtils/mtohUtils.py @@ -107,7 +107,9 @@ def setUp(self): self.setHdStormRenderer() # We've just opened a new scene, so we should not be modified. Setting - # Storm should conceptually not change that status. + # Storm as the renderer should conceptually not change that status, but + # unfortunately in automated tests it does (see setHdStormRender() + # method documentation). Restore modified status to false. cmds.file(modified=False) @classmethod @@ -133,6 +135,10 @@ def setHdStormRenderer(self): cmds.modelEditor( self.activeEditor, e=1, rendererOverrideName=HD_STORM_OVERRIDE) + # During automated tests, tracing demonstrates that the following call + # to refresh marks the scene as modified, with the modified node being + # defaultRenderGlobals. This behavior cannot be reproduced in a + # non-automated interactive Maya. cmds.refresh(f=1) self.delegateId = cmds.mayaHydra(renderer=HD_STORM, sceneDelegateId="MayaHydraSceneDelegate")