From 6f5d521da32a381b5de55b633c25254de045cb5d Mon Sep 17 00:00:00 2001 From: debloip Date: Fri, 24 Nov 2023 10:24:15 -0500 Subject: [PATCH 1/2] HYDRA-703 : Fix mesh adapter transform not updating --- .../adapters/cameraAdapter.cpp | 2 +- .../hydraExtensions/adapters/dagAdapter.cpp | 8 ++-- .../hydraExtensions/adapters/lightAdapter.cpp | 4 +- .../mayaHydraSceneProducer.cpp | 16 +++++++- .../hydraExtensions/mayaHydraSceneProducer.h | 2 + .../sceneIndex/mayaHydraSceneIndex.cpp | 38 +++++++++++++------ .../sceneIndex/mayaHydraSceneIndex.h | 15 ++++++-- lib/mayaHydra/mayaPlugin/renderOverride.cpp | 3 +- 8 files changed, 61 insertions(+), 27 deletions(-) diff --git a/lib/mayaHydra/hydraExtensions/adapters/cameraAdapter.cpp b/lib/mayaHydra/hydraExtensions/adapters/cameraAdapter.cpp index 2d3e3aae4d..b8c568f9fb 100644 --- a/lib/mayaHydra/hydraExtensions/adapters/cameraAdapter.cpp +++ b/lib/mayaHydra/hydraExtensions/adapters/cameraAdapter.cpp @@ -102,8 +102,8 @@ void MayaHydraCameraAdapter::CreateCallbacks() dag, +[](MObject& transformNode, MDagMessage::MatrixModifiedFlags& modified, void* clientData) { auto* adapter = reinterpret_cast(clientData); - adapter->MarkDirty(HdCamera::DirtyTransform); adapter->InvalidateTransform(); + adapter->MarkDirty(HdCamera::DirtyTransform); }, reinterpret_cast(this), &status); diff --git a/lib/mayaHydra/hydraExtensions/adapters/dagAdapter.cpp b/lib/mayaHydra/hydraExtensions/adapters/dagAdapter.cpp index 2fb92a7c64..8d411ee758 100644 --- a/lib/mayaHydra/hydraExtensions/adapters/dagAdapter.cpp +++ b/lib/mayaHydra/hydraExtensions/adapters/dagAdapter.cpp @@ -82,8 +82,8 @@ void _TransformNodeDirty(MObject& node, MPlug& plug, void* clientData) // that dirty as well... if (adapter->IsVisible(false)) { // Transform can change while dag path is hidden. - adapter->MarkDirty(HdChangeTracker::DirtyVisibility | HdChangeTracker::DirtyTransform); adapter->InvalidateTransform(); + adapter->MarkDirty(HdChangeTracker::DirtyVisibility | HdChangeTracker::DirtyTransform); } else { adapter->MarkDirty(HdChangeTracker::DirtyVisibility); } @@ -91,8 +91,8 @@ void _TransformNodeDirty(MObject& node, MPlug& plug, void* clientData) // DON'T update visibility from within this callback, since the change // has't propagated yet } else if (adapter->IsVisible(false)) { - adapter->MarkDirty(HdChangeTracker::DirtyTransform); adapter->InvalidateTransform(); + adapter->MarkDirty(HdChangeTracker::DirtyTransform); } } @@ -210,9 +210,9 @@ void MayaHydraDagAdapter::CreateCallbacks() void MayaHydraDagAdapter::MarkDirty(HdDirtyBits dirtyBits) { if (dirtyBits != 0) { - GetSceneProducer()->GetRenderIndex().GetChangeTracker().MarkRprimDirty(GetID(), dirtyBits); + GetSceneProducer()->MarkRprimDirty(GetID(), dirtyBits); if (IsInstanced()) { - GetSceneProducer()->GetRenderIndex().GetChangeTracker().MarkInstancerDirty(GetInstancerID(), dirtyBits); + GetSceneProducer()->MarkInstancerDirty(GetInstancerID(), dirtyBits); } if (dirtyBits & HdChangeTracker::DirtyVisibility) { _visibilityDirty = true; diff --git a/lib/mayaHydra/hydraExtensions/adapters/lightAdapter.cpp b/lib/mayaHydra/hydraExtensions/adapters/lightAdapter.cpp index 7c3536b19e..d2f1878be8 100644 --- a/lib/mayaHydra/hydraExtensions/adapters/lightAdapter.cpp +++ b/lib/mayaHydra/hydraExtensions/adapters/lightAdapter.cpp @@ -72,9 +72,9 @@ void _dirtyTransform(MObject& node, void* clientData) TF_UNUSED(node); auto* adapter = reinterpret_cast(clientData); if (adapter->IsVisible()) { + adapter->InvalidateTransform(); adapter->MarkDirty( HdLight::DirtyTransform | HdLight::DirtyParams | HdLight::DirtyShadowParams); - adapter->InvalidateTransform(); } } @@ -83,8 +83,8 @@ void _dirtyParams(MObject& node, void* clientData) TF_UNUSED(node); auto* adapter = reinterpret_cast(clientData); if (adapter->IsVisible()) { - adapter->MarkDirty(HdLight::DirtyParams | HdLight::DirtyShadowParams); adapter->InvalidateTransform(); + adapter->MarkDirty(HdLight::DirtyParams | HdLight::DirtyShadowParams); } } diff --git a/lib/mayaHydra/hydraExtensions/mayaHydraSceneProducer.cpp b/lib/mayaHydra/hydraExtensions/mayaHydraSceneProducer.cpp index e20bbec28a..5b716c93bf 100644 --- a/lib/mayaHydra/hydraExtensions/mayaHydraSceneProducer.cpp +++ b/lib/mayaHydra/hydraExtensions/mayaHydraSceneProducer.cpp @@ -315,7 +315,7 @@ void MayaHydraSceneProducer::MarkRprimDirty(const SdfPath& id, HdDirtyBits dirty { if (enableMayaNativeSceneIndex()) { - _sceneIndex->MarkPrimDirty(id, dirtyBits); + _sceneIndex->MarkRprimDirty(id, dirtyBits); } else { @@ -323,6 +323,18 @@ void MayaHydraSceneProducer::MarkRprimDirty(const SdfPath& id, HdDirtyBits dirty } } +void MayaHydraSceneProducer::MarkInstancerDirty(const SdfPath& id, HdDirtyBits dirtyBits) +{ + if (enableMayaNativeSceneIndex()) + { + _sceneIndex->MarkInstancerDirty(id, dirtyBits); + } + else + { + _sceneDelegate->GetRenderIndex().GetChangeTracker().MarkInstancerDirty(id, dirtyBits); + } +} + void MayaHydraSceneProducer::InsertSprim( MayaHydraAdapter* adapter, const TfToken& typeId, @@ -355,7 +367,7 @@ void MayaHydraSceneProducer::MarkSprimDirty(const SdfPath& id, HdDirtyBits dirty { if (enableMayaNativeSceneIndex()) { - _sceneIndex->MarkPrimDirty(id, dirtyBits); + _sceneIndex->MarkSprimDirty(id, dirtyBits); } else { diff --git a/lib/mayaHydra/hydraExtensions/mayaHydraSceneProducer.h b/lib/mayaHydra/hydraExtensions/mayaHydraSceneProducer.h index 6ae7776f9d..acb4752a0d 100644 --- a/lib/mayaHydra/hydraExtensions/mayaHydraSceneProducer.h +++ b/lib/mayaHydra/hydraExtensions/mayaHydraSceneProducer.h @@ -92,6 +92,8 @@ class MAYAHYDRALIB_API MayaHydraSceneProducer // Mark a Rprim in hydra scene as dirty void MarkRprimDirty(const SdfPath& id, HdDirtyBits dirtyBits); + void MarkInstancerDirty(const SdfPath& id, HdDirtyBits dirtyBits); + // Insert a Sprim to hydra scene void InsertSprim( MayaHydraAdapter* adapter, diff --git a/lib/mayaHydra/hydraExtensions/sceneIndex/mayaHydraSceneIndex.cpp b/lib/mayaHydra/hydraExtensions/sceneIndex/mayaHydraSceneIndex.cpp index b7ec9ea029..916680c0bf 100644 --- a/lib/mayaHydra/hydraExtensions/sceneIndex/mayaHydraSceneIndex.cpp +++ b/lib/mayaHydra/hydraExtensions/sceneIndex/mayaHydraSceneIndex.cpp @@ -580,7 +580,7 @@ void MayaHydraSceneIndex::SetDefaultLight(const GlfSimpleLight& light) _mayaDefaultLight.SetDiffuse(light.GetDiffuse()); _mayaDefaultLight.SetSpecular(light.GetSpecular()); _mayaDefaultLight.SetPosition(light.GetPosition()); - MarkPrimDirty(_mayaDefaultLightPath, HdLight::DirtyParams); + MarkSprimDirty(_mayaDefaultLightPath, HdLight::DirtyParams); } } @@ -943,18 +943,33 @@ void MayaHydraSceneIndex::_AddPrimAncestors(const SdfPath& path) } -void MayaHydraSceneIndex::MarkPrimDirty(const SdfPath& id, HdDirtyBits dirtyBits) +void MayaHydraSceneIndex::MarkRprimDirty(const SdfPath& id, HdDirtyBits dirtyBits) { + _MarkPrimDirty(id, dirtyBits, HdDirtyBitsTranslator::RprimDirtyBitsToLocatorSet); +} + +void MayaHydraSceneIndex::MarkSprimDirty(const SdfPath& id, HdDirtyBits dirtyBits) +{ + _MarkPrimDirty(id, dirtyBits, HdDirtyBitsTranslator::SprimDirtyBitsToLocatorSet); +} + +void MayaHydraSceneIndex::MarkBprimDirty(const SdfPath& id, HdDirtyBits dirtyBits) +{ + _MarkPrimDirty(id, dirtyBits, HdDirtyBitsTranslator::BprimDirtyBitsToLocatorSet); +} + +void MayaHydraSceneIndex::MarkInstancerDirty(const SdfPath& id, HdDirtyBits dirtyBits) +{ + _MarkPrimDirty(id, dirtyBits, HdDirtyBitsTranslator::InstancerDirtyBitsToLocatorSet); +} + +void MayaHydraSceneIndex::_MarkPrimDirty( + const SdfPath& id, + HdDirtyBits dirtyBits, + DirtyBitsToLocatorsFunc dirtyBitsToLocatorsFunc) { - // Dispatch based on prim type. HdSceneIndexPrim prim = GetPrim(id); HdDataSourceLocatorSet locators; - if (HdPrimTypeIsGprim(prim.primType)) { - HdDirtyBitsTranslator::RprimDirtyBitsToLocatorSet(prim.primType, dirtyBits, &locators); - } - else { - HdDirtyBitsTranslator::SprimDirtyBitsToLocatorSet(prim.primType, dirtyBits, &locators); - } - + dirtyBitsToLocatorsFunc(prim.primType, dirtyBits, &locators); if (!locators.IsEmpty()) { DirtyPrims({ {id, locators} }); } @@ -1254,11 +1269,10 @@ void MayaHydraSceneIndex::RecreateAdapter(const SdfPath& id, const MObject& obj) }, _materialAdapters)) { auto& renderIndex = GetRenderIndex(); - auto& changeTracker = renderIndex.GetChangeTracker(); for (const auto& rprimId : renderIndex.GetRprimIds()) { const auto* rprim = renderIndex.GetRprim(rprimId); if (rprim != nullptr && rprim->GetMaterialId() == id) { - changeTracker.MarkRprimDirty(rprimId, HdChangeTracker::DirtyMaterialId); + MarkRprimDirty(rprimId, HdChangeTracker::DirtyMaterialId); } } if (MObjectHandle(obj).isValid()) { diff --git a/lib/mayaHydra/hydraExtensions/sceneIndex/mayaHydraSceneIndex.h b/lib/mayaHydra/hydraExtensions/sceneIndex/mayaHydraSceneIndex.h index 088cb7c7e6..7b9eea3a3d 100644 --- a/lib/mayaHydra/hydraExtensions/sceneIndex/mayaHydraSceneIndex.h +++ b/lib/mayaHydra/hydraExtensions/sceneIndex/mayaHydraSceneIndex.h @@ -47,6 +47,7 @@ #include #include #include +#include "pxr/imaging/hd/dirtyBitsTranslator.h" #include @@ -106,10 +107,10 @@ class MAYAHYDRALIB_API MayaHydraSceneIndex : public HdRetainedSceneIndex, public // Remove a primitive from hydra scene void RemovePrim(const SdfPath& id); - // Mark a primitive in hydra scene as dirty - void MarkPrimDirty( - const SdfPath& id, - HdDirtyBits dirtyBits); + void MarkRprimDirty(const SdfPath& id, HdDirtyBits dirtyBits); + void MarkSprimDirty(const SdfPath& id, HdDirtyBits dirtyBits); + void MarkBprimDirty(const SdfPath& id, HdDirtyBits dirtyBits); + void MarkInstancerDirty(const SdfPath& id, HdDirtyBits dirtyBits); // Operation that's performed on rendering a frame void PreFrame(const MHWRender::MDrawContext& drawContext); @@ -202,6 +203,12 @@ class MAYAHYDRALIB_API MayaHydraSceneIndex : public HdRetainedSceneIndex, public using LightDagPathMap = std::unordered_map; LightDagPathMap _GetActiveLightPaths() const; static VtValue CreateMayaDefaultMaterial(); + + using DirtyBitsToLocatorsFunc = std::function; + void _MarkPrimDirty( + const SdfPath& id, + HdDirtyBits dirtyBits, + DirtyBitsToLocatorsFunc dirtyBitsToLocatorsFunc); private: // ------------------------------------------------------------------------ // HdSceneIndexBase implementations diff --git a/lib/mayaHydra/mayaPlugin/renderOverride.cpp b/lib/mayaHydra/mayaPlugin/renderOverride.cpp index 3d412b4eab..6eb31af770 100644 --- a/lib/mayaHydra/mayaPlugin/renderOverride.cpp +++ b/lib/mayaHydra/mayaPlugin/renderOverride.cpp @@ -643,8 +643,7 @@ MStatus MtohRenderOverride::Render( if (_mayaHydraSceneProducer) { params.camera = _mayaHydraSceneProducer->SetCameraViewport(camPath, _viewport); if (vpDirty) - _mayaHydraSceneProducer->GetRenderIndex().GetChangeTracker().MarkSprimDirty( - params.camera, HdCamera::DirtyParams); + _mayaHydraSceneProducer->MarkSprimDirty(params.camera, HdCamera::DirtyParams); } } } else { From 85b5275fc41a34a9a5d272380d02c8686ce4d415 Mon Sep 17 00:00:00 2001 From: debloip Date: Fri, 24 Nov 2023 16:20:01 -0500 Subject: [PATCH 2/2] HYDRA-703 : Add test for dirty notification --- .../hydraExtensions/mayaHydraLibInterface.h | 2 +- .../mayaUsd/render/mayaToHydra/CMakeLists.txt | 1 + .../render/mayaToHydra/cpp/CMakeLists.txt | 1 + .../cpp/testMeshAdapterTransform.cpp | 56 +++++++++++++++++++ .../cpp/testMeshAdapterTransform.py | 35 ++++++++++++ .../render/mayaToHydra/cpp/testUtils.h | 54 +++++++++++++++++- 6 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 test/lib/mayaUsd/render/mayaToHydra/cpp/testMeshAdapterTransform.cpp create mode 100644 test/lib/mayaUsd/render/mayaToHydra/cpp/testMeshAdapterTransform.py diff --git a/lib/mayaHydra/hydraExtensions/mayaHydraLibInterface.h b/lib/mayaHydra/hydraExtensions/mayaHydraLibInterface.h index 2d5c0bf0a2..b41169494d 100644 --- a/lib/mayaHydra/hydraExtensions/mayaHydraLibInterface.h +++ b/lib/mayaHydra/hydraExtensions/mayaHydraLibInterface.h @@ -26,7 +26,7 @@ PXR_NAMESPACE_OPEN_SCOPE -using SceneIndicesVector = std::vector;//Be careful, these are not not Ref counted. Elements could become dangling +using SceneIndicesVector = std::vector; /// In order to access this interface, call the function GetMayaHydraLibInterface() class MayaHydraLibInterface diff --git a/test/lib/mayaUsd/render/mayaToHydra/CMakeLists.txt b/test/lib/mayaUsd/render/mayaToHydra/CMakeLists.txt index e614f5a072..6a04af7923 100644 --- a/test/lib/mayaUsd/render/mayaToHydra/CMakeLists.txt +++ b/test/lib/mayaUsd/render/mayaToHydra/CMakeLists.txt @@ -29,6 +29,7 @@ set(TEST_SCRIPT_FILES # both versions of the test in parallel will conflict. set(TEST_SCRIPT_FILES_MESH_ADAPTER testMeshes.py + cpp/testMeshAdapterTransform.py ) foreach(script ${TEST_SCRIPT_FILES}) diff --git a/test/lib/mayaUsd/render/mayaToHydra/cpp/CMakeLists.txt b/test/lib/mayaUsd/render/mayaToHydra/cpp/CMakeLists.txt index f59cd60821..461c6b7bbf 100644 --- a/test/lib/mayaUsd/render/mayaToHydra/cpp/CMakeLists.txt +++ b/test/lib/mayaUsd/render/mayaToHydra/cpp/CMakeLists.txt @@ -22,6 +22,7 @@ target_sources(${TARGET_NAME} testWireframeSelectionHighlightSceneIndex.cpp testFvpViewportInformationMultipleViewports.cpp testFvpViewportInformationRendererSwitching.cpp + testMeshAdapterTransform.cpp ) # ----------------------------------------------------------------------------- diff --git a/test/lib/mayaUsd/render/mayaToHydra/cpp/testMeshAdapterTransform.cpp b/test/lib/mayaUsd/render/mayaToHydra/cpp/testMeshAdapterTransform.cpp new file mode 100644 index 0000000000..82ce986afe --- /dev/null +++ b/test/lib/mayaUsd/render/mayaToHydra/cpp/testMeshAdapterTransform.cpp @@ -0,0 +1,56 @@ +// 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 "testUtils.h" + +#include +#include + +#include + +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace { +const std::string kCubeName = "testCube"; +} // namespace + +TEST(MeshAdapterTransform, testDirtying) +{ + // Setup notifications accumulator for the first terminal scene index + const SceneIndicesVector& sceneIndices = GetTerminalSceneIndices(); + ASSERT_GT(sceneIndices.size(), static_cast(0)); + SceneIndexNotificationsAccumulator notifsAccumulator(sceneIndices.front()); + + // The test cube should still be selected from the Python driver + MGlobal::executeCommand("move 3 5 8"); + + // Check if the cube mesh prim had its xform dirtied + bool cubeXformWasDirtied = false; + for (const auto& dirtiedPrimEntry : notifsAccumulator.GetDirtiedPrimEntries()) { + HdSceneIndexPrim prim + = notifsAccumulator.GetObservedSceneIndex()->GetPrim(dirtiedPrimEntry.primPath); + + cubeXformWasDirtied = dirtiedPrimEntry.primPath.GetName() == kCubeName + "Shape" + && prim.primType == HdPrimTypeTokens->mesh + && dirtiedPrimEntry.dirtyLocators.Contains(HdXformSchema::GetDefaultLocator()); + + if (cubeXformWasDirtied) { + break; + } + } + EXPECT_TRUE(cubeXformWasDirtied); +} diff --git a/test/lib/mayaUsd/render/mayaToHydra/cpp/testMeshAdapterTransform.py b/test/lib/mayaUsd/render/mayaToHydra/cpp/testMeshAdapterTransform.py new file mode 100644 index 0000000000..a71a1fce73 --- /dev/null +++ b/test/lib/mayaUsd/render/mayaToHydra/cpp/testMeshAdapterTransform.py @@ -0,0 +1,35 @@ +# 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. +# +import maya.cmds as cmds +import fixturesUtils +import mtohUtils +from testUtils import PluginLoaded + +class TestMeshAdapterTransform(mtohUtils.MayaHydraBaseTestCase): + # MayaHydraBaseTestCase.setUpClass requirement. + _file = __file__ + + def setupScene(self): + self.setHdStormRenderer() + cmds.polyCube(name="testCube") + cmds.refresh() # Refresh to create the MeshAdapter and its mesh prim at the origin + + def test_Dirtying(self): + self.setupScene() + with PluginLoaded('mayaHydraCppTests'): + cmds.mayaHydraCppTest(f="MeshAdapterTransform.testDirtying") + +if __name__ == '__main__': + fixturesUtils.runTests(globals()) diff --git a/test/lib/mayaUsd/render/mayaToHydra/cpp/testUtils.h b/test/lib/mayaUsd/render/mayaToHydra/cpp/testUtils.h index 69b9f8bbb5..d905887278 100644 --- a/test/lib/mayaUsd/render/mayaToHydra/cpp/testUtils.h +++ b/test/lib/mayaUsd/render/mayaToHydra/cpp/testUtils.h @@ -31,7 +31,7 @@ PXR_NAMESPACE_OPEN_SCOPE constexpr double DEFAULT_TOLERANCE = std::numeric_limits::epsilon(); -using SceneIndicesVector = std::vector; +using SceneIndicesVector = std::vector; /** * @brief Retrieve the list of registered terminal scene indices from the Hydra plugin @@ -174,6 +174,58 @@ HdSceneIndexBaseRefPtr findSceneIndexInTree( const std::function& predicate ); +/** +* @class A utility class to accumulate and read SceneIndex notifications sent by a SceneIndex. +*/ +class SceneIndexNotificationsAccumulator : public HdSceneIndexObserver +{ +public: + SceneIndexNotificationsAccumulator(HdSceneIndexBaseRefPtr observedSceneIndex) + : _observedSceneIndex(observedSceneIndex) + { + _observedSceneIndex->AddObserver(HdSceneIndexObserverPtr(this)); + } + ~SceneIndexNotificationsAccumulator() override + { + _observedSceneIndex->RemoveObserver(HdSceneIndexObserverPtr(this)); + } + + HdSceneIndexBaseRefPtr GetObservedSceneIndex() { return _observedSceneIndex; } + + const AddedPrimEntries& GetAddedPrimEntries() { return _addedPrimEntries; } + const RemovedPrimEntries& GetRemovedPrimEntries() { return _removedPrimEntries; } + const DirtiedPrimEntries& GetDirtiedPrimEntries() { return _dirtiedPrimEntries; } + const RenamedPrimEntries& GetRenamedPrimEntries() { return _renamedPrimEntries; } + + void PrimsAdded(const HdSceneIndexBase& sender, const AddedPrimEntries& entries) override + { + _addedPrimEntries.insert(_addedPrimEntries.end(), entries.begin(), entries.end()); + } + + void PrimsRemoved(const HdSceneIndexBase& sender, const RemovedPrimEntries& entries) override + { + _removedPrimEntries.insert(_removedPrimEntries.end(), entries.begin(), entries.end()); + } + + void PrimsDirtied(const HdSceneIndexBase& sender, const DirtiedPrimEntries& entries) override + { + _dirtiedPrimEntries.insert(_dirtiedPrimEntries.end(), entries.begin(), entries.end()); + } + + void PrimsRenamed(const HdSceneIndexBase& sender, const RenamedPrimEntries& entries) override + { + _renamedPrimEntries.insert(_renamedPrimEntries.end(), entries.begin(), entries.end()); + } + +private: + HdSceneIndexBaseRefPtr _observedSceneIndex; + + AddedPrimEntries _addedPrimEntries; + DirtiedPrimEntries _dirtiedPrimEntries; + RemovedPrimEntries _removedPrimEntries; + RenamedPrimEntries _renamedPrimEntries; +}; + PXR_NAMESPACE_CLOSE_SCOPE #endif // MAYAHYDRA_TEST_UTILS_H