Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[geometry] Add RenderEngine support for shared_ptr #22353

Conversation

jwnimmer-tri
Copy link
Collaborator

@jwnimmer-tri jwnimmer-tri commented Dec 26, 2024

Towards #5842 and #21968.


This change is Reviewable

@jwnimmer-tri jwnimmer-tri added priority: low release notes: breaking change This pull request contains breaking changes release notes: feature This pull request contains a new feature labels Dec 26, 2024
@jwnimmer-tri jwnimmer-tri force-pushed the geometry-render-engine-shared branch from 833ecd7 to 293d5e5 Compare December 27, 2024 01:50
@jwnimmer-tri jwnimmer-tri force-pushed the geometry-render-engine-shared branch 2 times, most recently from 6e17d8c to 4f40183 Compare January 3, 2025 21:37
@jwnimmer-tri jwnimmer-tri force-pushed the geometry-render-engine-shared branch from 4f40183 to 9416731 Compare January 4, 2025 15:21
Copy link
Collaborator Author

@jwnimmer-tri jwnimmer-tri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+@SeanCurtis-TRI for feature review of the //geometry/... code, please.
+@rpoyner-tri for feature review of the //pydrake/... code, please.

Reviewed 10 of 11 files at r1, 10 of 10 files at r2, 2 of 2 files at r3, 1 of 1 files at r4, all commit messages.
Reviewable status: 1 unresolved discussion, LGTM missing from assignees SeanCurtis-TRI(platform),rpoyner-tri(platform)


a discussion (no related file):
Working

Needs (separate) platform reviewer assigned.

Copy link
Contributor

@SeanCurtis-TRI SeanCurtis-TRI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

//geometry/... reviewed -- I have some open questions.

Reviewed 3 of 11 files at r1, 3 of 10 files at r2, 2 of 2 files at r3, 1 of 1 files at r4, all commit messages.
Reviewable status: 5 unresolved discussions, LGTM missing from assignees rpoyner-tri(platform),SeanCurtis-TRI(platform)


a discussion (no related file):
Is it the case that if a user instantiates a RenderEngine in python and passes it to SceneGraph that the engine instance must be copied? I.e., the instance they had in hand was only the seed for what exists in SceneGraph, but is not a handle to it. That seems slightly unpythonic. Is it obvious to the python user that their instance got copied and operations on the in-hand instance will have no bearing on the in-SceneGraph instance? What if we allowed SceneGraph to take a shared_ptr<RenderEngine>? Would that lead to a more expected pythonic experience?

Admittedly, hanging onto the RenderEngine after creation and messing with it is a bit of an anti-pattern. So, maybe it's not worrying about.


