Solaris material edits and ProcessPrimResync with "procedural" Hydra adapters

I’ve been running into some problems with a custom Hydra primitive adapter and the default behaviour of ProcessPrimResync. I started a discussion about this on the old USD Google group a couple of months ago, and it unfortunately took me a while to revisit the idea suggested in the reply to my question.

Here is the original thread: https://groups.google.com/g/usd-interest/c/3hyh9VGb-MU/m/_4fPZKy7BQAJ

Here is a summary of my original question: I have a “procedural” Hydra primitive adapter (inheriting from UsdImagingPrimAdapter). The geometry which it renders is generated and cached at render time, i.e. it does not preexist on the USD stage.
ProcessPrimResync is called when editing materials attached to the prim in Solaris, and the default implementation of ProcessPrimResync deletes the prim and re-adds it in Hydra. This is slow since it wipes the cached geometry, so it makes editing materials which are attached to these prims rather painful. I asked what options might be available to mitigate this.

Tom Cauchois replied by suggesting that I can override ProcessPrimResync, and “ignore” the resync under some conditions:
“You’ll need to manually verify that the procedural prim you’re resyncing still exists, still has the right type, and verify the input values if you don’t want to invalidate them, but if that all looks ok you’re probably fine to ignore the resync.”

====

I have now tried playing around with this idea, but I can’t quite get it to work. At first, I could not get the Renderman RIS viewport in Houdini Solaris (19.0.x) to refresh after a material was edited. I was able to correct this for the first edit by manually invoking MarkMaterialDirty inside of my ProcessPrimResync function. However, this somehow doesn’t work beyond the first material edit - subsequent edits won’t be picked up when the prim gets re-rendered. So it would seem that something else needs to be dirtied, but I’m not sure what that might be.

Note that when you alter the composition of a prim, such as by adding a property, that will trigger a “resync”, but when you edit the value of an existing property we call that a “refresh” and it’s much lighter-weight. In that case, we’ll call ProcessPropertyChange on the affected prim adapter; so if you implemented your custom logic in ProcessPrimResync but not ProcessPropertyChange and you only get the first edit, that could be the place you want to start.

For debugging USD change-notice issues in general, I highly recommend setting the environment variable TF_DEBUG=USDIMAGING_CHANGES; we print out as much information as we can about the contents of the change notice and how we’re interpreting them, and that might help you.

Hope that helps!

Hi Tom,

Thank you for your reply. Thus far I have not been able to get this working properly.

Here is my “prototype” implementation of ::ProcessPrimResync (the final version probably needs to be more sophisticated)

void MyAdapter::ProcessPrimResync(const SdfPath& cachePath,
                                    UsdImagingIndexProxy* index)
{
    auto prim = _GetPrim(cachePath);

    if (prim) {
        MarkMaterialDirty(prim, cachePath, index);
        return;
    }
    else {
        _RemovePrim(cachePath, index);
        index->Repopulate(/*cachePath*/cachePath);
    }
}

In my test setup, I have assigned a basic PxrDiffuse material to an instance of my custom primitive (via Solaris), and I also have PxrPrimvar node in my material network which is disconnected at the start. In this state, changing the diffuse colour of the PxrDiffuse material node works correctly, but if I connect the resultRGB output of the PxrPrimvar node to the PxrDiffuse diffuseColor input, the colour turns grey (side note: in a previous message I said that the first material change is picked up correctly, but subsequent ones are not. This was not entirely correct, as some material operations won’t work at all, such as connecting the PxrPrimvar node to the PxrDiffuse, though going in the other direction does work if I change my test setup so that the nodes are connected to begin with). I then lose the ability to change the diffuse colour, even if I disconnect the PxrPrimvar again.

The last few entries of the USD log (enabled via TF_DEBUG=USDIMAGING_CHANGES) for my implementation of the ProcessPrimResync are as follows:

[Refresh Object]: /materials/pxrdiffuse1/pxrdiffuse1.inputs:diffuseColor [ ]
[Refresh Object]: Shader property </materials/pxrdiffuse1/pxrdiffuse1.inputs:diffuseColor> modified; updating material </materials/pxrdiffuse1>.
  - affected prim: </PATH_TO_MY_PRIM>
[Repopulate] Populating </> on stage LOP:rootlayer
[Repopulate] Root path: </materials/pxrdiffuse1>
[Repopulate] Pruned at </materials/pxrdiffuse1> due to prim type <Material>
[Repopulate] 0 variability tasks in worker

The last few entries of the USD log for the default implementation of ProcessPrimResync are as follows:

[Refresh Object]: /materials/pxrdiffuse1/pxrdiffuse1.inputs:diffuseColor [ ]
[Refresh Object]: Shader property </materials/pxrdiffuse1/pxrdiffuse1.inputs:diffuseColor> modified; updating material </materials/pxrdiffuse1>.
[Repopulate] Populating </> on stage LOP:rootlayer
[Repopulate] Root path: </PATH_TO_MY_PRIM>
[Repopulate] Root path: </materials/pxrdiffuse1>
[Repopulate] Pruned at </materials/pxrdiffuse1> due to prim type <Material>
[Add HdPrim Info] </PATH_TO_MY_PRIM> adapter=MY_ADAPTER
[Add dependency] </PATH_TO_MY_PRIM> -> </PATH_TO_MY_PRIM>
[Add HdPrim Info] </materials/pxrdiffuse1> adapter=UsdImagingMaterialAdapter
[Add dependency] </materials/pxrdiffuse1> -> </materials/pxrdiffuse1>
[Add dependency] </materials/pxrdiffuse1/pxrdiffuse1> -> </materials/pxrdiffuse1>
[Add dependency] </materials/pxrdiffuse1/pxrprimvar2> -> </materials/pxrdiffuse1>
[Add dependency] </materials/pxrdiffuse1/pxrdiffuse1_preview> -> </materials/pxrdiffuse1>
[Add dependency] </materials/pxrdiffuse1> -> </PATH_TO_MY_PRIM>
[Repopulate] 2 variability tasks in worker

So it looks like something is missing here.

As far as implementing ::ProcessPropertyChange goes, we do in fact override it. Whenever material changes are made (e.g. connecting the PxrPrimvar to the PxrDiffuse), we see that the function is called with properties such as “inputs:diffuseColor”. However, it’s not really clear to me what we are supposed to do with these.

Hi @tomc - would you have any more information on @fgochez’s last post? Many thanks for your assistance!

Hey all, sorry for the delay. After reading through the problem statement and the UsdImaging edit processing code a few times and writing a wall of text about UsdImaging edit processing, I think I’ve concluded that the issue is that, while changing an attribute connection in USD counts as a resync and a structural change, we want to promote it to a value change in UsdImagingDelegate so that we can send the same kind of invalidation, of asking the renderer to pull a new version of the material network.

It looks like that’s what we were previously doing before this PR: Resync prim path on connection changed by seando-adsk · Pull Request #2017 · PixarAnimationStudios/OpenUSD · GitHub, which promotes a connection change to a resync. I think that PR, in turn, was trying to fix invalidation for material changes causing different primvars to be applied to a gprim, but another way we could accomplish that is by having GprimAdapter::ProcessPropertyChange check if prim.IsA or UsdShadeShader, and if so, speculatively return DirtyPrimvar. If you want to try rolling back the PR and adding the replacement fix in GprimAdapter, I’d welcome that as a PR, but if you’d like to file a github issue with repro steps and let us handle it that’s fine as well. When we triage the github issue I’ll keep in mind that there are a lot of folks watching this thread.

The short version of my wall of text:

UsdImagingDelegate maintains (via tracking Populate and additional AddDependency calls) a map from Usd prims to Hydra prims, which it uses to propagate invalidations. A Usd value edit will cause a ProcessPropertyChange on all hydra prims dependent on the usd prim the value was authored under. A usd resync will cause a ProcessPrimResync on all hydra prims dependent on said usd prim. This mapping is what USDIMAGING_CHANGES is printing out.

ProcessPropertyChange takes (changed Usd prim, affected hydra prim, changed Usd property), and ProcessPrimResync takes (affected hydra prim).

Because material binding resolution is dependent on the existence of relationship targets, and because materials are populated by reference and not by traversal in UsdImagingDelegate, we register a dependency from the UsdShadeMaterial prim to the hydra geometry it’s bound to (in your case, your procedural). In theory, a geometry adapter’s exposure to actual Usd data from the material prim is minimal, so we’d like to turn a material resync into: (1) the geometry adapter marking its material binding dirty and recomputing it (via MarkMaterialDirty); and (2) the geometry adapter populating the new material binding target, if it hasn’t been populated already. This concept was the gist of my earlier comments, but since ProcessPrimResync only takes the path to the hydra prim (in this case, the geometry), we have no way of telling whether the resync was caused by the geometry prim being overwritten or just by a dependent resync from the material. I don’t think it’s practical to compare the entire prim data of your geometry before/after the resync, so handling this at that level would only really make sense if we added new API to UsdImagingDelegate (such as adding a new prim adapter function for dependent resyncs).

I’ll note here that one of the great advantages of UsdImagingStageSceneIndex is that it’s fundamentally better architected to keep data transformations and data dependencies bundled and encapsulated, to avoid problems exactly like this.

Hi Tom, Thank you.
In ProcessPropertyChange, we only know the property name. How can we know the prim the proerty belongs to so we know if it is a UsdShader?