The change handles changes that were the result of the asset localization that relate...
authormatthewcpp <matthewcpp@users.noreply.github.com>
Sat, 3 Feb 2024 03:54:29 +0000 (19:54 -0800)
committerpixar-oss <pixar-oss@users.noreply.github.com>
Sat, 3 Feb 2024 04:09:39 +0000 (20:09 -0800)
A cache has been added to asset localization delegates in order to prevent the user processing function from being triggered multiple times for the same layer / asset path combination.

UsdUtilsComputeAllDependencies has been modified to ensure that the same resolved apths do no appear multiple times in the same output array.

(Internal change: 2313564)

13 files changed:
pxr/usd/usdUtils/assetLocalization.cpp
pxr/usd/usdUtils/assetLocalizationDelegate.cpp
pxr/usd/usdUtils/assetLocalizationDelegate.h
pxr/usd/usdUtils/dependencies.cpp
pxr/usd/usdUtils/testenv/testUsdUtilsDependencies.py
pxr/usd/usdUtils/testenv/testUsdUtilsDependencies/computeAllDependenciesMultipleReferences/asset.txt [new file with mode: 0644]
pxr/usd/usdUtils/testenv/testUsdUtilsDependencies/computeAllDependenciesMultipleReferences/common.usda [new file with mode: 0644]
pxr/usd/usdUtils/testenv/testUsdUtilsDependencies/computeAllDependenciesMultipleReferences/reference.usda [new file with mode: 0644]
pxr/usd/usdUtils/testenv/testUsdUtilsDependencies/computeAllDependenciesMultipleReferences/root.usda [new file with mode: 0644]
pxr/usd/usdUtils/testenv/testUsdUtilsUserProcessingFunc.py
pxr/usd/usdUtils/testenv/testUsdUtilsUserProcessingFunc/duplicatePaths/default.usda [new file with mode: 0644]
pxr/usd/usdUtils/testenv/testUsdUtilsUserProcessingFunc/duplicatePaths/modified.usda [new file with mode: 0644]
pxr/usd/usdUtils/testenv/testUsdUtilsUserProcessingFunc/duplicatePaths/root.usda [new file with mode: 0644]