geometry/geometry_state.h line 157 at r4 (raw file):

  }
  DeepCopySharedPtr(DeepCopySharedPtr&& other) noexcept {
    std::swap(value_, other.value_);

nit: I would've expected move semantics more akin to that of shared pointer. shared_ptr would leave other with a null value. Same for move assignment. Is there an explicit reason for this counter-intuitive implementation?


geometry/test/geometry_state_test.cc line 4958 at r4 (raw file):

TEST_F(GeometryStateNoRendererTest, PerceptionRoleWithoutRenderer) {
  const InternalGeometry& geometry = *gs_tester_.GetGeometry(geometries_[0]);
  ASSERT_EQ(gs_tester_.RendererCount(), 0u);

nit: the u is now quite the useless appendage.


geometry/scene_graph.h line 702 at r4 (raw file):

  //@{

  /** Adds a new render engine to this %SceneGraph.

BTW The overload is documented as the "non-copying variant". Is it worth being explicit that the engine gets copied in this main documentation?

@jwnimmer-tri jwnimmer-tri force-pushed the geometry-render-engine-shared branch from 9416731 to ebef3bc Compare January 6, 2025 22:33
Copy link
Collaborator Author

@jwnimmer-tri jwnimmer-tri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 3 of 3 files at r5, all commit messages.
Reviewable status: 2 unresolved discussions, LGTM missing from assignees rpoyner-tri(platform),SeanCurtis-TRI(platform)


geometry/geometry_state.h line 157 at r4 (raw file):

Previously, SeanCurtis-TRI (Sean Curtis) wrote…

nit: I would've expected move semantics more akin to that of shared pointer. shared_ptr would leave other with a null value. Same for move assignment. Is there an explicit reason for this counter-intuitive implementation?

Derp.

Copy link
Collaborator Author

@jwnimmer-tri jwnimmer-tri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewable status: 2 unresolved discussions, LGTM missing from assignees rpoyner-tri(platform),SeanCurtis-TRI(platform)


a discussion (no related file):

Previously, SeanCurtis-TRI (Sean Curtis) wrote…

Is it the case that if a user instantiates a RenderEngine in python and passes it to SceneGraph that the engine instance must be copied? I.e., the instance they had in hand was only the seed for what exists in SceneGraph, but is not a handle to it. That seems slightly unpythonic. Is it obvious to the python user that their instance got copied and operations on the in-hand instance will have no bearing on the in-SceneGraph instance? What if we allowed SceneGraph to take a shared_ptr<RenderEngine>? Would that lead to a more expected pythonic experience?

Admittedly, hanging onto the RenderEngine after creation and messing with it is a bit of an anti-pattern. So, maybe it's not worrying about.

I guess I'd say that anyway their engine will be copied at context-creation time, so having it retain reference equality / identity while living in SceneGraph as a kind if "default value" but not while part of the Context didn't seem like an important win. The big picture I'm aiming for "if its destination is eventually the context, then you never get to keep a pointer". Things in the context have value semantics, not identity semantics. (This stands in contract with our recent pointer and memory tweaks for System subclasses, which do not have value semantics.)

If the user wants to keep tabs on all instances of their custom engines, their __init__ and/or __deepcopy__ methods have the power to register their engines with a global pool (e.g., for debugging / introspection).

I do think it's a bug for a user to try to mutate their engine using the retained pointer after adding it to a scene graph. There might be a case for granting them const access, but mutable access into internal geometry state smells wrong, and in Python we can't really enforce constness so it seems like a bad idea to allow them to keep a pointer to the engine they gave us.

I suppose the best reason to avoid the extra copy would be for performance, but I'd hope an empty engine would be relatively cheap to copy? The argument passed to AddRenderEngine should nominally always be empty (no geometry added yet).

Copy link
Collaborator Author

@jwnimmer-tri jwnimmer-tri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewable status: 2 unresolved discussions, LGTM missing from assignees rpoyner-tri(platform),SeanCurtis-TRI(platform)


a discussion (no related file):

Previously, jwnimmer-tri (Jeremy Nimmer) wrote…

I guess I'd say that anyway their engine will be copied at context-creation time, so having it retain reference equality / identity while living in SceneGraph as a kind if "default value" but not while part of the Context didn't seem like an important win. The big picture I'm aiming for "if its destination is eventually the context, then you never get to keep a pointer". Things in the context have value semantics, not identity semantics. (This stands in contract with our recent pointer and memory tweaks for System subclasses, which do not have value semantics.)

If the user wants to keep tabs on all instances of their custom engines, their __init__ and/or __deepcopy__ methods have the power to register their engines with a global pool (e.g., for debugging / introspection).

I do think it's a bug for a user to try to mutate their engine using the retained pointer after adding it to a scene graph. There might be a case for granting them const access, but mutable access into internal geometry state smells wrong, and in Python we can't really enforce constness so it seems like a bad idea to allow them to keep a pointer to the engine they gave us.

I suppose the best reason to avoid the extra copy would be for performance, but I'd hope an empty engine would be relatively cheap to copy? The argument passed to AddRenderEngine should nominally always be empty (no geometry added yet).

But anyway, you're right that this is one of the key questions to decide in this pull request, so I'm open to more discussion with you and always want to hear what Rico things. I'd rather take our time and get this right.

Copy link
Contributor

@SeanCurtis-TRI SeanCurtis-TRI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:LGTM: on the //geometry/... stuff.

Reviewed 3 of 3 files at r5, all commit messages.
Reviewable status: 2 unresolved discussions, LGTM missing from assignee rpoyner-tri(platform)


a discussion (no related file):

Previously, jwnimmer-tri (Jeremy Nimmer) wrote…

But anyway, you're right that this is one of the key questions to decide in this pull request, so I'm open to more discussion with you and always want to hear what Rico things. I'd rather take our time and get this right.

In some ways, I wonder if life wouldn't have been better if the SceneGraph API had simply taken the RenderEngine factory function instead of the instance. :)

I believe your hypothesis is correct that copying an empty engine should be cheap. The only cost is the infrastructure. RenderEngineVtk recreates all of the VTK pipeline artifacts. RenderEngineGl....shares the context? Something like that. So, performance at engine registration is probably not the end of the world.

That said, changing the SceneGraph api from const RenderEngine& renderer to std::shared_ptr<RenderEngine> renderer seems like an unfettered win. It completely eliminates the copy, regardless and, generally, I think we've taught our users that all configuration (geometry, etc.) should pass through SceneGraph. So, the idea that they have a handle on an engine stored in the model would only be used in the most self-flagellating circumstances. I like the idea of not having to ask the question, "is the copy expensive", at all.

tl;dr

Assuming there are no hidden traps in upstream library resource allocation, copying empty RenderEngine should be an acceptable mechanism to ensure user's can't even try to mutate SceneGraph internals directly.

At the same time, I'm ok with simply telling them not to do that in favor of the benefits.

I'm good either way.

Copy link
Collaborator Author

@jwnimmer-tri jwnimmer-tri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewable status: 2 unresolved discussions, LGTM missing from assignee rpoyner-tri(platform)


a discussion (no related file):

Previously, SeanCurtis-TRI (Sean Curtis) wrote…

In some ways, I wonder if life wouldn't have been better if the SceneGraph API had simply taken the RenderEngine factory function instead of the instance. :)

I believe your hypothesis is correct that copying an empty engine should be cheap. The only cost is the infrastructure. RenderEngineVtk recreates all of the VTK pipeline artifacts. RenderEngineGl....shares the context? Something like that. So, performance at engine registration is probably not the end of the world.

That said, changing the SceneGraph api from const RenderEngine& renderer to std::shared_ptr<RenderEngine> renderer seems like an unfettered win. It completely eliminates the copy, regardless and, generally, I think we've taught our users that all configuration (geometry, etc.) should pass through SceneGraph. So, the idea that they have a handle on an engine stored in the model would only be used in the most self-flagellating circumstances. I like the idea of not having to ask the question, "is the copy expensive", at all.

tl;dr

Assuming there are no hidden traps in upstream library resource allocation, copying empty RenderEngine should be an acceptable mechanism to ensure user's can't even try to mutate SceneGraph internals directly.

At the same time, I'm ok with simply telling them not to do that in favor of the benefits.

I'm good either way.

My main worry with taking a shared_ptr<RenderEngine> in the C++ API was that we'd need to write the API documentation "no really, you must stop using this engine now, please pinky swear" which is somewhat at odds with what shared_ptr<> usually denotes. Having the "stop using it" be absolutely enforced by cloning seemed safest and more natural.

I'll wait for Rico to weigh in before changing anything.

Copy link
Contributor

@SeanCurtis-TRI SeanCurtis-TRI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewable status: 2 unresolved discussions, LGTM missing from assignee rpoyner-tri(platform)


a discussion (no related file):

Previously, jwnimmer-tri (Jeremy Nimmer) wrote…

My main worry with taking a shared_ptr<RenderEngine> in the C++ API was that we'd need to write the API documentation "no really, you must stop using this engine now, please pinky swear" which is somewhat at odds with what shared_ptr<> usually denotes. Having the "stop using it" be absolutely enforced by cloning seemed safest and more natural.

I'll wait for Rico to weigh in before changing anything.

I buy that argument. It would put both c++ and python in the same pinky-swear space. In the absence of unique pointers (or simply making sure the pointer is never accessible in the first place), copying is the only protection we have left. I'm sold.

Copy link
Contributor

@rpoyner-tri rpoyner-tri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:lgtm: pydrake stuff.

Reviewed 3 of 10 files at r2.
Reviewable status: 3 unresolved discussions


bindings/pydrake/geometry/geometry_py_render.cc line 82 at r5 (raw file):

    // can post-process the return value.
    auto make_python_deepcopy = [&]() -> py::object {
      PYBIND11_OVERLOAD_INT(py::object, Base, "DoClone");

nit It's probably worth noting to the casual reader that control will fall through this macro if there is no suitable DoClone method. That wasn't obvious to me until I went and read the macro.

Copy link
Contributor

@rpoyner-tri rpoyner-tri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 1 of 10 files at r2.
Reviewable status: 3 unresolved discussions

Copy link
Contributor

@rpoyner-tri rpoyner-tri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed all commit messages.
Reviewable status: 3 unresolved discussions

Because we allow implementations of RenderEngine as Python subclasses,
we cannot assume that the deleter associated with a call to Clone is
`delete get()`, i.e., the return type of unique_ptr<RenderEngine> is
incompatible with Python implementations.

Instead, we add a template argument to Clone() to select between
unique_ptr and shared_ptr (retaining the default of unique_ptr), add
another NVI hook for the shared_ptr flavor, switch our bindings to use
shared_ptr, and change all of our internal C++ call sites (just one --
in GeometryState) to opt-in to shared_ptr so that Python subclasses
can be safely manipulated from C++.

We also now allow Python implementations to implement the canonical
pythonic spelling of the "__deepcopy__" method, instead of the weird
DoClone spelling.

Engines subclasses implemented in C++ do not need to change; their
override of NVI DoClone for unique_ptr will keep working indefinitely.

This commit contains one breaking change: If downstream C++ code was
calling RenderEngine::Clone and the runtime type of the object was a
downstream Python subclass of RenderEngine, then the call will now
throw an exception. The C++ call must be updated to opt-in to the
shared_ptr template argument.
Copy link
Collaborator Author

@jwnimmer-tri jwnimmer-tri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 1 of 1 files at r6, all commit messages.
Reviewable status: 1 unresolved discussion, LGTM missing from assignee xuchenhan-tri(platform)


a discussion (no related file):

Previously, jwnimmer-tri (Jeremy Nimmer) wrote…

Working

Needs (separate) platform reviewer assigned.

+@xuchenhan-tri for platform review per schedule, please.


a discussion (no related file):

Previously, SeanCurtis-TRI (Sean Curtis) wrote…

I buy that argument. It would put both c++ and python in the same pinky-swear space. In the absence of unique pointers (or simply making sure the pointer is never accessible in the first place), copying is the only protection we have left. I'm sold.

Done (no changes).


bindings/pydrake/geometry/geometry_py_render.cc line 82 at r5 (raw file):

Previously, rpoyner-tri (Rick Poyner (rico)) wrote…

nit It's probably worth noting to the casual reader that control will fall through this macro if there is no suitable DoClone method. That wasn't obvious to me until I went and read the macro.

Indeed, my comment paragraph above primed you to expect an unconditional return.

Copy link
Contributor

@SeanCurtis-TRI SeanCurtis-TRI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed all commit messages.
Reviewable status: LGTM missing from assignee xuchenhan-tri(platform)

Copy link
Contributor

@rpoyner-tri rpoyner-tri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 2 of 11 files at r1, 2 of 10 files at r2, 2 of 2 files at r3, 3 of 3 files at r5, 1 of 1 files at r6, all commit messages.
Reviewable status: LGTM missing from assignee xuchenhan-tri(platform)

Copy link
Contributor

@xuchenhan-tri xuchenhan-tri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Platform :lgtm:

Reviewed 2 of 11 files at r1, 5 of 10 files at r2, 2 of 2 files at r3, 3 of 3 files at r5, 1 of 1 files at r6, all commit messages.
Reviewable status: :shipit: complete! all discussions resolved, LGTM from assignees rpoyner-tri(platform),SeanCurtis-TRI(platform),xuchenhan-tri(platform)

@jwnimmer-tri jwnimmer-tri merged commit 2fd6673 into RobotLocomotion:master Jan 8, 2025
10 checks passed
@jwnimmer-tri jwnimmer-tri deleted the geometry-render-engine-shared branch January 8, 2025 21:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
priority: low release notes: breaking change This pull request contains breaking changes release notes: feature This pull request contains a new feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants