Skip to content

Commit 59b610e

Browse files
committed
Fix dirtying attributes on non-meshes
1 parent 7eefd4b commit 59b610e

File tree

8 files changed

+220
-1
lines changed

8 files changed

+220
-1
lines changed

lib/mayaHydra/hydraExtensions/adapters/cameraAdapter.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
#include <pxr/base/gf/interval.h>
2424
#include <pxr/imaging/hd/camera.h>
25+
#include <pxr/imaging/hd/changeTracker.h>
2526

2627
#include <maya/MDagMessage.h>
2728
#include <maya/MFnCamera.h>
@@ -79,7 +80,10 @@ void MayaHydraCameraAdapter::Populate()
7980
void MayaHydraCameraAdapter::MarkDirty(HdDirtyBits dirtyBits)
8081
{
8182
if (_isPopulated && dirtyBits != 0) {
82-
dirtyBits = dirtyBits & HdCamera::AllDirty;
83+
// We support extension-attribute primvars on cameras, so keep DirtyPrimvar even though
84+
// cameras are sprims and normally only expose HdCamera dirty bits.
85+
const HdDirtyBits primvarBits = dirtyBits & HdChangeTracker::DirtyPrimvar;
86+
dirtyBits = (dirtyBits & HdCamera::AllDirty) | primvarBits;
8387
GetMayaHydraSceneIndex()->MarkSprimDirty(GetID(), dirtyBits);
8488
}
8589
}

lib/mayaHydra/hydraExtensions/adapters/lightAdapter.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include <pxr/base/tf/diagnostic.h>
2626
#include <pxr/base/tf/type.h>
2727
#include <pxr/imaging/hd/light.h>
28+
#include <pxr/imaging/hd/changeTracker.h>
2829
#include <pxr/imaging/hdx/simpleLightTask.h>
2930
#include <pxr/usd/usdLux/tokens.h>
3031

@@ -138,6 +139,10 @@ void MayaHydraLightAdapter::Populate()
138139
void MayaHydraLightAdapter::MarkDirty(HdDirtyBits dirtyBits)
139140
{
140141
if (_isPopulated && dirtyBits != 0) {
142+
// We support extension-attribute primvars on lights, so keep DirtyPrimvar even though
143+
// lights are sprims and normally only expose HdLight dirty bits.
144+
const HdDirtyBits primvarBits = dirtyBits & HdChangeTracker::DirtyPrimvar;
145+
dirtyBits = (dirtyBits & HdLight::AllDirty) | primvarBits;
141146
GetMayaHydraSceneIndex()->MarkSprimDirty(GetID(), dirtyBits);
142147
}
143148
}

lib/mayaHydra/hydraExtensions/adapters/materialAdapter.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include <pxr/base/tf/token.h>
2828
#include <pxr/base/tf/type.h>
2929
#include <pxr/imaging/hd/material.h>
30+
#include <pxr/imaging/hd/changeTracker.h>
3031
#include <pxr/pxr.h>
3132
#include <pxr/usd/sdf/types.h>
3233
#include <pxr/usd/usd/prim.h>
@@ -85,6 +86,10 @@ bool MayaHydraMaterialAdapter::HasType(const TfToken& typeId) const
8586

8687
void MayaHydraMaterialAdapter::MarkDirty(HdDirtyBits dirtyBits)
8788
{
89+
// We support extension-attribute primvars on materials, so keep DirtyPrimvar even though
90+
// materials are sprims and normally only expose HdMaterial dirty bits.
91+
const HdDirtyBits primvarBits = dirtyBits & HdChangeTracker::DirtyPrimvar;
92+
dirtyBits = (dirtyBits & HdMaterial::AllDirty) | primvarBits;
8893
GetMayaHydraSceneIndex()->MarkSprimDirty(GetID(), dirtyBits);
8994
}
9095

lib/mayaHydra/hydraExtensions/sceneIndex/mayaHydraSceneIndex.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include <mayaHydraLib/sceneIndex/mayaHydraDataSource.h>
2828

