Date: | Thu, 31 Mar 2022 13:54:02 -0400 |
From: | Luke Iannini |
Subject: | Re: Depth-first when unmatched |
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);EndClaim (display) has framebuffer (framebuffer).EndIf 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 displaysWhen (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 -100When unmatched [capturing (display)]: -- this runs at priority 0unregister_resource(display, vkDestroyDisplay) -- the destructor is called at priority -99EndClaim (display) is a “display”.End-- Create framebuffersWhen /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)EndClaim (display) has framebuffer (framebuffer).End-- Create color imagesWhen /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)EndClaim (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 listendfunction unregister_resource (resource, destructor)_rt.resource_destructors[resource] = destructor -- mark resource for destructionendWhen the time is /t/ [converging with priority (-99)]:if not next(_rt.resource_destructors) then return endfor i = #_rt.resources, 1, -1 do -- go through resources from latest to earliestlocal resource = _rt.resources[i]local destructor = resource_destructors[resource]if destructor then -- if it's been unregistered, destroy it and remove itdestructor(resource)table.remove(_rt.resources, i)endend_rt.resource_destructors = {}EndOn 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 displaysWhen (your_area) is the rendering area:Call create_display_object() returning /VkDisplay* display/.When unmatched [capturing (display)]:vkDestroyDisplay(display);EndClaim (display) is a “display”.End-- Create framebuffersWhen /display/ is a “display":Call create_framebuffer_object_for_display(display) returning /VkFramebuffer* framebuffer/.When unmatched [capturing (framebuffer)]:vkDestroyFramebuffer(framebuffer);EndClaim (display) has framebuffer (framebuffer).End-- Create color imagesWhen /display/ has framebuffer /framebuffer/:Call create_color_image_for_framebuffer(framebuffer) returning /VkImage* color_image/.When unmatched [capturing (color_image)]:vkDestroyImage(color_image);EndClaim (framebuffer) has color image (color_image).Endand 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!