Date: Thu, 31 Mar 2022 13:54:02 -0400
From: Luke Iannini
Subject: Re: Depth-first when unmatched
Yeah that makes perfect sense! The resource registration idea was the direction I was heading in but it seemed fundamental enough to be worth thinking through more generally, and the asymmetric unmatching and matching is probably the missing piece I was needing!

(I was thinking of something like, during construction, register the resource either as a “root” resource (e.g. the display) or a resource that’s a child of another (e.g. the display's framebuffer and the framebuffer’s color-image)

register_root_resource(display)
register_child_resource(display, framebuffer) -- adds framebuffer to a free-list keyed by the display
register_child_resource(framebuffer, image) -- adds image to a free-list keyed by the framebuffer

And then we loop through these and figure out the correct ordering to free things depth-first, but I think your list idea is nice and elegant and achieves the same thing.)

Another questionable idea is still leaning on the Lua GC to work out these dependencies for us (since we're basically building a GC of some kind here), but with the addition of a manual garbage collection call before construction to ensure prompt finalization of any prior resources (which would be slow in a scenario of rapid change, but seems fine here since these resources are only rarely re-evaluated)
This actually seems to work but I'm not sure if it's fully deterministic...
-- Create displays
When (your_area) is the rendering area:
    Call collectgarbage("collect"). -- take out the trash
    Call create_display_object() returning /VkDisplay* display/.
    When (display) is garbage collected [capturing (display)]:
        vkDestroyDisplay(display);
    End
    Claim (display) is a “display”.
End

-- Create framebuffers
When /display/ is a “display":
    Call collectgarbage("collect").
 -- take out the trash
    Call create_framebuffer_object_for_display(display) returning /VkFramebuffer* framebuffer/.
    -- Capture display to ensure display isn't collected before framebuffer
    When (framebuffer) is garbage collected [capturing (display, framebuffer)]:
        vkDestroyFramebuffer(framebuffer);
    End
    Claim (display) has framebuffer (framebuffer).
End

-- Create color images
When /display/ has framebuffer /framebuffer/:
    Call collectgarbage("collect").
 -- take out the trash
    Call create_color_image_for_framebuffer(framebuffer) returning /VkImage* color_image/.
    -- Capture framebuffer to ensure framebuffer isn't collected before color_image
    When (color_image) is garbage collected [capturing (framebuffer, color_image)]:
        vkDestroyImage(color_image);
    End
    Claim (framebuffer) has color image (color_image).
End

On Mar 30, 2022, at 8:54 PM, Bret Victor wrote:

This is very tricky!  But I can think of a way to make it work.  (I think.)

To see the crux of the problem, consider your example, but where the second rule takes a parameter:

When /display/ is a “display", /display/ has some parameter /x/:
    Call create_framebuffer_object_for_display(display) returning /VkFramebuffer* framebuffer/.
    When unmatched [capturing (framebuffer)]:
        vkDestroyFramebuffer(framebuffer);
    End
    Claim (display) has framebuffer (framebuffer).
End

If that parameter changes, we need to reevaluate the body with the new parameter.  "When unmatched" needs to run to clean up the old evaluation before we run the new evaluation -- that's what it's for.  (To destroy the old framebuffer before creating the new framebuffer.).  But it's only after the new evaluation has run that we know if the resulting claims have changed, and only then can any rules downstream (such as your third rule) react to those new claims.

What we want is:
 1. The claim about the framebuffer goes away, which causes:
 2. The claim about the image goes away.
 3. The image is destroyed.
 4. The framebuffer is destroyed.
 5. The new framebuffer is created, and claimed, which causes:
 6. The new image is created and claimed.

With the current reactor, this is impossible, because (1,4,5) always happen back-to-back.  There's no way to "postpone" 4 and 5.

An idea for postponing 4:  in the finalizer ("when unmatched"), instead of directly destroying the resource, enqueue the destructor to be called later at some low priority.

A (weird) idea for postponing 5:  give the rule two priorities, one for matching and one for unmatching.  (In reactor terms, one priority for responding to "+" events, and one for responding to "-" events.)  If the matching priority is low and the unmatching priority is normal, then when the parameter changes, the rule will just unmatch and its claims will simply go away, then everyone else will respond to that, and then later the rule will rematch with the new parameter and make its new claims.

Essentially, everyone gets to see this rule naked while it's changing.  We don't normally want this in Realtalk, but I don't think it violates Realtalk semantics because it all happens within the tick.

I've sketched an implementation below.  It's verbose, but there could be special syntax if this is a common pattern.

I need to think about this a bit more before committing to it, but look through your code and let me know if this would work for what you're planning.  (Or if it even makes sense.)


-- Create displays
When (your_area) is the rendering area [matching and unmatching with priorities (-100) and (0)]:
    Call create_display_object() returning /VkDisplay* display/.
    register_resource(display)                          -- this runs at priority -100
    When unmatched [capturing (display)]:               -- this runs at priority 0
        unregister_resource(display, vkDestroyDisplay)  -- the destructor is called at priority -99
    End
    Claim (display) is a “display”.
End

-- Create framebuffers
When /display/ is a “display" [matching and unmatching with priorities (-100) and (0)]:
    Call create_framebuffer_object_for_display(display) returning /VkFramebuffer* framebuffer/.
    register_resource(framebuffer)
    When unmatched [capturing (framebuffer)]:
        unregister_resource(framebuffer, vkDestroyFramebuffer)
    End
    Claim (display) has framebuffer (framebuffer).
End

-- Create color images
When /display/ has framebuffer /framebuffer/ [matching and unmatching with priorities (-100) and (0)]:
    Call create_color_image_for_framebuffer(framebuffer) returning /VkImage* color_image/.
    register_resource(color_image)
    When unmatched [capturing (color_image)]:
        unregister_resource(color_image, vkDestroyImage)
    End
    Claim (framebuffer) has color image (color_image).
End


-- Resource destruction

_rt.resources = _rt.resources or {}  -- list of resources in order of registration
_rt.resource_destructors = _rt.resource_destructors or {}

function register_resource (resource)
    table.insert(_rt.resources, resource)  -- append resource to list
end

function unregister_resource (resource, destructor)
    _rt.resource_destructors[resource] = destructor  -- mark resource for destruction
end

When the time is /t/ [converging with priority (-99)]:
    if not next(_rt.resource_destructors) then return end
    for i = #_rt.resources, 1, -1 do  -- go through resources from latest to earliest
        local resource = _rt.resources[i]
        local destructor = resource_destructors[resource]
        if destructor then     -- if it's been unregistered, destroy it and remove it
            destructor(resource)
            table.remove(_rt.resources, i)
        end
    end
    _rt.resource_destructors = {}
End




On Mar 30, 2022, at 10:32 AM, Luke Iannini wrote:

On re-encountering the Vulkan construction/destruction conundrum I had an idea I can’t remember if we’ve discussed before (apologies if so!)

If “When unmatched” was guaranteed to run deterministically "depth-first"...

i.e. I could write

-- Create displays
When (your_area) is the rendering area:
    Call create_display_object() returning /VkDisplay* display/.
    When unmatched [capturing (display)]:
vkDestroyDisplay(display);
    End
    Claim (display) is a “display”.
End

-- Create framebuffers
When /display/ is a “display":
    Call create_framebuffer_object_for_display(display) returning /VkFramebuffer* framebuffer/.
    When unmatched [capturing (framebuffer)]:
        vkDestroyFramebuffer(framebuffer);
    End
    Claim (display) has framebuffer (framebuffer).
End

-- Create color images
When /display/ has framebuffer /framebuffer/:
    Call create_color_image_for_framebuffer(framebuffer) returning /VkImage* color_image/.
    When unmatched [capturing (color_image)]:
        vkDestroyImage(color_image);
    End
    Claim (framebuffer) has color image (color_image).
End

and I could be sure that when “Create displays” is edited or removed, the When-unmatcheds will run in a deterministic order of “Create color images” -> then “Create framebuffers” -> then “Create displays”, then everything should work perfectly with no further external state tracking needed.

This is basically the Vulkan API decree — that any Object B that is created from Object A must be destroyed before destroying Object A, and I think it’s common enough in resource acquisition/release APIs that this would be a useful property in general.

I don’t have the latest reactor loaded into my brain at the moment so I don’t know how feasible this is!