## Copyright (c) 2021, NVIDIA CORPORATION.  All rights reserved.
##
## NVIDIA CORPORATION and its licensors retain all intellectual property
## and proprietary rights in and to this software, related documentation
## and any modifications thereto.  Any use, reproduction, disclosure or
## distribution of this software and related documentation without an express
## license agreement from NVIDIA CORPORATION is strictly prohibited.
##
import omni.kit.app
import omni.kit.commands
import omni.kit.undo
import omni.kit.test
import omni.timeline
import carb.settings
from .test_hydra_common import RtxHydraTest, RtxHydraMaterialsTest, RtxHydraInstancerTest
from .test_common import postLoadTestSettings, set_transform_helper, wait_for_update
from pxr import Vt, Gf, Sdf, UsdGeom, UsdShade

cubePts = [(-50, -50, -50), (50, -50, -50), (-50, -50, 50), (50, -50, 50),
           (-50, 50, -50), (50, 50, -50), (50, 50, 50), (-50, 50, 50)]
cubeFaceVCount = [4, 4, 4, 4, 4, 4]
cubeIndices = [0, 1, 3, 2, 0, 4, 5, 1, 1, 5, 6, 3, 2, 3, 6, 7, 0, 2, 7, 4, 4, 7, 6, 5]


class TestRtxHydraMesh(RtxHydraTest):
    TEST_PATH = "hydra/mesh"
    PRIM_PATH = "/World/box"

    async def setUp(self):
        await super().setUp()
        timeline = omni.timeline.get_timeline_interface()
        timeline.set_target_framerate(timeline.get_time_codes_per_seconds())

    def create_geometry(self, name=PRIM_PATH):
        box = UsdGeom.Mesh.Define(self.ctx.get_stage(), name)
        box.CreatePointsAttr(cubePts)
        box.CreateFaceVertexCountsAttr(cubeFaceVCount)
        box.CreateFaceVertexIndicesAttr(cubeIndices)
        box.CreateSubdivisionSchemeAttr("none")
        return box

    #
    # Common tests for mesh
    #

    async def test_visibility(self):
        """
        Test hydra mesh - visibility
        """
        await self.visibility()

        # OM-34158
        # Verify proper refresh when points are moved while the mesh is invisible.
        prim = self.ctx.get_stage().GetPrimAtPath(self.PRIM_PATH)
        mesh = UsdGeom.Mesh(prim)
        points = mesh.GetPointsAttr().Get()
        for idx, p in enumerate(points):
            points[idx] = p + Gf.Vec3f(100, 0, 0)
        mesh.GetPointsAttr().Set(points)
        await wait_for_update()
        mesh.GetVisibilityAttr().Set(UsdGeom.Tokens.inherited)
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "boxPtsMovedWhileInvisible.png")

    async def test_doNotCastShadow(self):
        """
        Test hydra mesh - doNotCastShadow toggle
        """
        self.create_geometry_with_floor()
        # OM-58993: Properly cast shadows.
        await self.toggle_primvar(self.PRIM_PATH, "doNotCastShadows")

    # TODO setup scene which shows the difference
    # async def test_shadowTerminatorFix(self):
    #     """
    #     Test hydra mesh - enableShadowTerminatorFix toggle
    #     """
    #     self.create_geometry_with_floor()
    #     await self.toggle_primvar(self.PRIM_PATH, "enableShadowTerminatorFix")

    async def test_matteObject(self):
        """
        Test hydra mesh - isMatteObject toggle
        """
        self.create_geometry_with_floor()
        await self.matte()

    async def test_hideForCamera(self):
        """
        Test hydra mesh - hideForCamera toggle
        """
        self.create_geometry_with_floor()
        await self.toggle_primvar(self.PRIM_PATH, "hideForCamera")

    async def test_wireframe(self):
        """
        Test hydra mesh - wireframe toggle
        """
        self.create_geometry()
        self.set_settings({"/rtx/wireframe/wireframeThickness": 10})
        await self.toggle_primvar(self.PRIM_PATH, "wireframe", baseGoldenName="geom.png", threshould=1e-7)

    async def test_singleSided(self):
        """
        Test hydra mesh - singleSided toggle
        """
        await self.single_sided()

    async def test_pickable(self):
        """
        Test hydra mesh - pickable flag toggle
        """
        await self.pickable()

    # OM-3262 Verify proper refresh when authoring purpose
    async def test_purpose(self):
        """
        Test hydra mesh - purpose toggle
        """
        self.set_settings({'/persistent/app/hydra/displayPurpose/guide': False})

        self.create_geometry()
        geom = self.ctx.get_stage().GetPrimAtPath(self.PRIM_PATH)
        UsdGeom.Imageable(geom).CreatePurposeAttr().Set(UsdGeom.Tokens.guide)
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "purpose.png")

        # OM-62070
        # Verify mesh visibility updates when changing global setting
        self.set_settings({'/persistent/app/hydra/displayPurpose/guide': True})
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "guideVisEnable.png")
        self.set_settings({'/persistent/app/hydra/displayPurpose/guide': False})
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "guideVisDisable.png")

        # Verify inherited purpose authoring
        self.set_settings({'/persistent/app/hydra/displayPurpose/guide': False})
        super().open_usd("hydra/inheritedPurpose.usda")
        scope = self.ctx.get_stage().GetPrimAtPath('/World/Scope')
        UsdGeom.Imageable(scope).CreatePurposeAttr().Set(UsdGeom.Tokens.guide)
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "inheritedGuidePurpose.png")
        UsdGeom.Imageable(scope).GetPurposeAttr().Clear()
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "inheritedDefaultPurpose.png", 5e-5)

        # OM-32623
        # Verify proper population of new repr when toggling saved guide purpose to default.
        self.set_settings({'/persistent/app/hydra/displayPurpose/guide': False})
        super().open_usd("hydra/guideRepr.usda")
        geom = self.ctx.get_stage().GetPrimAtPath('/World/Cube')
        UsdGeom.Imageable(geom).GetPurposeAttr().Clear()
        await wait_for_update()
        # Allow for a bit of noise between local run and TC
        await self.capture_and_compare(self.TEST_PATH, "guideRepr.png", 1e-4)

    async def test_transform(self):
        """
        Test hydra mesh - tranform
        """
        await self.transform_all()

        # OM-26055
        # Verify that fallback xform values are properly initialized.
        super().open_usd("hydra/OM-26055_scene.usda")
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "OM-26055_scene.png")

        # OM-34447
        # Verify that xforms properly resync when initially present with no value.
        super().open_usd("hydra/empty_xform_mtx.usda")
        stage = self.ctx.get_stage()
        set_transform_helper("/World/Mesh", translate=Gf.Vec3d(200, 0, 0))
        await wait_for_update()
        # Looser threshold, as the test is somewhat noisy on Linux TC
        await self.capture_and_compare(self.TEST_PATH, "resync-empty_xform_mtx.png", 1e-4)

        # Verify that ancestral xforms properly affect invisible geometry.
        super().open_usd("hydra/simpleCubeAncestralXforms.usda")
        stage = self.ctx.get_stage()
        await wait_for_update()
        intermediateXform = UsdGeom.Imageable(stage.GetPrimAtPath("/World/Xform/Xform_01"))
        intermediateXform.MakeVisible(False)
        await wait_for_update()
        ancestralXform = UsdGeom.XformCommonAPI(stage.GetPrimAtPath("/World/Xform"))
        ancestralXform.SetTranslate((100,0,0))
        await wait_for_update()
        intermediateXform.MakeVisible(True)
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "postVisAncestralXform.png")

    async def test_backfaceCulling(self):
        """
        Test hydra mesh - verify toggling of backface culling is correctly propagated
        """

        # Scene loads without backface culling: a green place partially covers a gray sphere
        super().open_usd("hydra/backFaceCulling.usda")
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "backfaceCulling_off.png")

        self.set_settings({"/rtx/hydra/faceCulling/enabled": True})
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "backfaceCulling_on.png")

        stage = self.ctx.get_stage()
        prim = stage.GetPrimAtPath("/World/Plane")
        attr = prim.GetAttribute("singleSided")
        attr.Set(False)
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "backfaceCulling_doublesided.png")

    async def test_displayColor(self):
        """
        Test hydra mesh - change display color
        """
        await self.geometry_with_attribute("primvars:displayColor", Sdf.ValueTypeNames.Color3f, [(0, 1, 0)])
        await self.capture_and_compare(self.TEST_PATH, "displayColor.png")

        # OM-58993 - Changing only display color should work
        prim = self.ctx.get_stage().GetPrimAtPath(self.PRIM_PATH)
        mesh = UsdGeom.Mesh(prim)
        displayColorAttr = mesh.GetDisplayColorAttr()
        displayColorAttr.Set([(0, 0, 1)])
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "displayColorChange.png")

        stage = self.ctx.get_stage()
        prim = stage.GetPrimAtPath(self.PRIM_PATH)
        mesh = UsdGeom.Mesh(prim)
        attr = prim.GetAttribute("primvars:displayColor")
        primvar = UsdGeom.Primvar(attr)

        primvar.SetInterpolation(UsdGeom.Tokens.uniform)
        colors = [(1, 0, 0), (1, 1, 0), (0, 1, 0), (0, 1, 1), (0, 0, 1), (1, 0, 1)]
        attr.Set(colors)
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "displayColorUniform.png")

        primvar.SetInterpolation(UsdGeom.Tokens.vertex)
        colors = [(1, 0, 0), (1, 0, 1), (1, 1, 1), (1, 1, 0), (0, 0, 0), (0, 0, 1), (0, 1, 1), (0, 1, 0)]
        attr.Set(colors)
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "displayColorVertex.png")

    async def test_primvarBinding(self):
        """
        Test hydra mesh - varify effects of material edits to geometry primvar binding
        """

        # OM-83102 - the file should open with a material lookup pointing to no primvar. It should render
        # a default magenta color
        super().open_usd("hydra/primvars_edit.usda")
        await self.capture_and_compare(self.TEST_PATH, "primvarNotFound_magenta.png")

        # changing the default lookup color to yellow
        prim = self.ctx.get_stage().GetPrimAtPath("/World/Looks/mtl_emissive/data_lookup_color")
        attr = prim.GetAttribute("inputs:default_value")
        attr.Set((1, 1, 0))
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "primvarNotFound_yellow.png")

        # specify an existing primvar to lookup
        attr = prim.GetAttribute("inputs:name")
        attr.Set("blue")
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "primvarFound_blue.png")

        # change to a different existing primvar to lookup
        attr = prim.GetAttribute("inputs:name")
        attr.Set("green")
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "primvarFound_green.png")

        # change the default value to red now should have no effect
        attr = prim.GetAttribute("inputs:default_value")
        attr.Set((1, 0, 0))
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "primvarFound_still_green.png")

        # change to a different not-existing primvar to lookup, now it should render the default color set in previous step
        attr = prim.GetAttribute("inputs:name")
        attr.Set("foo_bar")
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "primvarNotFound_red.png")


    #
    # Mesh specific tests
    #
    async def test_topology(self):
        """
        Test hydra mesh - topology changes - transform cube to wedge
        """
        box = self.create_geometry()
        await wait_for_update()  # Wait 5 frames to be sure that we update topology
        box.CreateFaceVertexCountsAttr([4, 3, 4, 3, 4])
        box.CreateFaceVertexIndicesAttr([0, 1, 3, 2, 0, 4, 1, 0, 2, 7, 4, 2, 3, 7, 1, 4, 7, 3])
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "topology.png")

    async def subdiv_default(self, shouldWait=False):
        box = self.create_geometry()
        if shouldWait:
            await wait_for_update()
        subdivAttr = box.GetSubdivisionSchemeAttr()
        subdivAttr.Set("catmullClark")
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "subdivDefault.png")

    async def test_subdiv(self):
        """
        Test hydra mesh - subdiv
        """
        await self.subdiv_default(shouldWait=True)
        self.ctx.get_stage().RemovePrim(self.PRIM_PATH)
        await wait_for_update()
        await self.subdiv_default(shouldWait=False)
        self.ctx.get_stage().RemovePrim(self.PRIM_PATH)

        # Set refinement level with setting
        self.set_settings({"/rtx/hydra/subdivision/refinementLevel": 2})
        geom = self.create_geometry()
        subdivAttr = geom.GetSubdivisionSchemeAttr()
        subdivAttr.Set("catmullClark")
        geom.GetPrim().CreateAttribute("refinementLevel", Sdf.ValueTypeNames.Int).Set(0)
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "subdivRefinementLevel.png", 1e-4)
        self.ctx.get_stage().RemovePrim(self.PRIM_PATH)

        # Set refinement level override from usd
        self.set_settings({"/rtx/hydra/subdivision/refinementLevel": 0})
        geom = self.create_geometry()
        subdivAttr = geom.GetSubdivisionSchemeAttr()
        subdivAttr.Set("catmullClark")
        geom.GetPrim().CreateAttribute("refinementEnableOverride", Sdf.ValueTypeNames.Bool).Set(True)
        geom.GetPrim().CreateAttribute("refinementLevel", Sdf.ValueTypeNames.Int).Set(2)
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "subdivRefinementLevel.png", 1e-4)

        # OM-21340 Verify that load of refined geometry does not crash.
        super().open_usd("hydra/cone_sbdv.usda")
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "cone_sbdv.png", 1e-4)

    async def test_subdiv_toggle(self):
        """
        Test hydra mesh - subdiv toggle on/off, this test verifies that change between regular polygons and subdiv
        geometry doesn't leave the mesh in a corrupt internal state.
        """
        # OM-88053: toggle on/off sudivision surfaces on a mesh *with authored normals*.
        # Begin with checking the input geometry: a primitve cube with authored normals.
        super().open_usd("hydra/cube_subdiv.usda")
        geom = self.ctx.get_stage().GetPrimAtPath("/World/Cube")
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "subdivToggle_off.png", 1e-4)

        # Enabling Catmull-Clark subdivision without changing any other option would simply
        # discard the authored normals, but still render the polygonal cage.
        subdivAttr = geom.GetAttribute("subdivisionScheme")
        subdivAttr.Set("catmullClark")
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "subdivToggle_on_smoothNormals.png", 1e-4)

        # This should produce a subdivided smooth mesh.
        geom.CreateAttribute("refinementEnableOverride", Sdf.ValueTypeNames.Bool).Set(True)
        geom.CreateAttribute("refinementLevel", Sdf.ValueTypeNames.Int).Set(2)
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "subdivToggle_on_subdivided.png", 1e-4)

        # Deform by moving a point.
        mesh = UsdGeom.Mesh(geom)
        points = mesh.GetPointsAttr().Get()
        points[7] = points[7] + Gf.Vec3f(0, 40, 0)
        mesh.GetPointsAttr().Set(points)
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "subdivToggle_on_deformed.png", 1e-4)

        # Also test bilinear subdivision
        subdivAttr.Set("bilinear")
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "subdivToggle_on_bilinear.png", 1e-4)

        # Finally this should go back to the original mesh with authored normals.
        subdivAttr.Set("none")
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "subdivToggle_off_again.png", 1e-4)

    async def test_handedness(self):
        """
        Test hydra mesh - toggle orintation between right and left handed, in a combination
        of triangulated mesh and quadrangulated mesh, with and without subdivision surfaces.
        """
        # OM-113941: toggle handedness. A custom material front-facing green and back-facing red.
        super().open_usd("hydra/handedness_001.usda")
        await wait_for_update()
        self.set_settings(postLoadTestSettings)

        # This triangulated mesh is left-handed and has subdivision surface on. It also has broken normals
        # (a vector of zero entries) that would have resulted in a crash when toggling subdivision surfaces off.
        geomT = self.ctx.get_stage().GetPrimAtPath("/World/glasst_mod/SH000254198100001_Tube/SH000254198100001_Tube")
        # This is a mesh made of quads and right-handed
        geomQ = self.ctx.get_stage().GetPrimAtPath("/World/glassq/SH000254198100001_Tube/SH000254198100001_Tube")
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "handedness_001_subdiv_green.png", 1e-4)

        # Toggle handedness
        geomT.GetAttribute("orientation").Set("leftHanded") # This edit would not render red in OM-113941
        geomQ.GetAttribute("orientation").Set("rightHanded")
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "handedness_001_subdiv_red.png", 1e-4)

        # Toggle subdivision surface
        geomT.GetAttribute("subdivisionScheme").Set("none") # This edit would crash OM-113941
        geomQ.GetAttribute("subdivisionScheme").Set("catmullClark")
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "handedness_001_mesh_red.png", 1e-4)

        # Toggle handedness again
        geomT.GetAttribute("orientation").Set("rightHanded")
        geomQ.GetAttribute("orientation").Set("leftHanded")
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "handedness_001_mesh_green.png", 1e-4)

    async def test_points(self):
        """
        Test hydra mesh - update points
        """
        pts = [cubePts, cubePts.copy()]
        pts[1][1] = (150, -50, -50)
        await self.attribute_test_case(self.create_geometry, "points", Sdf.ValueTypeNames.Point3f, pts,
                                       ["geom.png", "points.png"])
        self.ctx.get_stage().RemovePrim(self.PRIM_PATH)

        # OM-76321: this set of tests [interleavedPointsEdit_*] verifies intermittent primvar edits during
        # regular points edit. Special care is due because of the minimal updates we do to mesh VBs
        # Interleaved points edit - initial state, this should render a blue cube.
        geom = self.create_geometry()
        mesh = UsdGeom.Mesh(geom)
        displayColorAttr = mesh.GetDisplayColorAttr()
        displayColorAttr.Set([(0, 0, 1)])
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "interleavedPointsEdit_0.png", 1e-4)

        # Interleaved points edit - first edit, the cube is slightly deformed and still blue.
        # This first edit goes through full mesh update to create mutable buffers (not deduplicated).
        pts[1][1] = (60, -50, -50)
        geom.GetPointsAttr().Set(pts[1])
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "interleavedPointsEdit_1.png", 1e-4)

        # Interleaved points edit - second edit, the cube is deformed again and is now set to green
        # The second edit goes through meshUpdateVertices and creates a second VB.
        pts[1][1] = (70, -50, -50)
        geom.GetPointsAttr().Set(pts[1])
        displayColorAttr.Set([(0, 1, 0)])
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "interleavedPointsEdit_2.png", 1e-4)

        # Interleaved points edit - third edit, the cube is more deformed and should still be green
        # The last edit verifies that the previous edit to primvars need to be propagated to The
        # VB ping-pong swap. If this fails the cube will be blue (as in previous frame) instead of green.
        pts[1][1] = (80, -50, -50)
        geom.GetPointsAttr().Set(pts[1])
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "interleavedPointsEdit_3.png", 1e-4)
        self.ctx.get_stage().RemovePrim(self.PRIM_PATH)

    async def test_normals(self):
        """
        Test hydra mesh - update normals
        """
        self.set_settings({"/rtx/debugView/target": "normal"})

        # Authored face varying interpolation
        normals = [[(0, -1, 0), (0, -1, 0), (0, -1, 0), (0, -1, 0), (0, 0, -1), (0, 0, -1), (0, 0, -1), (0, 0, -1),
                    (1, 0, 0), (1, 0, 0), (1, 0, 0), (1, 0, 0), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1),
                    (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0)],
                   [(0, -1, 0), (0, -1, 0), (0, -1, 0), (0, -1, 0), (0, 0, -1), (0, 0, -1), (0, 0, -1), (0, 0, -1),
                    (0.5, 0.5, 0), (0.5, 0.5, 0), (0.5, 0.5, 0), (0.5, 0.5, 0), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1),
                    (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0)]]

        def createGeom():
            geom = self.create_geometry()
            geom.CreateNormalsAttr()
            geom.SetNormalsInterpolation("faceVarying")
            return geom
        await self.attribute_test_case(createGeom, "normals", Sdf.ValueTypeNames.Point3f, normals,
                                       ["normals.png", "normalsAuthoredFaceVarying.png"])
        self.ctx.get_stage().RemovePrim(self.PRIM_PATH)

        # Authored vertex interpolation
        geom = self.create_geometry()
        geom.GetNormalsAttr().Set([(0, -1, 0), (0, -1, 0), (0, -1, 0), (0, -1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0)])
        geom.SetNormalsInterpolation("vertex")
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "normalsAuthoredVertex.png", 1e-4)
        self.ctx.get_stage().RemovePrim(self.PRIM_PATH)

        # Authored uniform interpolation
        geom = self.create_geometry()
        geom.GetNormalsAttr().Set([(0, -1, 0), (0, 0, -1), (1, 0, 0), (0, 0, 1), (-1, 0, 0), (0, 1, 0)])
        geom.SetNormalsInterpolation("uniform")
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "normals.png")
        self.ctx.get_stage().RemovePrim(self.PRIM_PATH)

        # Authored constant interpolation
        geom = self.create_geometry()
        geom.GetNormalsAttr().Set([(0, 1, 0)])
        geom.SetNormalsInterpolation("constant")
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "normalsAuthoredConstant.png")
        self.ctx.get_stage().RemovePrim(self.PRIM_PATH)

        # Generated smooth
        geom = self.create_geometry()
        geom.GetSubdivisionSchemeAttr().Set("catmullClark")
        geom.GetPrim().CreateAttribute("refinementLevel", Sdf.ValueTypeNames.Int).Set(0)
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "normalsGeneratedSmooth.png")
        self.ctx.get_stage().RemovePrim(self.PRIM_PATH)

        # Generated flat
        self.create_geometry()
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "normals.png")
        self.ctx.get_stage().RemovePrim(self.PRIM_PATH)

        # TBN Frame mode - generate normals on gpu
        self.set_settings({"/rtx/hydra/TBNFrameMode": 2})
        geom = self.create_geometry()
        geom.GetSubdivisionSchemeAttr().Set("catmullClark")
        geom.GetPrim().CreateAttribute("refinementLevel", Sdf.ValueTypeNames.Int).Set(0)
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "normalsGeneratedSmooth-gpu.png", 5e-5)
        self.ctx.get_stage().RemovePrim(self.PRIM_PATH)

    # OM-94303: Unstable due to golden image mismatch
    async def test_UNSTABLE_gpu_normals(self):
        """
        Test hydra mesh - gpu normals
        """
        super().open_usd("hydra/normals_gen_001.usda")
        await omni.kit.app.get_app().next_update_async()
        self.set_settings(postLoadTestSettings)
        self.set_settings({"/rtx/hydra/TBNFrameMode": 2})
        self.set_settings({"/rtx/debugView/target": "materialGeometryNormal"})

        await self.capture_and_compare(self.TEST_PATH, "gpu_normals_creation.png")

        # Generated on GPU with reference normals
        stage = self.ctx.get_stage()
        prim = stage.GetPrimAtPath("/World/pSphere2")
        mesh = UsdGeom.Mesh(prim)
        points = mesh.GetPointsAttr().Get()

        # By moving a point we force the generation of normals. Regression test for OM-90364 / MR !24845
        points[16] = Gf.Vec3f(0, 11, 0)
        mesh.GetPointsAttr().Set(points)

        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "gpu_normals_editing.png")

    async def test_texCoords(self):
        """
        Test hydra mesh - update tex coords
        """
        self.set_settings({"/rtx/debugView/target": "texcoord0"})

        # faceVarying
        geom = self.create_geometry()
        # https://github.com/PixarAnimationStudios/USD/commit/592b4d39edf5daf0534d467e970c95462a65d44b
        # UsdGeom.Imageable.CreatePrimvar deprecated in v19.03 and removed in v22.08
        UsdGeom.PrimvarsAPI(geom.GetPrim()).CreatePrimvar("st", Sdf.ValueTypeNames.TexCoord2fArray, UsdGeom.Tokens.faceVarying).Set(
            [(1, 0), (0, 0), (0, 1), (1, 1)])
        geom.GetPrim().CreateAttribute("primvars:st:indices", Sdf.ValueTypeNames.IntArray, False).Set(
            [0, 1, 2, 3, 0, 3, 2, 1, 0, 1, 2, 3, 0, 1, 2, 3, 0, 3, 2, 1, 0, 3, 2, 1])
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "texCoordsFaceVarying.png")
        self.ctx.get_stage().RemovePrim(self.PRIM_PATH)

        # vertex or varying (they are the same for a mesh geometry)
        geom = self.create_geometry()
        # https://github.com/PixarAnimationStudios/USD/commit/592b4d39edf5daf0534d467e970c95462a65d44b
        # UsdGeom.Imageable.CreatePrimvar deprecated in v19.03 and removed in v22.08
        UsdGeom.PrimvarsAPI(geom.GetPrim()).CreatePrimvar("st", Sdf.ValueTypeNames.TexCoord2fArray, UsdGeom.Tokens.vertex).Set(
            [(0, 0), (0.1, 0), (0.2, 0), (0.3, 0), (0.4, 0), (0.5, 0), (0.6, 0), (0.7, 0)])
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "texCoordsVertex.png")
        self.ctx.get_stage().RemovePrim(self.PRIM_PATH)

        # uniform
        geom = self.create_geometry()
        # https://github.com/PixarAnimationStudios/USD/commit/592b4d39edf5daf0534d467e970c95462a65d44b
        # UsdGeom.Imageable.CreatePrimvar deprecated in v19.03 and removed in v22.08
        UsdGeom.PrimvarsAPI(geom.GetPrim()).CreatePrimvar("st", Sdf.ValueTypeNames.TexCoord2fArray, UsdGeom.Tokens.uniform).Set(
            [(0, 0), (0, 0.1), (0, 0.2), (0, 0.3), (0, 0.4), (0, 0.5)])
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "texCoordsUniform.png")
        self.ctx.get_stage().RemovePrim(self.PRIM_PATH)


    async def test_tangents(self):
        """
        Test hydra mesh - update tangents
        """
        self.set_settings({"/rtx/debugView/target": "tangentu"})

        # default - gpu
        geom = self.create_geometry()
        # https://github.com/PixarAnimationStudios/USD/commit/592b4d39edf5daf0534d467e970c95462a65d44b
        # UsdGeom.Imageable.CreatePrimvar deprecated in v19.03 and removed in v22.08
        UsdGeom.PrimvarsAPI(geom.GetPrim()).CreatePrimvar("st", Sdf.ValueTypeNames.TexCoord2fArray, UsdGeom.Tokens.faceVarying).Set(
            [(1, 0), (0, 0), (0, 1), (1, 1)])
        geom.GetPrim().CreateAttribute("primvars:st:indices", Sdf.ValueTypeNames.IntArray, False).Set(
            [0, 1, 2, 3, 0, 3, 2, 1, 0, 1, 2, 3, 0, 1, 2, 3, 0, 3, 2, 1, 0, 3, 2, 1])
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "tangentsU.png")
        self.set_settings({"/rtx/debugView/target": "tangentv"})
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "tangentsV.png")
        self.ctx.get_stage().RemovePrim(self.PRIM_PATH)

        # cpu - uniform
        geom = self.create_geometry()
        geom.GetNormalsAttr().Set([(0, -1, 0), (0, 0, -1), (1, 0, 0), (0, 0, 1), (-1, 0, 0), (0, 1, 0)])
        geom.SetNormalsInterpolation("uniform")
        # https://github.com/PixarAnimationStudios/USD/commit/592b4d39edf5daf0534d467e970c95462a65d44b
        # UsdGeom.Imageable.CreatePrimvar deprecated in v19.03 and removed in v22.08
        UsdGeom.PrimvarsAPI(geom.GetPrim()).CreatePrimvar("st", Sdf.ValueTypeNames.TexCoord2fArray, UsdGeom.Tokens.faceVarying).Set(
            [(1, 0), (0, 0), (0, 1), (1, 1)])
        geom.GetPrim().CreateAttribute("primvars:st:indices", Sdf.ValueTypeNames.IntArray, False).Set(
            [0, 1, 2, 3, 0, 3, 2, 1, 0, 1, 2, 3, 0, 1, 2, 3, 0, 3, 2, 1, 0, 3, 2, 1])
        self.set_settings({"/rtx/debugView/target": "tangentu"})
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "tangentsU.png")
        self.set_settings({"/rtx/debugView/target": "tangentv"})
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "tangentsV.png")
        self.ctx.get_stage().RemovePrim(self.PRIM_PATH)

        # cpu - vertex
        geom = self.create_geometry()
        geom.GetNormalsAttr().Set([(-0.57735, -0.57735, -0.57735), (0.57735, -0.57735, -0.57735), (-0.57735, -0.57735, 0.57735), (0.57735, -0.57735, 0.57735),
           (-0.57735, 0.57735, -0.57735), (0.57735, 0.57735, -0.57735), (0.57735, 0.57735, 0.57735), (-0.57735, 0.57735, 0.57735)])
        geom.SetNormalsInterpolation("vertex")
        # https://github.com/PixarAnimationStudios/USD/commit/592b4d39edf5daf0534d467e970c95462a65d44b
        # UsdGeom.Imageable.CreatePrimvar deprecated in v19.03 and removed in v22.08
        UsdGeom.PrimvarsAPI(geom.GetPrim()).CreatePrimvar("st", Sdf.ValueTypeNames.TexCoord2fArray, UsdGeom.Tokens.faceVarying).Set(
            [(1, 0), (0, 0), (0, 1), (1, 1)])
        geom.GetPrim().CreateAttribute("primvars:st:indices", Sdf.ValueTypeNames.IntArray, False).Set(
            [0, 1, 2, 3, 0, 3, 2, 1, 0, 1, 2, 3, 0, 1, 2, 3, 0, 3, 2, 1, 0, 3, 2, 1])
        self.set_settings({"/rtx/debugView/target": "tangentu"})
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "tangentsVertexU.png")
        self.set_settings({"/rtx/debugView/target": "tangentv"})
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "tangentsVertexV.png")
        self.ctx.get_stage().RemovePrim(self.PRIM_PATH)

        # cpu - facevarying
        geom = self.create_geometry()
        geom.GetNormalsAttr().Set([(0, -1, 0), (0, -1, 0), (0, -1, 0), (0, -1, 0), (0, 0, -1), (0, 0, -1), (0, 0, -1), (0, 0, -1),
                    (1, 0, 0), (1, 0, 0), (1, 0, 0), (1, 0, 0), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1),
                    (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0)])
        geom.SetNormalsInterpolation("faceVarying")
        # https://github.com/PixarAnimationStudios/USD/commit/592b4d39edf5daf0534d467e970c95462a65d44b
        # UsdGeom.Imageable.CreatePrimvar deprecated in v19.03 and removed in v22.08
        UsdGeom.PrimvarsAPI(geom.GetPrim()).CreatePrimvar("st", Sdf.ValueTypeNames.TexCoord2fArray, UsdGeom.Tokens.faceVarying).Set(
            [(1, 0), (0, 0), (0, 1), (1, 1)])
        geom.GetPrim().CreateAttribute("primvars:st:indices", Sdf.ValueTypeNames.IntArray, False).Set(
            [0, 1, 2, 3, 0, 3, 2, 1, 0, 1, 2, 3, 0, 1, 2, 3, 0, 3, 2, 1, 0, 3, 2, 1])
        self.set_settings({"/rtx/debugView/target": "tangentu"})
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "tangentsFacevaryingU.png")
        self.set_settings({"/rtx/debugView/target": "tangentv"})
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "tangentsFacevaryingV.png")
        self.ctx.get_stage().RemovePrim(self.PRIM_PATH)

    async def test_skeleton(self):
        """
        Test hydra mesh - skeleton
        """
        super().open_usd("hydra/skelcylinder.usda")
        # self.set_settings(postLoadTestSettings)
        await omni.kit.app.get_app().next_update_async()
        self.set_settings(postLoadTestSettings)
        # create
        timeline = omni.timeline.get_timeline_interface()
        timeline.play()
        timeline.set_auto_update(False)
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "skeleton0.png")
        timeline.set_current_time(1)
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "skeleton1.png")
        timeline.set_auto_update(True)
        timeline.stop()
        # TODO remove, update
        # create many instances
        # SkinningMethod: eDualQuaternion, eClassicLinear, eWeightedBlend

    async def test_mesh_with_api_schemas(self):
        """
        Verify proper population and refresh when apiSchemas are present
        """
        # OM-24223: Verify proper mesh population when duplicating with apiSchemas present
        stage = self.ctx.get_stage()
        cube = UsdGeom.Cube.Define(stage, "/Cube")
        cube.GetSizeAttr().Set(50.0)
        # Doesn't matter which API schema is applied, just use one from core, to avoid depending on physics.
        # https://github.com/PixarAnimationStudios/USD/commit/6f0ce585bf5d06ca929584b515cd3bdf05d78eb3
        # https://github.com/PixarAnimationStudios/USD/commit/1222ea7cd2478e576f4fc32936f781d2a7f61cd5
        # https://github.com/PixarAnimationStudios/USD/commit/61fcc34d8555bb269fae49af90eab5154989392c
        if hasattr(UsdGeom, 'VisibilityAPI'):
            UsdGeom.VisibilityAPI.Apply(cube.GetPrim())
        else:
            from pxr import UsdRender
            UsdRender.SettingsAPI.Apply(cube.GetPrim())
        await wait_for_update()
        omni.kit.commands.execute("CopyPrim", path_from="/Cube", path_to="/DupeCube")
        self.ctx.get_selection().clear_selected_prim_paths()
        set_transform_helper("/DupeCube", translate=Gf.Vec3d(200, 0, 0))
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "dupeWithApiSchema.png")
        self.ctx.get_stage().RemovePrim("/Cube")
        self.ctx.get_stage().RemovePrim("/DupeCube")
        await wait_for_update()

        # OM-29819: Verify proper normals after applying an API schema
        super().open_usd("hydra/torus_mesh.usda")
        await wait_for_update()
        stage = self.ctx.get_stage()
        # Doesn't matter which API schema is applied, just use one from core, to avoid depending on physics.
        # https://github.com/PixarAnimationStudios/USD/commit/6f0ce585bf5d06ca929584b515cd3bdf05d78eb3
        # https://github.com/PixarAnimationStudios/USD/commit/1222ea7cd2478e576f4fc32936f781d2a7f61cd5
        # https://github.com/PixarAnimationStudios/USD/commit/61fcc34d8555bb269fae49af90eab5154989392c
        if hasattr(UsdGeom, 'VisibilityAPI'):
            UsdGeom.VisibilityAPI.Apply(stage.GetPrimAtPath("/World/Torus"))
        else:
            from pxr import UsdRender
            UsdRender.SettingsAPI.Apply(stage.GetPrimAtPath("/World/Torus"))
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "normalsWithApiSchema.png", 3e-4)

    async def test_UNSTABLE_tetMesh_UNSTABLE(self):
        """
        Test hydra tetmesh
        """

         # Eventually this should live in the physics repo, when omni hydra supports plugin adapters.
        super().open_usd("hydra/tetmesh_teddy.usda")
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "tetmesh_teddy.png", 3e-4)

    async def test_mesh_dedup(self):
        """
        Verify hash collisions in mesh dedup do not crash
        """
        # OM-64181: Verify hash collisions in mesh dedup do not crash
        timeline = omni.timeline.get_timeline_interface()
        timeline.set_current_time(0)
        super().open_usd("hydra/AlternatingCubePlane.usda")
        await wait_for_update()

        N = 50
        while N > 0:
            N = N - 1
            timeline.forward_one_frame()
            await wait_for_update()
            timeline.rewind_one_frame()
            await wait_for_update()