2929
#include <pxr/base/tf/envSetting.h>
30+
#include <pxr/imaging/hd/primvarsSchema.h>
3031
#include <pxr/imaging/hd/retainedDataSource.h>
3132
#include <pxr/imaging/hd/rprim.h>
3233
#include <pxr/usdImaging/usdImaging/tokens.h>
@@ -903,6 +904,9 @@ void MayaHydraSceneIndex::_MarkPrimDirty(
903904
HdSceneIndexPrim prim = GetPrim(id);
904905
HdDataSourceLocatorSet locators;
905906
dirtyBitsToLocatorsFunc(prim.primType, dirtyBits, &locators);
907+
if (dirtyBits & HdChangeTracker::DirtyPrimvar) {
908+
locators.append(HdPrimvarsSchema::GetDefaultLocator());
909+
}
906910
if (!locators.IsEmpty()) {
907911
DirtyPrims({ { id, locators } });
908912
}

test/lib/mayaUsd/render/mayaToHydra/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ set(INTERACTIVE_TEST_SCRIPT_FILES
100100
cpp/testGeomSubsetsPicking.py
101101
cpp/testSinglePicking.py
102102
cpp/testSceneIndexDirtying.py
103+
cpp/testCustomAttributeDirtying.py
103104
cpp/testUsdStageInvalidate.py
104105
cpp/testCamera.py
105106
cpp/testCustomAttributes.py|depOnPlugins:mtoa

test/lib/mayaUsd/render/mayaToHydra/cpp/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ target_sources(${TARGET_NAME}
3333
testFlowViewportAPIAddPrims.cpp
3434
testUsdStageLayerMuting.cpp
3535
testMeshAdapterTransform.cpp
36+
testCustomAttributeDirtying.cpp
3637
testFlowViewportAPIFilterPrims.cpp
3738
testSceneCorrectness.cpp
3839
testSelection.cpp
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright 2026 Autodesk
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
16+
#include "testUtils.h"
17+
18+
#include <pxr/imaging/hd/primvarsSchema.h>
19+
#include <pxr/imaging/hd/tokens.h>
20+
21+
#include <maya/MGlobal.h>
22+
23+
#include <gtest/gtest.h>
24+
25+
#include <algorithm>
26+
#include <cmath>
27+
#include <string>
28+
29+
PXR_NAMESPACE_USING_DIRECTIVE
30+
31+
namespace {
32+
33+
const char* kAttrName = "extDirty";
34+
const char* kMeshShapeName = "dirtyMeshShape";
35+
const char* kCameraShapeName = "dirtyCameraShape";
36+
const char* kLightShapeName = "dirtyLightShape";
37+
const char* kMaterialSgName = "dirtyMaterialSG";
38+
const char* kMeshShapeOptionVar = "mhDirtyMeshShape";
39+
const char* kCameraShapeOptionVar = "mhDirtyCameraShape";
40+
const char* kLightShapeOptionVar = "mhDirtyLightShape";
41+
const char* kMaterialSgOptionVar = "mhDirtyMaterialSg";
42+
43+
std::string GetOptionVarOrDefault(const char* optionVar, const char* fallback)
44+
{
45+
if (MGlobal::optionVarExists(optionVar)) {
46+
return MGlobal::optionVarStringValue(optionVar).asChar();
47+
}
48+
return fallback;
49+
}
50+
51+
bool AttrExists(const std::string& nodeName)
52+
{
53+
int exists = 0;
54+
const std::string cmd = std::string("attributeQuery -exists -node \"")
55+
+ nodeName + "\" " + kAttrName;
56+
MGlobal::executeCommand(cmd.c_str(), exists);
57+
return exists != 0;
58+
}
59+
60+
void SetAttrAndRefresh(const std::string& nodeName, double value)
61+
{
62+
const std::string cmd = std::string("setAttr \"")
63+
+ nodeName + "." + kAttrName + "\" " + std::to_string(value);
64+
MGlobal::executeCommand(cmd.c_str());
65+
MGlobal::executeCommand("refresh");
66+
}
67+
68+
bool FindDirtyPrimWithPrimvarValueSince(
69+
SceneIndexNotificationsAccumulator& accumulator,
70+
size_t startIndex,
71+
double expectedValue)
72+
{
73+
const auto& entries = accumulator.GetDirtiedPrimEntries();
74+
auto sceneIndex = accumulator.GetObservedSceneIndex();
75+
const TfToken primvarName(kAttrName);
76+
77+
for (size_t i = startIndex; i < entries.size(); ++i) {
78+
if (!entries[i].dirtyLocators.Contains(HdPrimvarsSchema::GetDefaultLocator())) {
79+
continue;
80+
}
81+
82+
HdSceneIndexPrim prim = sceneIndex->GetPrim(entries[i].primPath);
83+
if (!prim.dataSource) {
84+
continue;
85+
}
86+
87+
HdSampledDataSourceHandle valueDs
88+
= HdPrimvarsSchema::GetFromParent(prim.dataSource)
89+
.GetPrimvar(primvarName)
90+
.GetPrimvarValue();
91+
if (!valueDs) {
92+
continue;
93+
}
94+
95+
VtValue value = valueDs->GetValue(0.0f);
96+
if (!value.IsHolding<double>()) {
97+
continue;
98+
}
99+
100+
const double actual = value.UncheckedGet<double>();
101+
if (std::abs(actual - expectedValue) <= 1e-6) {
102+
return true;
103+
}
104+
}
105+
return false;
106+
}
107+
108+
} // namespace
109+
110+
// Unit test: ensure extension-attribute primvars dirty for mesh, camera, light, material.
111+
TEST(CustomAttributeDirtying, testDirtyPrimvars)
112+
{
113+
const SceneIndicesVector& sceneIndices = GetTerminalSceneIndices();
114+
ASSERT_GT(sceneIndices.size(), 0u);
115+
SceneIndexNotificationsAccumulator notifsAccumulator(sceneIndices.front());
116+
117+
auto checkDirty = [&](const std::string& nodeName, double value) {
118+
const size_t startIndex = notifsAccumulator.GetDirtiedPrimEntries().size();
119+
SetAttrAndRefresh(nodeName, value);
120+
EXPECT_TRUE(FindDirtyPrimWithPrimvarValueSince(notifsAccumulator, startIndex, value));
121+
};
122+
123+
checkDirty(GetOptionVarOrDefault(kMeshShapeOptionVar, kMeshShapeName), 1.0);
124+
checkDirty(GetOptionVarOrDefault(kCameraShapeOptionVar, kCameraShapeName), 2.0);
125+
checkDirty(GetOptionVarOrDefault(kLightShapeOptionVar, kLightShapeName), 3.0);
126+
checkDirty(GetOptionVarOrDefault(kMaterialSgOptionVar, kMaterialSgName), 4.0);
127+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Copyright 2026 Autodesk
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
import maya.cmds as cmds
16+
import fixturesUtils
17+
import mtohUtils
18+
from testUtils import PluginLoaded
19+
20+
21+
class TestCustomAttributeDirtying(mtohUtils.MayaHydraBaseTestCase):
22+
# MayaHydraBaseTestCase.setUpClass requirement.
23+
_file = __file__
24+
25+
def setupScene(self):
26+
cmds.file(new=True, force=True)
27+
mesh_transform, mesh_shape = cmds.polyCube(name="dirtyMesh")
28+
cam_transform, cam_shape = cmds.camera(name="dirtyCamera")
29+
light_shape = cmds.pointLight(name="dirtyLight")
30+
31+
material = cmds.shadingNode("lambert", asShader=True, name="dirtyMaterial")
32+
material_sg = cmds.sets(
33+
renderable=True,
34+
noSurfaceShader=True,
35+
empty=True,
36+
name="dirtyMaterialSG")
37+
cmds.connectAttr(material + ".outColor", material_sg + ".surfaceShader", force=True)
38+
cmds.sets(mesh_transform, edit=True, forceElement=material_sg)
39+
40+
mesh_transform = cmds.ls(mesh_transform, long=True)[0]
41+
mesh_shape = cmds.listRelatives(mesh_transform, shapes=True, fullPath=True)[0]
42+
cam_shape = cmds.listRelatives(cam_transform, shapes=True, fullPath=True)[0]
43+
light_shape = cmds.ls(light_shape, long=True)[0]
44+
material_sg = cmds.ls(material_sg, long=True)[0]
45+
46+
def _ensure_ext_dirty(node):
47+
if not cmds.attributeQuery("extDirty", node=node, exists=True):
48+
cmds.addAttr(node, longName="extDirty", attributeType="double", defaultValue=0.0)
49+
try:
50+
cmds.setAttr(node + ".extDirty", edit=True, keyable=True)
51+
except RuntimeError:
52+
cmds.setAttr(node + ".extDirty", edit=True, channelBox=True)
53+
54+
for node in (mesh_shape, cam_shape, light_shape, material_sg):
55+
_ensure_ext_dirty(node)
56+
57+
self.setHdStormRenderer()
58+
cmds.optionVar(stringValue=("mhDirtyMeshShape", mesh_shape))
59+
cmds.optionVar(stringValue=("mhDirtyCameraShape", cam_shape))
60+
cmds.optionVar(stringValue=("mhDirtyLightShape", light_shape))
61+
cmds.optionVar(stringValue=("mhDirtyMaterialSg", material_sg))
62+
63+
cmds.refresh()
64+
65+
def test_customAttributeDirtying(self):
66+
self.setupScene()
67+
with PluginLoaded('mayaHydraCppTests'):
68+
cmds.mayaHydraCppTest(f="CustomAttributeDirtying.testDirtyPrimvars")
69+
70+
71+
if __name__ == '__main__':
72+
fixturesUtils.runTests(globals())

0 commit comments

Comments
 (0)