Table of Contents

July 2025 newsletter

This past month, we gave a talk about the Folk gadget as part of Recurse Center's Localhost series (more information below):

(If you want to see the latest gadget yourself, our next Folk open house is in the afternoon on Saturday, August 30, in East Williamsburg, Brooklyn.)

Workshop interest form

If you’d be interested in attending some experimental in-person workshops we’re planning, please fill out this form. We’ll post more updates on workshops on the main folk.computer page and our Instagram page as these workshops come together.

What we've been up to

Demos and applications

A new system: folk-cambridge

Hamish Todd set up folk-cambridge in his house:

img_20250713_180748367_hdr.jpg img_20250713_180834483.jpg

Mini PC: Beelink SER8, Ryzen 7 8845HS.

Projector: LG cinebeam set up (upside down though, which I'll correct when the webcam arrives!). The lack of screw holes on it is really annoying, our current solution involves two carabiners and a hairband. But the projection on the whiteboard looks gooooood 🤤

Motivation: my primary work is mathematics, which for years I've done by alternating between two my notebook and my computer. In both I work both symbolically and pictorally, and that means completely different things in both. I want to make it all easier!”|]]

A proposal for a new system

folk2 (parallel Folk evaluator)

Omar: folk2 is finally working enough (my bar was calibration + editor + printing all working, and no blinking on basic workloads) that I made a draft pull request with some basic notes on what's new. (There are quite a few breaking changes.)

I'm very excited to get this through and start using the improved performance: do more in-system editing, refine the new quad APIs, actually work on applications/demos again.

Please test it if you have a Folk system already and have some time!

Calibration works accurately

Inability to calibrate well on folk2 has been the biggest blocker, as has been documented in past months. It's fixed now!

I came into the studio in the middle of the night a few weeks ago and finally made a breakthrough on getting an accurate calibration in folk2 (it was a part that I changed in folk2 and never questioned until then):

This was the first hacky diff to just draw the image over the whole projector area instead of trying to draw the local area of the board:

(we're pretty much still doing this now. it works! I am curious why the local image thing doesn't work, though.)

WebSocket disconnect retracts all statements

Added WsConnection destroy and a WsConnection destructor to retract the socket statement – fixes stuff like when you open /new, create a program, and then close the tab – the program now will get retracted properly on tab close.

(same for opening /calibrate and then closing the tab to kill the calibration)

Camera slices

Started working on porting the When $this has camera slice /slice/ and Wish $this displays camera slice $slice.

There are two parts of it: 1. generating the subimage of the camera frame on demand and 2. supporting churning the GPU texture so you can display the fresh camera slice every frame. Interestingly, both of these are real tests of the statement destructor system in folk2.

For the first part (taking the CPU-side subimage), you'd like to keep the whole camera frame in memory ('pin' it, do not free it) until it's guaranteed that nobody is using any subimage of it, so you can share the RAM and not have to copy the subimage pixels. We're already using a destructor to manage the camera frame, so this is a matter of making sure that the destructor covers the whole lifetime of the subimages too.

For the second part (managing the GPU-side textures), you want to keep the camera slice copied and pinned on the GPU side until it's guaranteed that no one wants to draw it anymore. (Then you can free the GPU-side texture.) In folk1, we actually kind of hacked around this problem by treating GPU textures as a least-recently-used cache, so we'd evict images that hadn't been used for a while. But managing that cache requires more centralization of data than is idiomatic for Folk (folk1 did all this stuff in one big GPU thread), so I want to try doing everything with destructors (and I think it would generally be useful to nail the destructor system)

So, to do both correctly, we need a way for child statements to keep their parents 'alive' (do not fire destructors) as long as needed. Destructors need to be able to outlive their original statement and adhere to descendants of that statement who may still be using the data.

Destructor sets

The idea I came up with is destructor sets.

Every Statement and Match has a destructor set attached to it. When you create a Statement with a destructor (like to close a file or free a heap-allocated block on statement destruction), that destructor is added to the statement's destructor set.

When a Statement is created, it also inherits the destructor set of its parent Match. (and when a Match is created, it inherits the destructor sets of its parent Statements).

A destructor set is a set of references to destructors. Destructors are refcounted. When a Destructor's refcount hits 0, it is fired.

When a statement/match is destroyed, every destructor in its destructor set has refcount decremented.

So a statement might be destroyed but its destructors not fire, because a downstream statement (like a camera slice or a draw command) may still be using the data, which is exactly the behavior we want. We want to pin destructors (and therefore the associated resources) until we're sure nobody downstream is using the resources.

After fixing calibration and camera slices, the last problem is that programs start to blink in and out, especially as you add more load to the table. (We still have this a little).

Recollect/uncollect race

The worst kind of blinking was the persistent blinking-out, where a program would literally vanish and then not come back until you occluded the tag for a second and then brought it back in.

Like, it makes sense that you'd lose a race sometimes and get a temporary blink, but the semi-permanent disappearance is scary – what is the state that's keeping the page disabled? Doesn't the camera get a new frame every frame and we run the tag detector on it?

It turns out that it's the collection of geometries for a given page that was failing. Here's a cut-down excerpt from tags-geometry.folk:

When the default program geometry is /defaultGeom/ &\
     /someone/ wishes /tag/ has resolved geometry {

    When the collected matches for [list /someone/ claims $tag has geometry /geom/] are /matches/ {
        # Choose a geometry.
        if {[llength $matches] == 1} {
            set geom [dict get [lindex $matches 0] geom]
        } elseif {[llength $matches] == 0} {
            set geom $defaultGeom
        } else {
            puts stderr "tags-to-quads: WARNING: Multiple geometries for $tag"
            set geom [dict get [lindex $matches 0] geom]
        }

        # ...
        # This (resolved geometry) feels like a hack.
        Claim $tag has resolved geometry $geom
        # ...
    }
}

There's actually nothing wrong with this code per se. But there was a bug in our implementation of Collect which meant that sometimes you'd just lose the collection, which means you'd lose the Claim $tag has resolved geometry $geom, which would break anything that depends on the geometry to draw (outlines, etc).

The bug was this race:

A solution I put in place (that seems to work so far) is to //always// query for the current set of collectors, before doing the Hold in Recollect, and then not have Uncollect at all, so its behavior is always up to date. Now the persistent disappearance of pages seems to be gone. (It makes sense that the persistent state was in the Hold of collected results that was stuck.)

Only render to textures when needed

Once that was in place, I still had lots of transient blinking when workload was high, so I wanted to do some optimization. The most obvious thing is that we shouldn't have to rerender to textures (pages) that aren't changing, so I implemented that.

Keep statements

I also made the resolved geometry stick around, after realizing that it's the root of a lot of page display behavior (so it going away causes blinking).

Gadget

Omar: In preparation for the Recurse Localhost talk, did a bunch of long-awaited work on the gadget.

New chassis and grip

I printed a new gadget2 chassis in this Retro Platinum PLA (80s Macintosh replica color) that I saw online:

img_2343.jpeg

I designed a new trigger grip (based on this base grip) specifically for the switch we use, so you print most of it in one piece and put the switch in directly (the old one we were using had a block in the middle that you printed separately, and the CAD for it wasn't open-source):

img_2371.jpeg

(ultimately, I want to cover the switch with a trigger button so it doesn't get banged up over time)

so, with the blue panel I printed at Gradient last month, the gadget looks like this:

img_2374.jpeg

Camera slice fix

Camera slices were flickering in weird ways and/or crashing the gadget, as we found out in pre-talk testing (one of the programs we had was a camera-slice program). You can see it a bit here, how the slice just jumps between random images:

It turned out this was because I was mistakenly using width instead of bytesPerRow in subimage, so subimages of subimages (including the subimage we take from the stereo camera frame image) were totally broken. Easy fix (although it's ugly that we duplicate the subimage code in a few places right now):

Fast GPIO reading for trigger button

As a last-minute improvement before showing off the gadget, I wanted to reduce the hiccups in rendering, which were mostly from it exec-ing the gpio program every frame to check if the trigger button is pressed (slow and blocking!)

So I compiled my own wiringOP and wrote a little C binding to read the GPIO pin in-process, which removes all the stuttering!

set cc [c create]
$cc include <wiringPi.h>
$cc proc gpioInit {} void {
    // gpio mode 16 up
    FOLK_ENSURE(wiringPiSetup() != -1);
    pinMode(16, INPUT);
    pullUpDnControl(16, PUD_UP);
}
$cc proc gpioRead {} int {
    // gpio read 16
    return digitalRead(16);
}

c loadlib /home/folk/wiringOP/wiringPi/libwiringPi.so.2.58
$cc compile
exec sudo chmod 666 /dev/mem

gpioInit
When the clock time is /t/ {
    set pressed [expr {![gpioRead]}]
    Hold button \
        {Claim the button is [expr {$pressed ? "pressed" : "unpressed"}]}
}

(it's so common that the most annoying part is getting around Linux permissions, and that was true here too, with no obvious solution for the Orange Pi – I ended up just making /dev/mem world-readable and world-writable, way easier than trying to do it correctly, and we already have password-less sudo on the gadget anyway)

Public repos

Updated the original gadget repo with dual-camera updates.

Finally put up the gadget2 repo, which is most of what I've been working on.

Daniel Pipkin has been working on doing the first independent build of a gadget, which is exciting:

I'm excited to make this something we can make more of / distribute / get out to people. (Making more gadgets will also let us do stuff like the escape-room installation where multiple gadgets are floating around a space.)

Outreach

Recurse Center Localhost talk

As mentioned at the top, we gave a talk about the Folk gadget as part of Recurse Center's Localhost series.

img_2526.jpeg

We also had some fun prepping the week before – our friend Viola He joined:

Other visitors and interactions

What we'll be up to in August

Omar

Andrés

From the community