index 82811d287f1ef713139637633798374a740543eb..6b7ab14baebbbd4e99ef83caaa7512f021fab481 100644 (file)
@@ -600,16 +600,15 @@ struct UsdUtils_ExtractExternalReferencesClient {
     UsdUtilsDependencyInfo 
     Process (
         const SdfLayerRefPtr &, 
-        const std::string &assetPath,
-        const std::vector<std::string> &dependencies,
+        const UsdUtilsDependencyInfo &depInfo,
         UsdUtils_DependencyType dependencyType
     )
     {
-        if (dependencies.empty()) {
-            PlaceAsset(assetPath, dependencyType);
+        if (depInfo.GetDependencies().empty()) {
+            PlaceAsset(depInfo.GetAssetPath(), dependencyType);
         }
         else {
-            for (const auto& dependency : dependencies) {
+            for (const auto& dependency : depInfo.GetDependencies()) {
                 PlaceAsset(dependency, dependencyType);
             }
         }
@@ -665,7 +664,7 @@ void UsdUtils_ExtractExternalReferences(
     UsdUtils_ReadOnlyLocalizationDelegate delegate(
         std::bind(&UsdUtils_ExtractExternalReferencesClient::Process, &client,
         std::placeholders::_1, std::placeholders::_2, 
-        std::placeholders::_3, std::placeholders::_4));
+        std::placeholders::_3));
     UsdUtils_LocalizationContext context(&delegate);
     context.SetRefTypesToInclude(refTypesToInclude);
     context.SetRecurseLayerDependencies(false);
index 163b41e8bfe83c081440157cf92516b03238b418..48f23ea27763cb5e43fea87885d834feafe4009e 100644 (file)
@@ -45,6 +45,28 @@ _AllDependenciesForInfo(
     return dependencies;
 }
 
+UsdUtilsDependencyInfo 
+UsdUtils_ProcessedPathCache::GetProcessedInfo(
+    const SdfLayerRefPtr &layer, 
+    const UsdUtilsDependencyInfo &dependencyInfo,
+    UsdUtils_DependencyType dependencyType)
+{
+    auto depKey = 
+        std::make_tuple(layer->GetRealPath(), dependencyInfo.GetAssetPath());
+    auto result = _cachedPaths.find(depKey);
+    if (result == _cachedPaths.end()) {
+        UsdUtilsDependencyInfo depInfo = _processingFunc(
+            layer, dependencyInfo, dependencyType);
+
+        _cachedPaths.insert(std::make_pair(depKey, depInfo.GetAssetPath()));
+
+        return depInfo;
+    }
+    else {
+        return UsdUtilsDependencyInfo(result->second);
+    }
+}
+
 // Processes sublayer paths, removing duplicates and only updates the paths in
 // the writable layer if the processed list differs from the source list.
 std::vector<std::string> 
@@ -56,7 +78,7 @@ UsdUtils_WritableLocalizationDelegate::ProcessSublayers(
 
     for (const std::string& sublayerPath : sublayerPaths) {
         UsdUtilsDependencyInfo depInfo(sublayerPath);
-        UsdUtilsDependencyInfo info = _processingFunc
+        UsdUtilsDependencyInfo info = _pathCache.GetProcessedInfo
             layer, depInfo, UsdUtils_DependencyType::Sublayer);
 
         if (info.GetAssetPath().empty()) {
@@ -163,7 +185,7 @@ UsdUtils_WritableLocalizationDelegate::_ProcessRefOrPayload(
     }
 
     UsdUtilsDependencyInfo depInfo(refOrPayload.GetAssetPath());
-    const UsdUtilsDependencyInfo info = _processingFunc
+    const UsdUtilsDependencyInfo info = _pathCache.GetProcessedInfo
         layer, depInfo, DEP_TYPE);
 
     if (info.GetAssetPath().empty()) {
@@ -204,7 +226,7 @@ UsdUtils_WritableLocalizationDelegate::ProcessValuePath(
     const std::vector<std::string> &dependencies)
 {
     UsdUtilsDependencyInfo depInfo = {authoredPath, dependencies};
-    UsdUtilsDependencyInfo info = _processingFunc(
+    UsdUtilsDependencyInfo info = _pathCache.GetProcessedInfo(
         layer, depInfo, UsdUtils_DependencyType::Reference);
 
     const std::string relativeKeyPath = _GetRelativeKeyPath(keyPath);
@@ -231,7 +253,7 @@ UsdUtils_WritableLocalizationDelegate::ProcessValuePathArrayElement(
     const std::vector<std::string> &dependencies)
 {
     UsdUtilsDependencyInfo depInfo = {authoredPath, dependencies};
-    const UsdUtilsDependencyInfo info = _processingFunc(
+    const UsdUtilsDependencyInfo info = _pathCache.GetProcessedInfo(
         layer, depInfo, UsdUtils_DependencyType::Reference);
     
     if (!info.GetAssetPath().empty()) {
@@ -355,8 +377,8 @@ UsdUtils_WritableLocalizationDelegate::ProcessClipTemplateAssetPath(
     std::vector<std::string> dependencies)
 {
     UsdUtilsDependencyInfo depInfo = {templateAssetPath, dependencies};
-    const UsdUtilsDependencyInfo info = _processingFunc(layer, depInfo,
-        UsdUtils_DependencyType::Reference);
+    const UsdUtilsDependencyInfo info = _pathCache.GetProcessedInfo(layer,
+        depInfo, UsdUtils_DependencyType::Reference);
 
     if (info.GetAssetPath() == templateAssetPath) {
         return _AllDependenciesForInfo(info);
@@ -448,8 +470,8 @@ UsdUtils_ReadOnlyLocalizationDelegate::ProcessSublayers(
     std::vector<std::string> dependencies;
 
     for (const auto &path : layer->GetSubLayerPaths()) {
-        UsdUtilsDependencyInfo info = _processingFunc(
-            layer, path, {}, UsdUtils_DependencyType::Sublayer);
+        UsdUtilsDependencyInfo info = _pathCache.GetProcessedInfo(
+            layer, {path, {}}, UsdUtils_DependencyType::Sublayer);
 
         if (info.GetAssetPath().empty()) {
             continue;
@@ -500,8 +522,8 @@ UsdUtils_ReadOnlyLocalizationDelegate::ProcessReferencesOrPayloads(
             continue;
         }
 
-        UsdUtilsDependencyInfo info = _processingFunc(layer, 
-            refOrPayload.GetAssetPath(), {}, dependencyType);
+        UsdUtilsDependencyInfo info = _pathCache.GetProcessedInfo(layer, 
+            {refOrPayload.GetAssetPath(), {}}, dependencyType);
 
         if (info.GetAssetPath().empty()) {
             continue;
@@ -522,10 +544,8 @@ UsdUtils_ReadOnlyLocalizationDelegate::ProcessValuePath(
     const std::string &authoredPath,
     const std::vector<std::string> &dependencies)
 {
-    return _AllDependenciesForInfo(_processingFunc(layer, authoredPath, 
-        dependencies, UsdUtils_DependencyType::Reference));
-
-    return {};
+    return _AllDependenciesForInfo(_pathCache.GetProcessedInfo(layer, 
+        {authoredPath, dependencies}, UsdUtils_DependencyType::Reference));
 }
 
 std::vector<std::string>
@@ -535,8 +555,8 @@ UsdUtils_ReadOnlyLocalizationDelegate::ProcessValuePathArrayElement(
     const std::string &authoredPath,
     const std::vector<std::string> &dependencies)
 {    
-    return _AllDependenciesForInfo(_processingFunc(layer, authoredPath
-        dependencies, UsdUtils_DependencyType::Reference));
+    return _AllDependenciesForInfo(_pathCache.GetProcessedInfo(layer
+        {authoredPath, dependencies}, UsdUtils_DependencyType::Reference));
 }
 
 std::vector<std::string>
@@ -547,8 +567,8 @@ UsdUtils_ReadOnlyLocalizationDelegate::ProcessClipTemplateAssetPath(
     const std::string &templateAssetPath,
     std::vector<std::string> dependencies)
 {
-    return _AllDependenciesForInfo(_processingFunc(
-        layer, templateAssetPath, dependencies
+    return _AllDependenciesForInfo(_pathCache.GetProcessedInfo(
+        layer, {templateAssetPath, dependencies}
         UsdUtils_DependencyType::ClipTemplateAssetPath));
 }
 
index fc6cdb78ccb4bf4b4897db21dad5ce23cff42ff9..75e0069504006225dc019980859b188da6bbaa1a 100644 (file)
@@ -32,6 +32,8 @@
 #include "pxr/usd/usdUtils/dependencies.h"
 #include "pxr/usd/usdUtils/userProcessingFunc.h"
 
+#include "pxr/base/tf/hash.h"
+
 #include <vector>
 #include <string>
 #include <unordered_map>
@@ -54,6 +56,11 @@ enum class UsdUtils_DependencyType {
 // context.
 struct UsdUtils_LocalizationDelegate
 {
+    using ProcessingFunc = std::function<UsdUtilsDependencyInfo(
+        const SdfLayerRefPtr &layer, 
+        const UsdUtilsDependencyInfo &dependencyInfo,
+        UsdUtils_DependencyType dependencyType)>;
+
     virtual std::vector<std::string> ProcessSublayers(
         const SdfLayerRefPtr &layer) { return {}; }
 
@@ -110,6 +117,31 @@ struct UsdUtils_LocalizationDelegate
         std::vector<std::string> dependencies) { return {}; }
 };
 
+class UsdUtils_ProcessedPathCache {
+public:
+    UsdUtils_ProcessedPathCache(
+        const UsdUtils_LocalizationDelegate::ProcessingFunc &processingFun)
+        : _processingFunc(processingFun) {}
+
+    UsdUtilsDependencyInfo GetProcessedInfo(
+        const SdfLayerRefPtr &layer, 
+        const UsdUtilsDependencyInfo &dependencyInfo,
+        UsdUtils_DependencyType dependencyType);
+
+private:
+    using PathKey = std::tuple<std::string, std::string>;
+
+    struct ProcessedPathHash {
+        size_t operator()(const PathKey& key) const
+        {
+            return TfHash::Combine(std::get<0>(key), std::get<1>(key));
+        }
+    };
+
+    std::unordered_map<PathKey, std::string, ProcessedPathHash> _cachedPaths;
+    UsdUtils_LocalizationDelegate::ProcessingFunc _processingFunc;
+};
+
 // A Delegate which allows for modification and optional removal of
 // asset path values.  This delegate invokes a user supplied processing function
 // on every asset path it encounters.  It will update the path with the returned
@@ -118,14 +150,9 @@ class UsdUtils_WritableLocalizationDelegate
     : public UsdUtils_LocalizationDelegate
 {
 public:
-    using ProcessingFunc = std::function<UsdUtilsDependencyInfo(
-        const SdfLayerRefPtr &layer, 
-        const UsdUtilsDependencyInfo &dependencyInfo,
-        UsdUtils_DependencyType dependencyType)>;
-
     UsdUtils_WritableLocalizationDelegate(
         ProcessingFunc processingFunc)
-        :_processingFunc(processingFunc)
+        : _pathCache(processingFunc)
     {}
 
     virtual std::vector<std::string> ProcessSublayers(
@@ -218,8 +245,7 @@ private:
 
     static std::string _GetRelativeKeyPath(const std::string& fullPath);
 
-    // the user supplied processing function that will be invoked on every path.
-    ProcessingFunc _processingFunc;
+    UsdUtils_ProcessedPathCache _pathCache;
 
     SdfAssetPath _currentValuePath;
     VtArray<SdfAssetPath> _currentValuePathArray;
@@ -244,14 +270,8 @@ class UsdUtils_ReadOnlyLocalizationDelegate
 : public UsdUtils_LocalizationDelegate
 {
 public:
-    using ProcessingFunc = std::function<UsdUtilsDependencyInfo(
-            const SdfLayerRefPtr &layer, 
-            const std::string &assetPath,
-            const std::vector<std::string> &dependencies,
-            UsdUtils_DependencyType dependencyType)>;
-
     UsdUtils_ReadOnlyLocalizationDelegate(ProcessingFunc processingFunc)
-        : _processingFunc(processingFunc) {}
+        : _pathCache(processingFunc){}
 
     virtual std::vector<std::string> ProcessSublayers(
         const SdfLayerRefPtr &layer) override;
@@ -289,7 +309,7 @@ private:
         const SdfLayerRefPtr &layer,
         const std::vector<RefOrPayloadType>& appliedItems);
 
-    ProcessingFunc _processingFunc;
+    UsdUtils_ProcessedPathCache _pathCache;
 };
 
 PXR_NAMESPACE_CLOSE_SCOPE
index 23ae7609355501a0d716358b4b3093ea0d764c24..da9ffd8ef90154276c3463dcaab90d0ca45e8630 100644 (file)
@@ -37,6 +37,7 @@
 #include "pxr/base/trace/trace.h"
 
 #include <functional>
+#include <unordered_set>
 #include <vector>
 
 PXR_NAMESPACE_OPEN_SCOPE
@@ -66,13 +67,11 @@ struct UsdUtils_ComputeAllDependenciesClient
     UsdUtilsDependencyInfo 
     Process( 
         const SdfLayerRefPtr &layer, 
-        const std::string & assetPath,
-        const std::vector<std::string> &dependencies,
+        const UsdUtilsDependencyInfo &depInfo,
         UsdUtils_DependencyType dependencyType)
     {
         
         if (processingFunc) {
-            UsdUtilsDependencyInfo depInfo = {assetPath, dependencies};
             UsdUtilsDependencyInfo processedInfo = 
                 processingFunc(layer, depInfo);
             
@@ -84,7 +83,7 @@ struct UsdUtils_ComputeAllDependenciesClient
             // such as clips or udim, if the user does not modify the
             // asset path, we do not want to place it in the resulting arrays
             // We always want to add dependencies, however
-            bool originalPathIsTemplate = !dependencies.empty();
+            bool originalPathIsTemplate = !depInfo.GetDependencies().empty();
             if (processedInfo != depInfo || !originalPathIsTemplate) {
                 PlaceAsset(layer, processedInfo.GetAssetPath(), dependencyType);
             }
@@ -96,11 +95,11 @@ struct UsdUtils_ComputeAllDependenciesClient
             return processedInfo;
         }
 
-        if (dependencies.empty()) {
-            PlaceAsset(layer, assetPath, dependencyType);
+        if (depInfo.GetDependencies().empty()) {
+            PlaceAsset(layer, depInfo.GetAssetPath(), dependencyType);
         }
         else {
-            for (const auto & dependency : dependencies) {
+            for (const auto & dependency : depInfo.GetDependencies()) {
                 PlaceAsset(layer, dependency, dependencyType);
             }
         }
@@ -139,19 +138,19 @@ struct UsdUtils_ComputeAllDependenciesClient
 
         if (resolvedPath.empty()) {
             if (PathShouldResolve(layer, resolvedPath, dependencyType)) {
-                unresolvedPaths.emplace_back(anchoredPath);
+                unresolvedPaths.insert(anchoredPath);
             }
         }
         else if (UsdStage::IsSupportedFile(anchoredPath)) {
-            layers.push_back(SdfLayer::FindOrOpen(anchoredPath));
+            layers.insert(SdfLayer::FindOrOpen(anchoredPath));
         }
         else {
-            assets.push_back(resolvedPath);
+            assets.insert(resolvedPath);
         }
     }
 
-    std::vector<SdfLayerRefPtr> layers;
-    std::vector<std::string> assets, unresolvedPaths;
+    std::unordered_set<SdfLayerRefPtr, TfHash> layers;
+    std::unordered_set<std::string> assets, unresolvedPaths;
     std::function<UsdUtilsProcessingFunc> processingFunc;
 };
 
@@ -173,24 +172,30 @@ UsdUtilsComputeAllDependencies(
     UsdUtils_ReadOnlyLocalizationDelegate delegate(
         std::bind(&UsdUtils_ComputeAllDependenciesClient::Process, &client,
             std::placeholders::_1, std::placeholders::_2, 
-            std::placeholders::_3, std::placeholders::_4));
+            std::placeholders::_3));
     UsdUtils_LocalizationContext context(&delegate);
     context.SetMetadataFilteringEnabled(true);
 
-    client.layers.emplace_back(rootLayer);
-
     if (!context.Process(rootLayer)) {
         return false;
     }
 
     if (outLayers) {
-        *outLayers = std::move(client.layers);
+        outLayers->push_back(rootLayer);
+        outLayers->insert(outLayers->end(), 
+            client.layers.begin(), client.layers.end());
+        std::sort(outLayers->begin() + 1, outLayers->end(), [](const SdfLayerRefPtr& a, const SdfLayerRefPtr& b) {
+            return a->GetRealPath() < b->GetRealPath();
+        });
     }
     if (outAssets) {
-        *outAssets = std::move(client.assets);
+        outAssets->assign(client.assets.begin(), client.assets.end());
+        std::sort(outAssets->begin(), outAssets->end());
     }
     if (outUnresolvedPaths) {
-        *outUnresolvedPaths = std::move(client.unresolvedPaths);
+        outUnresolvedPaths->assign(
+            client.unresolvedPaths.begin(), client.unresolvedPaths.end());
+        std::sort(outAssets->begin(), outAssets->end());
     }
 
     return true;
index 31698b2841003c4e3c25bbb2f7630e0f1d667a62..0a3989a5a9c1f92fbfae4ee8acd9d239ea4126ea 100644 (file)
@@ -143,6 +143,33 @@ class TestUsdUtilsDependencies(unittest.TestCase):
             Sdf.Layer.Find(layer.identifier), 
             Sdf.Layer.Find("anon_sublayer.usda")])
         self.assertEqual(unresolved, ["unresolved.usda"])
+    
+    def test_ComputeAllDependenciesMultipleReferences(self):
+        """Tests that identical paths only appear once in result arrays"""
+
+        def TestDirPath(path):
+            return os.path.normcase(
+                os.path.abspath(os.path.join(testDir, path)))
+        
+        testDir = "computeAllDependenciesMultipleReferences"
+        rootLayer = TestDirPath("root.usda")
+        layers, assets, unresolved = \
+            UsdUtils.ComputeAllDependencies(rootLayer)
+        
+        self.assertEqual(len(layers), 3)
+        self.assertSetEqual(
+            set(layers), 
+            {Sdf.Layer.Find(TestDirPath("common.usda")),
+             Sdf.Layer.Find(TestDirPath("reference.usda")),
+             Sdf.Layer.Find(rootLayer)})
+
+        self.assertEqual(
+            [os.path.normcase(f) for f in assets],
+            [TestDirPath("asset.txt")])
+
+        self.assertEqual(
+            [os.path.normcase(f) for f in unresolved],
+            [TestDirPath("missing.usda")])
 
     def test_ComputeAllDependenciesUserFuncFilterPaths(self):
         """Tests paths that are filtered by the processing func 
@@ -206,7 +233,7 @@ class TestUsdUtilsDependencies(unittest.TestCase):
                           os.path.normcase(os.path.abspath(assetPathDep))])
         self.assertEqual(unresolved, [])
 
-    def test_ComputeAllDependenciesUserFuncModifyPathss(self):
+    def test_ComputeAllDependenciesUserFuncModifyPaths(self):
         """Tests assets paths which are modified by the processing func
         appear correctly in returned vectors"""
 
@@ -265,8 +292,6 @@ class TestUsdUtilsDependencies(unittest.TestCase):
         self.assertEqual([os.path.normcase(f) for f in references],
             [os.path.normcase(os.path.abspath("file.txt"))])
         self.assertEqual(unresolved, [])
-            
-
 
 if __name__=="__main__":
     unittest.main()
diff --git a/pxr/usd/usdUtils/testenv/testUsdUtilsDependencies/computeAllDependenciesMultipleReferences/asset.txt b/pxr/usd/usdUtils/testenv/testUsdUtilsDependencies/computeAllDependenciesMultipleReferences/asset.txt
new file mode 100644 (file)
index 0000000..8d757e0
--- /dev/null
@@ -0,0 +1 @@
+Text Asset
\ No newline at end of file
diff --git a/pxr/usd/usdUtils/testenv/testUsdUtilsDependencies/computeAllDependenciesMultipleReferences/common.usda b/pxr/usd/usdUtils/testenv/testUsdUtilsDependencies/computeAllDependenciesMultipleReferences/common.usda
new file mode 100644 (file)
index 0000000..c82305c
--- /dev/null
@@ -0,0 +1,6 @@
+#usda 1.0
+(
+    defaultPrim = "common"
+)
+
+def "common" {}
\ No newline at end of file
diff --git a/pxr/usd/usdUtils/testenv/testUsdUtilsDependencies/computeAllDependenciesMultipleReferences/reference.usda b/pxr/usd/usdUtils/testenv/testUsdUtilsDependencies/computeAllDependenciesMultipleReferences/reference.usda
new file mode 100644 (file)
index 0000000..0a1b842
--- /dev/null
@@ -0,0 +1,13 @@
+#usda 1.0
+(
+    defaultPrim = "main"
+)
+
+def "main"
+(
+    references = [@./common.usda@]
+)
+{
+    asset textAsset = @./asset.txt@
+    asset missing = @./missing.usda@
+}
\ No newline at end of file
diff --git a/pxr/usd/usdUtils/testenv/testUsdUtilsDependencies/computeAllDependenciesMultipleReferences/root.usda b/pxr/usd/usdUtils/testenv/testUsdUtilsDependencies/computeAllDependenciesMultipleReferences/root.usda
new file mode 100644 (file)
index 0000000..4eec1cc
--- /dev/null
@@ -0,0 +1,9 @@
+#usda 1.0
+
+def "Test" (
+    references = [@./reference.usda@, @./common.usda@]
+)
+{
+    asset textAsset = @./asset.txt@
+    asset missing = @./missing.usda@
+}
\ No newline at end of file
index 97daaaca988f5b5075896690466a2a3197b662f4..a5f5d718af12f02e1d5c6d922c66b9780f7019b3 100644 (file)
@@ -109,15 +109,14 @@ class TestUsdUtilsUserProcessFunc(unittest.TestCase):
         with tempfile.TemporaryDirectory() as tempDir:
             def TestUserFunc(layer, depInfo):
                 root, ext = os.path.splitext(depInfo.assetPath)
-                if ext != ".usda":
+                if ext != '.usda':
                     return depInfo
                 
                 sourceDepLayer = Sdf.Layer.FindOrOpenRelativeToLayer(
                     layer, depInfo.assetPath)
-                crateFileName = root + ".usdc"
+                crateFileName = root + '.usdc'
                 cratePath = os.path.join(tempDir, crateFileName)
                 sourceDepLayer.Export(cratePath)
-
                 return UsdUtils.DependencyInfo(cratePath)
         
             rootPath = 'fileFormatConversion/root.usda'
@@ -134,6 +133,42 @@ class TestUsdUtilsUserProcessFunc(unittest.TestCase):
 
             self._CheckLocalizedPackageContents(localizationDir, expectedFiles)
 
+    def test_CachedProcessingFuncValues(self):
+        """Tests that the system caches processed asset path values and only
+           invokes the callback once for each layer / path pair"""
+        
+        processedPaths = set()
+        def TestUserFunc(layer, depInfo):
+            self.assertTrue(depInfo.assetPath not in processedPaths)
+            processedPaths.add(depInfo.assetPath)
+
+            name, _ = os.path.splitext(os.path.basename(depInfo.assetPath))
+
+            if name.startswith('modify'):
+                return UsdUtils.DependencyInfo('./modified.usda')
+            elif name.startswith('remove'):
+                return UsdUtils.DependencyInfo()
+            else:
+                return depInfo
+            
+        rootPath = 'duplicatePaths/root.usda'
+        localizationDir = 'duplicatePaths_localized'
+        localizedRoot = os.path.join(localizationDir, 'root.usda')
+
+        self._Localize(rootPath, localizationDir, TestUserFunc)
+        self.assertIsNotNone(Usd.Stage.Open(localizedRoot))
+
+        self.assertSetEqual(processedPaths, 
+            {'./default.usda', './modify.usda', './remove.usda'})
+
+        expectedFiles = [
+            'default.usda',
+            'modified.usda',
+            'root.usda'
+        ]
+
+        self._CheckLocalizedPackageContents(localizationDir, expectedFiles)
+
     def _Localize(self, rootPath, localizationDir, userFunc):
         if os.path.isdir(localizationDir):
             shutil.rmtree(localizationDir)
diff --git a/pxr/usd/usdUtils/testenv/testUsdUtilsUserProcessingFunc/duplicatePaths/default.usda b/pxr/usd/usdUtils/testenv/testUsdUtilsUserProcessingFunc/duplicatePaths/default.usda
new file mode 100644 (file)
index 0000000..872ab28
--- /dev/null
@@ -0,0 +1 @@
+#usda 1.0
\ No newline at end of file
diff --git a/pxr/usd/usdUtils/testenv/testUsdUtilsUserProcessingFunc/duplicatePaths/modified.usda b/pxr/usd/usdUtils/testenv/testUsdUtilsUserProcessingFunc/duplicatePaths/modified.usda
new file mode 100644 (file)
index 0000000..872ab28
--- /dev/null
@@ -0,0 +1 @@
+#usda 1.0
\ No newline at end of file
diff --git a/pxr/usd/usdUtils/testenv/testUsdUtilsUserProcessingFunc/duplicatePaths/root.usda b/pxr/usd/usdUtils/testenv/testUsdUtilsUserProcessingFunc/duplicatePaths/root.usda
new file mode 100644 (file)
index 0000000..ef1d5a0
--- /dev/null
@@ -0,0 +1,16 @@
+#usda 1.0
+(
+    defaultPrim = "main"
+    subLayers = [
+        @./default.usda@,
+        @./modify.usda@,
+        @./remove.usda@
+    ]
+)
+
+def "main"
+{
+    asset shouldBeDefault = @./default.usda@
+    asset shouldBeModified = @./modify.usda@
+    asset shouldBeRemoved = @./remove.usda@
+}
\ No newline at end of file