class TestRtxHydraMeshMaterials(RtxHydraMaterialsTest):
    TEST_PATH = "hydra/mesh/material"
    PRIM_PATH = "/World/box"

    def create_geometry(self, name=PRIM_PATH):
        box = UsdGeom.Mesh.Define(self.ctx.get_stage(), name)
        box.CreatePointsAttr([(-50, -50, -50), (50, -50, -50), (-50, -50, 50), (50, -50, 50),
                              (-50, 50, -50), (50, 50, -50), (50, 50, 50), (-50, 50, 50)])
        box.CreateFaceVertexCountsAttr([4, 4, 4, 4, 4, 4])
        box.CreateFaceVertexIndicesAttr([0, 1, 3, 2, 0, 4, 5, 1, 1, 5, 6, 3, 2, 3, 6, 7, 0, 2, 7, 4, 4, 7, 6, 5])
        box.CreateSubdivisionSchemeAttr("none")
        return box

    async def test_materials(self):
        """
        Test hydra mesh - materials
        """
        materials = ["Green", "Red"]
        looksPath = "/World/Looks/"
        # Set and change material
        geomPrim = self.create_geometry().GetPrim()
        for m in materials:
            self.bind_material(geomPrim, looksPath + m)
            await wait_for_update()
            await self.capture_and_compare(self.TEST_PATH, "mat_{}.png".format(m))

        # Unbind material it should fallback to basic material
        UsdShade.MaterialBindingAPI(geomPrim).UnbindAllBindings()
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "mat_base.png")
        self.ctx.get_stage().RemovePrim(self.PRIM_PATH)

        # Subsets
        subsetPath = self.PRIM_PATH + "/subset"
        geomPrim = self.create_geometry().GetPrim()
        subset = UsdGeom.Subset.Define(self.ctx.get_stage(), subsetPath)

        # https://github.com/PixarAnimationStudios/USD/commit/c1cdbbaa8a2dd8ecbb9722de517ef44c1a680352
        # Family name required for subset material bindingas as of USD 21.11+;
        # has been available on subsets from the firsrt public release of USD.
        subset.CreateFamilyNameAttr().Set(UsdShade.Tokens.materialBind)

        attr = subset.CreateIndicesAttr()
        attr.Set([5])
        self.bind_material(geomPrim, looksPath + materials[0])
        self.bind_material(subset.GetPrim(), looksPath + materials[1])
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "mat_subsets.png")

        # Update subset
        attr.Set([3])
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "mat_subsets2.png")

        # Remove subset
        self.ctx.get_stage().RemovePrim(subsetPath)
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "mat_Green.png")


