diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index e4cbec7c90..a8e1880c08 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -502,7 +502,9 @@ impl NodeRuntime { // Graphic run: thumbnail (text-aware bounds, since the `BoundingBox` trait can't lay out `Graphic::Text` content) if type_id == std::any::TypeId::of::() { if update_thumbnails { - // SAFETY: the batch is resident for the read. + // SAFETY: `introspect_with`'s closure is higher-ranked over the batch's + // lifetime, so the item cannot escape this read window, which the + // frames outlive. let item = unsafe { GroupItem::from_resident(batch) }; let bounds = graphene_std::renderer::graphic_list_bounding_box(&RunView::::new(&item)?, DAffine2::IDENTITY); let group = Graphic::Group(Group { row: None, content: item }); @@ -514,7 +516,9 @@ impl NodeRuntime { // clips content to those rectangles so anything outside isn't visible else if type_id == std::any::TypeId::of::() { if update_thumbnails { - // SAFETY: the batch is resident for the read. + // SAFETY: `introspect_with`'s closure is higher-ranked over the batch's + // lifetime, so the item cannot escape this read window, which the + // frames outlive. let item = unsafe { GroupItem::from_resident(batch) }; let run = RunView::::new(&item)?; let bounds = artboard_clip_bounds(&run); @@ -524,7 +528,9 @@ impl NodeRuntime { } // Vector run: vector modifications else if type_id == std::any::TypeId::of::() { - // SAFETY: the batch is resident for the read. + // SAFETY: `introspect_with`'s closure is higher-ranked over the batch's + // lifetime, so the item cannot escape this read window, which the + // frames outlive. let item = unsafe { GroupItem::from_resident(batch) }; let run = RunView::::new(&item)?; use graphene_std::core_types::lane::LaneSource; @@ -534,7 +540,9 @@ impl NodeRuntime { // String run: thumbnail (bounds need text layout, which the `BoundingBox` trait can't do for a bare `String`) else if type_id == std::any::TypeId::of::() { if update_thumbnails { - // SAFETY: the batch is resident for the read. + // SAFETY: `introspect_with`'s closure is higher-ranked over the batch's + // lifetime, so the item cannot escape this read window, which the + // frames outlive. let item = unsafe { GroupItem::from_resident(batch) }; let run = RunView::::new(&item)?; let bounds = graphene_std::renderer::text_list_bounding_box(&run, DAffine2::IDENTITY); diff --git a/node-graph/libraries/core-types/src/record/run.rs b/node-graph/libraries/core-types/src/record/run.rs index de34627934..652be05784 100644 --- a/node-graph/libraries/core-types/src/record/run.rs +++ b/node-graph/libraries/core-types/src/record/run.rs @@ -248,8 +248,11 @@ impl<'e> GroupItem<'e> { /// A `GroupItem` over the batch's frames, without copying. /// /// # Safety - /// The frames must stay valid for the evaluation. Arena-resident batches - /// qualify, caller stack scratch does not. + /// The item borrows the batch's frames at `'e`, so the frames must stay + /// valid for the whole of `'e` and nothing derived from the item may outlive + /// it. Arena-resident batches span the evaluation; a caller's serve scratch + /// spans only the window it lends the batch for, which is sound exactly when + /// `'e` is bounded to that window. pub unsafe fn from_resident(batch: crate::node::RecordBatch<'e>) -> Self { let layout = batch.layout().clone(); assert_element_glue(&layout);