Date: Wed, 30 Mar 2022 17:54:57 -0700
From: Bret Victor
Subject: Re: Depth-first when unmatched
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!