class TestRtxHydraMeshInstancer(RtxHydraInstancerTest):
    # TODO full cover scene instancer with tests
    TEST_PATH = "hydra/mesh/instancer"
    GEOM_PATH = RtxHydraInstancerTest.PRIM_PATH + "/box"

    def create_geometry(self, name=GEOM_PATH):
        box = UsdGeom.Mesh.Define(self.ctx.get_stage(), name)
        box.CreatePointsAttr([(-50, -50, -50), (50, -50, -50), (-50, -50, 50), (50, -50, 50),
                              (-50, 50, -50), (50, 50, -50), (50, 50, 50), (-50, 50, 50)])
        box.CreateFaceVertexCountsAttr([4, 4, 4, 4, 4, 4])
        box.CreateFaceVertexIndicesAttr([0, 1, 3, 2, 0, 4, 5, 1, 1, 5, 6, 3, 2, 3, 6, 7, 0, 2, 7, 4, 4, 7, 6, 5])
        box.CreateSubdivisionSchemeAttr("none")
        return box

    async def test_sceneInstancer(self):
        """
        Test hydra mesh scene instancer
        """
        await self.si_all()

        sgInstSettingsKey = "/persistent/omnihydra/useSceneGraphInstancing"
        useSceneGraphInstancing = carb.settings.get_settings().get_as_bool(sgInstSettingsKey)

        try:

            async def nest_ptinst_in_sginst_test(self, enableOmniHydraScenegraphInstancing):
                carb.settings.get_settings().set_bool(sgInstSettingsKey, enableOmniHydraScenegraphInstancing)

                # OM-34669
                # Verify that turning on instanceable on a hierarchy that contains
                # a point instancer does not corrupt parent xforms.
                super().open_usd("hydra/ptinst_inside_sginst.usda")
                await wait_for_update()
                stage = self.ctx.get_stage()
                instance = stage.GetPrimAtPath('/World/Instance')
                instance.SetInstanceable(True)
                await wait_for_update()
                await self.capture_and_compare(self.TEST_PATH, "ptinst_inside_sginst.png")

                # Verify that authoring xforms on instances over internal references refreshes properly.
                set_transform_helper(instance.GetPath(), translate=Gf.Vec3d(200,0,0), euler=Gf.Vec3f(0, 0, -120))
                await wait_for_update()
                await self.capture_and_compare(self.TEST_PATH, "ptinst_inside_sginst_rotated.png")

                # Verify that authoring xforms on instances over external references refreshes properly.
                super().open_usd("hydra/ref_ptinst_inside_sginst.usda")
                await wait_for_update()
                stage = self.ctx.get_stage()
                instance = stage.GetPrimAtPath('/World/Instance')
                set_transform_helper(instance.GetPath(), euler=Gf.Vec3f(0, 0, -30))
                await wait_for_update()
                await self.capture_and_compare(self.TEST_PATH, "ref_ptinst_inside_sginst_rotated.png")

            await nest_ptinst_in_sginst_test(self, False)
            await nest_ptinst_in_sginst_test(self, True)
        finally:
            carb.settings.get_settings().set_bool(sgInstSettingsKey, useSceneGraphInstancing)

        # OM-35643
        # Verify xforms above scenegraph instance roots are honored by omni hydra scenegraph instancing.
        try:
            carb.settings.get_settings().set_bool(sgInstSettingsKey, True)
            super().open_usd("hydra/CubeWorld.usda")
            await wait_for_update()
            stage = self.ctx.get_stage()
            cubeInstance = stage.GetPrimAtPath("/CubeWorld/CubeInstance")
            cubeInstance.SetInstanceable(True)
            await wait_for_update()
            await self.capture_and_compare(self.TEST_PATH, "sginst_parent_xform.png", 1e-4)
            cubeInstance.SetInstanceable(False)
            await wait_for_update()
            await self.capture_and_compare(self.TEST_PATH, "sginst_parent_xform.png", 1e-4)
        finally:
            carb.settings.get_settings().set_bool(sgInstSettingsKey, useSceneGraphInstancing)

        # OM-36724
        # Verify xforms in the presence of nested scenegraph instancing with omni hydra scenegraph instancing enabled
        for testFile in ["hydra/nestedScenegraphInstances.usda", "hydra/nestedScenegraphInstancesOff.usda"]:
            try:
                carb.settings.get_settings().set_bool(sgInstSettingsKey, True)
                super().open_usd(testFile)
                await wait_for_update()
                stage = self.ctx.get_stage()
                cubeInstance = stage.GetPrimAtPath("/World/threeCubes")
                cubeInstance.SetInstanceable(True)
                await wait_for_update()
                await self.capture_and_compare(self.TEST_PATH, "nested_sginst_toggle.png", 1e-4)
                cubeInstance.SetInstanceable(False)
                await wait_for_update()
                await self.capture_and_compare(self.TEST_PATH, "nested_sginst_toggle.png", 1e-4)
                set_transform_helper(cubeInstance.GetPath(), translate=Gf.Vec3d(200,0,0))
                await wait_for_update()
                await self.capture_and_compare(self.TEST_PATH, "nested_sginst_xform_refresh.png", 1e-4)
            finally:
                carb.settings.get_settings().set_bool(sgInstSettingsKey, useSceneGraphInstancing)

    async def test_pointInstancer(self):
        """
        Test hydra mesh point instancer
        """
        await self.pi_all()

        # OM-30658 Verify proper population when prototype is not nested under point instancer
        super().open_usd("hydra/ptinst_unnested_proto.usda")
        await wait_for_update()
        # Raise threshold a bit to account for noise between local runs and TC
        await self.capture_and_compare(self.TEST_PATH, "ptinst_unnested_proto.png", 3e-4)

        # OM-32571 Verify proper population after creating an empty instancer.
        super().open_usd("hydra/cube_ptinst_unpopulated.usda")
        await wait_for_update()
        stage = self.ctx.get_stage()
        p = stage.GetPrimAtPath('/World/output')
        pos = p.GetAttribute('positions1').Get()
        ori = p.GetAttribute('orientations1').Get()
        scl = p.GetAttribute('scales1').Get()
        pi = p.GetAttribute('protoIndices1').Get()
        with Sdf.ChangeBlock():
            p.GetAttribute('positions').Set(pos)
            p.GetAttribute('orientations').Set(ori)
            p.GetAttribute('scales').Set(scl)
            p.GetAttribute('protoIndices').Set(pi)
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "cube_ptinst_unpopulated.png")

        # OM-35779 Verify proper refresh of nested point instancers
        super().open_usd("hydra/nested_ptinst.usda")
        await wait_for_update()
        stage = self.ctx.get_stage()
        prim = stage.GetPrimAtPath("/World/PointInstancer_parent")
        pos = prim.GetAttribute('positions').Get()
        new_pos = list(pos)
        new_pos[0] = Gf.Vec3d(300,300,0)
        prim.GetAttribute('positions').Set(new_pos)
        await wait_for_update()
        await self.capture_and_compare(self.TEST_PATH, "nested_ptinst.png")

        # OM-38291 Verify no crash with full update from Nucleus
        super().open_usd("hydra/OM-38291-one_ptinst.usda")
        await wait_for_update()
        stage = self.ctx.get_stage()

        # Mock a full update from Nucleus live edit by clearing and reloading the layer.
        stage.GetRootLayer().Clear()
        await wait_for_update()
        stage.GetRootLayer().Reload(True)

        await wait_for_update()

        # OM-110897 Verify no crash with nested point instancers with different dimensions
        super().open_usd("hydra/nested_ptinst_extradim.usda")
        await wait_for_update()
        stage = self.ctx.get_stage()
        await wait_for_update()
