<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Pierre Tempel]]></title><description><![CDATA[Pierre Tempel is Director of Product Management for GitHub's detection and AI-powered remediation engines behind GitHub Advanced Security.]]></description><link>https://pierretempel.com</link><image><url>https://substackcdn.com/image/fetch/$s_!WN8R!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F607f4bab-881b-43dc-9567-7d0f5a697b4d_1280x1280.png</url><title>Pierre Tempel</title><link>https://pierretempel.com</link></image><generator>Substack</generator><lastBuildDate>Thu, 16 Apr 2026 07:37:54 GMT</lastBuildDate><atom:link href="https://pierretempel.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Pierre Tempel]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[mail@pierretempel.com]]></webMaster><itunes:owner><itunes:email><![CDATA[mail@pierretempel.com]]></itunes:email><itunes:name><![CDATA[Pierre]]></itunes:name></itunes:owner><itunes:author><![CDATA[Pierre]]></itunes:author><googleplay:owner><![CDATA[mail@pierretempel.com]]></googleplay:owner><googleplay:email><![CDATA[mail@pierretempel.com]]></googleplay:email><googleplay:author><![CDATA[Pierre]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Fixing SMART PMDA Timeouts]]></title><description><![CDATA[Dude, where are my SMART temps?]]></description><link>https://pierretempel.com/p/fixing-smart-pmda-timeouts</link><guid isPermaLink="false">https://pierretempel.com/p/fixing-smart-pmda-timeouts</guid><dc:creator><![CDATA[Pierre]]></dc:creator><pubDate>Sun, 23 Nov 2025 13:15:42 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Dy4m!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21911050-c8ae-470e-ab0c-dc6422f3a109_2330x990.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I&#8217;ve recently added a bunch of storage to a server: 4 SATA SSDs connected via the MB chipset and 8 SATA HDDs connected through a PCIe LSI HBA. All working just fine. But I woke up to this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Dy4m!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21911050-c8ae-470e-ab0c-dc6422f3a109_2330x990.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Dy4m!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21911050-c8ae-470e-ab0c-dc6422f3a109_2330x990.png 424w, https://substackcdn.com/image/fetch/$s_!Dy4m!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21911050-c8ae-470e-ab0c-dc6422f3a109_2330x990.png 848w, https://substackcdn.com/image/fetch/$s_!Dy4m!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21911050-c8ae-470e-ab0c-dc6422f3a109_2330x990.png 1272w, https://substackcdn.com/image/fetch/$s_!Dy4m!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21911050-c8ae-470e-ab0c-dc6422f3a109_2330x990.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Dy4m!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21911050-c8ae-470e-ab0c-dc6422f3a109_2330x990.png" width="1456" height="619" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/21911050-c8ae-470e-ab0c-dc6422f3a109_2330x990.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:619,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:269881,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://pierretempel.com/i/179717176?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21911050-c8ae-470e-ab0c-dc6422f3a109_2330x990.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Dy4m!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21911050-c8ae-470e-ab0c-dc6422f3a109_2330x990.png 424w, https://substackcdn.com/image/fetch/$s_!Dy4m!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21911050-c8ae-470e-ab0c-dc6422f3a109_2330x990.png 848w, https://substackcdn.com/image/fetch/$s_!Dy4m!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21911050-c8ae-470e-ab0c-dc6422f3a109_2330x990.png 1272w, https://substackcdn.com/image/fetch/$s_!Dy4m!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21911050-c8ae-470e-ab0c-dc6422f3a109_2330x990.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>On the left, we have lmsensors metrics, expanded with it86 because my MB uses one of those blasted SuperIO chips for fan control (&#8220;SmartFan 6&#8221;). Incidentally, the sensors report temps from the 4 NVMe slots on the MB, each housing an 8TB WD Black drive.</p><p>On the right, data is pulled from SMART for each drive, on any drive port.</p><p>Both are populated by their respective PMDAs, metric agents that are emitting data to PMCD, which in turn is fed through PMLogger to Redis and finally Grafana.</p><p>The SMART data seems to disappear randomly, even though it is forced to refresh every second:</p><pre><code>log mandatory on 1 second {
        nvidia
        smart
        lmsensors
        filesys
}</code></pre><p>This config forces PMCD to poll the PMDA data for those four agents every second. It&#8217;s a bit much, as there&#8217;s some static data in there (you&#8217;re not hot-plugging GPUs every second, right?), but I have enough storage and I would rather not dig through the entire metrics tree. That&#8217;s why the lmsensors graph on the left is so detailed. So WTF is going on with the SMART data? This dashboard was in place before I added the new storage and worked just fine?!</p><p>Let&#8217;s just restart and see what&#8217;s what:</p><pre><code><strong>$ smart sudo systemctl restart pmcd</strong>
<strong>$ smart pmprobe smart.nvme_attributes.composite_temperature</strong>
smart.nvme_attributes.composite_temperature 4</code></pre><p>Ah nice, it works! 4 metrics (for four drives) are flowing in. But wait, a second later:</p><pre><code><strong>$ smart pmprobe smart.nvme_attributes.composite_temperature</strong>
smart.nvme_attributes.composite_temperature -12386 No PMCD agent for domain of request</code></pre><p>The agent disappeared?! What was the last thing it did?</p><pre><code><strong>$ sudo tail -n 50 /var/log/pcp/pmcd/smart.log</strong>
Log for pmdasmart on aitop started Sun Nov 23 12:23:32 2025

[Sun Nov 23 12:23:40.487304] Warning: __pmSendResult(1,...) failed: Broken pipe

Log finished Sun Nov 23 12:23:40 2025</code></pre><p>It failed to send data, the pipe to PMCD was shut down - it ran into a closed door and gave up. Why did PMCD close the door?</p><pre><code><strong>$ sudo tail -n 50 /var/log/pcp/pmcd/pmcd.log</strong>
Configuration file &#8216;/etc/pcp/pmcd/pmcd.conf&#8217; unchanged
Restarting any deceased agents:
    &#8220;smart&#8221; agent
    &#8220;bpftrace&#8221; agent

pmcd: unexpected end-of-file at initial exchange with bpftrace PMDA

active agent dom   pid  in out ver protocol parameters
============ === ===== === === === ======== ==========
[...]
<strong>libvirt</strong>      140 3682865  31  32   2 bin pipe cmd=/usr/bin/python3 /var/lib/pcp/pmdas/libvirt/pmdalibvirt.python
docker       141 3682870  33  34   2 bin pipe cmd=/var/lib/pcp/pmdas/docker/pmdadocker -d 141
<strong>smart</strong>        150 3686839  38  39   2 bin pipe cmd=/var/lib/pcp/pmdas/smart/pmdasmart -d 150

[Sun Nov 23 12:23:38] pmcd(3682845) Warning: pduread: <strong>timeout</strong> (after 5.000000 sec) while attempting to read 12 bytes out of 12 in HDR on fd=39
[Sun Nov 23 12:23:38] pmcd(3682845) Info: CleanupAgent ...
<strong>Cleanup &#8220;smart&#8221; agent (dom 150): protocol failure for fd=39</strong></code></pre><p>OK, multiple things are wrong. First off, there are read errors in the bpftrace and smart agent, they&#8217;d been marked as &#8220;<code>deceased</code>&#8221;, which explains why the PMDA couldn&#8217;t send any data - PMCD killed it. The timeout could be it, but the PID seems to point to libvirt&#8217;s PMDA. Let&#8217;s disable both bpftrace and libvirt metrics to isolate the smart failure:</p><pre><code><strong>$ cd /var/lib/pcp/pmdas/smart
$ cd ../libvirt
$ sudo ./Remove</strong>
Culling the Performance Metrics Name Space ...
libvirt ... done
Updating the PMCD control file, and notifying PMCD ...
Check libvirt metrics have gone away ... OK
<strong>$ cd ../bpftrace
$ sudo ./Remove</strong>
Culling the Performance Metrics Name Space ...
bpftrace ... done
Updating the PMCD control file, and notifying PMCD ...
Check bpftrace metrics have gone away ... OK</code></pre><p>OK, now checking the behavior again, it seems like the SMART probe takes longer and longer to respond, before being killed:</p><pre><code><strong>$ sudo systemctl restart pmcd
$ time pmprobe smart.nvme_attributes.composite_temperature</strong>
smart.nvme_attributes.composite_temperature 4
pmprobe smart.nvme_attributes.composite_temperature  0,00s user 0,00s system 7% cpu 0,052 total
<strong>$ time pmprobe smart.nvme_attributes.composite_temperature</strong>
smart.nvme_attributes.composite_temperature 4
pmprobe smart.nvme_attributes.composite_temperature  0,00s user 0,00s system 6% cpu 0,051 total
<strong>$ time pmprobe smart.nvme_attributes.composite_temperature</strong>
smart.nvme_attributes.composite_temperature 4
pmprobe smart.nvme_attributes.composite_temperature  0,00s user 0,00s system 0% cpu <strong>4,838 total</strong>
<strong>$ time pmprobe smart.nvme_attributes.composite_temperature</strong>
smart.nvme_attributes.composite_temperature -12386 No PMCD agent for domain of request
pmprobe smart.nvme_attributes.composite_temperature  0,00s user 0,00s system 0% cpu 3,467 total
<strong>$ sudo tail -n 50 /var/log/pcp/pmcd/pmcd.log</strong>
[Sun Nov 23 13:19:49] pmcd(3857845) Info:

pmcd RESTARTED at Sun Nov 23 13:19:49 2025

<strong>Restarting any deceased agents:
    &#8220;smart&#8221; agent</strong>

[...]

[Sun Nov 23 13:19:49] pmcd(3857845) Info: PMNS file &#8220;DEFAULT&#8221; is unchanged
[Sun Nov 23 13:19:55] pmcd(3857845) Warning: <strong>pduread: timeout (after 5.000000 sec)</strong> while attempting to read 12 bytes out of 12 in HDR on fd=34
[Sun Nov 23 13:19:55] pmcd(3857845) Info: CleanupAgent ...
<strong>Cleanup &#8220;smart&#8221; agent (dom 150): protocol failure for fd=34</strong></code></pre><p>OK that starts to make sense: the last successful probe took <em>nearly</em> 5 seconds, and the next one failed. PMCD is enforcing a 5-second timeout, and our polling forces a 1 second refresh. Let&#8217;s change the forced refresh to 10s for the SMART PMDA, and also adjust the PMCD timeout to match:</p><pre><code>log mandatory on 1 second {
        lmsensors
}

log mandatory on 10 second {
        nvidia
        smart
        filesys
}</code></pre><p>And adjust the timeout config in <code>/etc/pcp/pmcd/pmcd.options</code>:</p><pre><code># longer IPC timeout delay for slow agents (default 5)
-t 10</code></pre><p>OK, seems to work:</p><pre><code><strong>$ sudo systemctl restart pmcd
$ pmprobe -v pmcd.control.timeout</strong>
pmcd.control.timeout 1 10
<strong>$ time pmprobe smart.nvme_attributes.composite_temperature</strong>
smart.nvme_attributes.composite_temperature 4
pmprobe smart.nvme_attributes.composite_temperature  0,00s user 0,00s system 0% cpu <strong>5,533 total</strong>
<strong>$ time pmprobe smart.nvme_attributes.composite_temperature</strong>
smart.nvme_attributes.composite_temperature 4
pmprobe smart.nvme_attributes.composite_temperature  0,00s user 0,00s system 4% cpu <strong>0,088 total</strong></code></pre><p>Timing fluctuates a bit, but 10s gives us enough headroom. The reason for the delay is data availability. The default polling is <code>advisory</code> for most metrics, and <code>once</code> for one-offs:</p><pre><code><strong>$ grep &#8216;^log&#8217; /var/lib/pcp/config/pmlogger/config.default | sort | uniq</strong>
log advisory on 5 minutes {
log advisory on default {
log advisory on once {
log mandatory on 10 second {
log mandatory on 1 second {</code></pre><p>I&#8217;m pushing the system a bit with tight mandatory polling, and SMART doesn&#8217;t care how fast you want the data. But this works for now.</p><p>The naming in PMLogger is a bit odd, but one way to think about <code>mandatory</code> vs. <code>advisory</code> is: mandatory is like CSS&#8217;s <code>important</code> - it always wins and sets the specific logging period. PMDAs might provide advisory periods, i.e. if no one requests mandatory logging for this metric, maybe do it every so often (the timing is defined by the advisory). Mandatory also defends against any overrides by PMLC. TL;DR:</p><pre><code>if mandatory == on:
    log it (at mandatory interval)
elif mandatory == off:
    do NOT log it
elif mandatory == maybe:
    if advisory == on:
        log it (at advisory interval)
    else:
        do NOT log it</code></pre><p>One sensible thing to do here would be to specify the metrics I&#8217;m actually interested in. Currently, every 10s, the entire SMART PMDA is polled. But I plan to keep this while I&#8217;m building out the dashboard. After that, I can disable the metrics I&#8217;m not interested in. PMLC is a nice tool for interactive configs, but remember that our PMLogger config overrides this:</p><pre><code><strong>$ pmlc</strong>
Performance Co-Pilot Logger Control (pmlc), Version 7.0.3

<strong>pmlc&gt; show loggers</strong>
The following pmloggers are running on local::
    primary (3898924)
<strong>pmlc&gt; connect primary
pmlc&gt; status</strong>
pmlogger [primary] on host local: is logging metrics from host aitop
PMCD host        aitop
log started      Sun Nov 23 13:30:22.133330434 2025 (times in local time)
last log entry   Sun Nov 23 13:41:47.612559601 2025
current time     Sun Nov 23 13:41:47.912865078 2025
log volume       0
log size         2121756
<strong>pmlc&gt; query smart.nvme_attributes.composite_temperature</strong>
smart.nvme_attributes.composite_temperature
    adv  off nl              [0 or &#8220;sda&#8221;]
    adv  off nl              [1 or &#8220;sdb&#8221;]
    adv  off nl              [2 or &#8220;sdc&#8221;]
    adv  off nl              [3 or &#8220;sdd&#8221;]
    adv  off nl              [4 or &#8220;sde&#8221;]
    adv  off nl              [5 or &#8220;sdf&#8221;]
    adv  off nl              [6 or &#8220;sdg&#8221;]
    adv  off nl              [7 or &#8220;sdh&#8221;]
    adv  off nl              [8 or &#8220;sdi&#8221;]
    adv  off nl              [9 or &#8220;sdj&#8221;]
    adv  off nl              [10 or &#8220;sdk&#8221;]
    adv  off nl              [11 or &#8220;sdl&#8221;]
    adv  off nl              [12 or &#8220;sdm&#8221;]
    adv  off nl              [13 or &#8220;nvme3n1&#8221;]
    adv  off nl              [14 or &#8220;nvme2n1&#8221;]
    adv  off nl              [15 or &#8220;nvme1n1&#8221;]
    adv  off nl              [16 or &#8220;nvme0n1&#8221;]</code></pre><p>A &#8220;<code>query smart</code>&#8221; prompt would also give you a good idea of how long a full tree probe takes.</p><p>To help with debugging issues that happen &#8220;overnight,&#8221; I also want to revisit the retention periods. We have three components in play:</p><ol><li><p>PMCD serves metrics live (in RAM), no retention, what isn&#8217;t logged is lost</p></li><li><p>PMLogger writes metrics to disk (as the name implies)</p></li><li><p>PMProxy + Redis format the data for Grafana as timeseries</p></li></ol><p>2 and 3 have retention periods.</p><p>PMLogger runs a log rotation job, and keeps <code>k</code> days of metrics on disk. This parameter can be changed in XXX:</p><pre><code><strong>$ grep PMLOGGER_INTERVAL /etc/default/pmlogger</strong>
# PMLOGGER_INTERVAL=60
<strong>$ sudo nano /etc/default/pmlogger_timers
</strong># change/add: PMLOGGER_DAILY_PARAMS="-E -x 14 -k 180"
<strong>$ sudo systemctl restart pmlogger</strong></code></pre><p>This means:</p><ul><li><p><code>-E</code>: don&#8217;t bail out on minor metadata issues (default)</p></li><li><p><code>-x 14</code>: compress archives older than 14 days</p></li><li><p><code>-k 180</code>: delete archives older than 180 days</p></li></ul><p>And in <code>/etc/pcp/pmproxy/pmproxy.conf</code>, add, for example:</p><pre><code>[pmseries]
stream.expire = 2592000   # TTL in seconds
stream.maxlen = 43200     # max samples per metric/host</code></pre><p></p>]]></content:encoded></item><item><title><![CDATA[Set NVIDIA GPU Power Limits at Boot]]></title><description><![CDATA[systemd is still better than a fire extinguisher]]></description><link>https://pierretempel.com/p/set-nvidia-gpu-power-limits-at-boot</link><guid isPermaLink="false">https://pierretempel.com/p/set-nvidia-gpu-power-limits-at-boot</guid><dc:creator><![CDATA[Pierre]]></dc:creator><pubDate>Sun, 16 Nov 2025 15:01:35 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/7fa3d2ce-964a-4370-83f4-21fe3e8aa695_4776x3371.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>To the surprise of no one, I do have an &#8220;AI&#8221; workstation at home. Based on a Gigabyte&#8217;s AI TOP platform, which is a weird jailbreak on the AMD TRX50 chipset that turns it into half of a WRX90 by enabling 8-channel memory, it&#8217;s a beast: 512GB of DDR5 ECC system RAM, 4x 8TB NVMe storage, and: a bunch of NVIDIA&#8217;s new-ish Blackwell GPUs.</p><p>The latest additions are all MaxQ RTX Pro 6000 cards, hardware-limited to 300W, with effective air evacuation through the rear of the card, keeping the insane heat generated out of the main area of the system.</p><p>But when I started, Blackwell had just gone to market, and the only option was an OEM PNY version of the Workstation edition. Using the oversized flow-through cooler design from the 5090, it also inherits its biggest liability: 600W of power draw, via a connector that had never been designed for this, and has since <a href="https://gamersnexus.net/gpus/12vhpwr-dumpster-fire-investigation-contradicting-specs-corner-cutting">turned into a fire risk</a>.</p><p>This card is unwieldy, dangerous, and has the worst airflow path for my application. I&#8217;m looking to swap it at some point for a MaxQ, but in the meantime I need the horsepower.</p><p>Fortunately, <code>nvidia-smi</code> does provide a power limit switch:</p><pre><code>$ sudo nvidia-smi -i 1 -pl 300
Power limit for GPU 00000000:81:00.0 was set to 300.00 W from 600.00 W.
All done.</code></pre><p>But I want to minimize the chance of this ever spiking above 300W. So let&#8217;s first create a tool to set a power limit across all GPUs:</p><pre><code>#!/usr/bin/env bash
set -euo pipefail

TARGET_WATTS=300

NVIDIA_SMI=/usr/bin/nvidia-smi

# Wait for GPUs to appear (up to ~30 seconds)
for i in {1..30}; do
    if &#8220;$NVIDIA_SMI&#8221; -L &gt;/dev/null 2&gt;&amp;1; then
        break
    fi
    sleep 1
done

# If still no GPUs, just exit quietly
if ! &#8220;$NVIDIA_SMI&#8221; -L &gt;/dev/null 2&gt;&amp;1; then
    echo &#8220;set-nvidia-power-limit: no GPUs found, exiting&#8221;
    exit 0
fi

# Make sure persistence mode is enabled (helps keep settings)
# to enable, run once at any time: sudo nvidia-smi -pm ENABLED
&#8220;$NVIDIA_SMI&#8221; -pm 1 || true

# Loop over all GPU indices and set the power limit
for idx in $(&#8221;$NVIDIA_SMI&#8221; --query-gpu=index --format=csv,noheader); do
    echo &#8220;Setting power limit for GPU $idx to ${TARGET_WATTS}W&#8221;
    &#8220;$NVIDIA_SMI&#8221; -i &#8220;$idx&#8221; -pl &#8220;$TARGET_WATTS&#8221;
done</code></pre><p>Saved as <code>/usr/local/sbin/set-nvidia-power-limit.sh</code>, then try it once after enabling settings persistence (don&#8217;t forget to set the execute flag):</p><pre><code>$ sudo nvidia-smi -pm ENABLED
$ sudo /usr/local/sbin/set-nvidia-power-limit.sh
Persistence mode is already Enabled for GPU 00000000:41:00.0.
Persistence mode is already Enabled for GPU 00000000:81:00.0.
All done.
Setting power limit for GPU 0 to 300W
Power limit for GPU 00000000:41:00.0 was set to 300.00 W from 300.00 W.
All done.
Setting power limit for GPU 1 to 300W
Power limit for GPU 00000000:81:00.0 was set to 300.00 W from 300.00 W.
All done.</code></pre><p>Nice. Now create a service to run this at boot in <code>/etc/systemd/system/nvidia-power-limit.service</code></p><pre><code>[Unit]
Description=Set NVIDIA GPU power limits at boot
After=multi-user.target
Wants=nvidia-persistenced.service

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/set-nvidia-power-limit.sh

[Install]
WantedBy=multi-user.target</code></pre><p>And enable it:</p><pre><code>$ sudo systemctl daemon-reload
$ sudo systemctl enable nvidia-power-limit.service
$ sudo systemctl start nvidia-power-limit.service
$ systemctl status nvidia-power-limit.service</code></pre><pre><code>&#9675; nvidia-power-limit.service - Set NVIDIA GPU power limits at boot
     Loaded: loaded (/etc/systemd/system/nvidia-power-limit.service; enabled; preset: enabled)
     Active: inactive (dead) since Sun 2025-11-16 15:45:03 CET; 14min ago
    Process: 524590 ExecStart=/usr/local/sbin/set-nvidia-power-limit.sh (code=exited, status=0/SUCCESS)
   Main PID: 524590 (code=exited, status=0/SUCCESS)
        CPU: 204ms

Nov 16 15:45:03 aitop set-nvidia-power-limit.sh[524595]: Persistence mode is already Enabled for GPU 00000000:81:00.0.
Nov 16 15:45:03 aitop set-nvidia-power-limit.sh[524595]: All done.
Nov 16 15:45:03 aitop set-nvidia-power-limit.sh[524590]: Setting power limit for GPU 0 to 300W
Nov 16 15:45:03 aitop set-nvidia-power-limit.sh[524599]: Power limit for GPU 00000000:41:00.0 was set to 300.00 W from&gt;
Nov 16 15:45:03 aitop set-nvidia-power-limit.sh[524599]: All done.
Nov 16 15:45:03 aitop set-nvidia-power-limit.sh[524590]: Setting power limit for GPU 1 to 300W
Nov 16 15:45:03 aitop set-nvidia-power-limit.sh[524601]: Power limit for GPU 00000000:81:00.0 was set to 300.00 W from&gt;
Nov 16 15:45:03 aitop set-nvidia-power-limit.sh[524601]: All done.
Nov 16 15:45:03 aitop systemd[1]: nvidia-power-limit.service: Deactivated successfully.
Nov 16 15:45:03 aitop systemd[1]: Finished nvidia-power-limit.service - Set NVIDIA GPU power limits at boot.</code></pre><p>Done! A little more peace of mind. A Grafana dashboard and alert monitor this as well via pmlogger, the NVIDIA PMDA and PCP/Redis, just to be sure.</p><p></p>]]></content:encoded></item><item><title><![CDATA[CodeQL and the Future of Agentic Security]]></title><description><![CDATA[Watch now (12 mins) | Learn more about CodeQL, Coding Agent security, and Copilot Autofix in this recap from GitHub Universe 2025]]></description><link>https://pierretempel.com/p/universe-2025</link><guid isPermaLink="false">https://pierretempel.com/p/universe-2025</guid><dc:creator><![CDATA[Pierre]]></dc:creator><pubDate>Sat, 01 Nov 2025 01:43:53 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/177703922/630dd6276168997eabbbaa2520bd4d87.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<p>I joined my colleague Bas on stage for GitHub Universe 2025 to recap a year of ships in Code Security and CodeQL and to unveil how we&#8217;re making sure Copilot Coding Agent contributions are secure and high quality.</p>]]></content:encoded></item><item><title><![CDATA[Nim usage on GitHub]]></title><description><![CDATA[How's my favorite language doing?]]></description><link>https://pierretempel.com/p/nim-usage-on-github</link><guid isPermaLink="false">https://pierretempel.com/p/nim-usage-on-github</guid><dc:creator><![CDATA[Pierre]]></dc:creator><pubDate>Tue, 05 Aug 2025 15:51:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!USL5!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5805defa-7810-4931-b8cd-95236954350c_1200x602.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I was digging through some platform data today and thought I&#8217;d take a look at stats for Nim: as of Aug. 2025, there are 29k source repos (no forks, no archives, no spam) that have Nim as a primary or secondary language. Repos created per week have a slight upwards trend (first graph), and the cumulative growth is steady.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!USL5!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5805defa-7810-4931-b8cd-95236954350c_1200x602.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!USL5!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5805defa-7810-4931-b8cd-95236954350c_1200x602.webp 424w, https://substackcdn.com/image/fetch/$s_!USL5!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5805defa-7810-4931-b8cd-95236954350c_1200x602.webp 848w, https://substackcdn.com/image/fetch/$s_!USL5!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5805defa-7810-4931-b8cd-95236954350c_1200x602.webp 1272w, https://substackcdn.com/image/fetch/$s_!USL5!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5805defa-7810-4931-b8cd-95236954350c_1200x602.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!USL5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5805defa-7810-4931-b8cd-95236954350c_1200x602.webp" width="1200" height="602" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5805defa-7810-4931-b8cd-95236954350c_1200x602.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:602,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Screenshot 2025-08-05 at 6&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Screenshot 2025-08-05 at 6" title="Screenshot 2025-08-05 at 6" srcset="https://substackcdn.com/image/fetch/$s_!USL5!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5805defa-7810-4931-b8cd-95236954350c_1200x602.webp 424w, https://substackcdn.com/image/fetch/$s_!USL5!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5805defa-7810-4931-b8cd-95236954350c_1200x602.webp 848w, https://substackcdn.com/image/fetch/$s_!USL5!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5805defa-7810-4931-b8cd-95236954350c_1200x602.webp 1272w, https://substackcdn.com/image/fetch/$s_!USL5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5805defa-7810-4931-b8cd-95236954350c_1200x602.webp 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Jd6L!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96b631db-1651-4f29-afe4-0bc47689a773_1200x607.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Jd6L!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96b631db-1651-4f29-afe4-0bc47689a773_1200x607.webp 424w, https://substackcdn.com/image/fetch/$s_!Jd6L!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96b631db-1651-4f29-afe4-0bc47689a773_1200x607.webp 848w, https://substackcdn.com/image/fetch/$s_!Jd6L!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96b631db-1651-4f29-afe4-0bc47689a773_1200x607.webp 1272w, https://substackcdn.com/image/fetch/$s_!Jd6L!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96b631db-1651-4f29-afe4-0bc47689a773_1200x607.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Jd6L!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96b631db-1651-4f29-afe4-0bc47689a773_1200x607.webp" width="1200" height="607" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/96b631db-1651-4f29-afe4-0bc47689a773_1200x607.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:607,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Screenshot 2025-08-05 at 6&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Screenshot 2025-08-05 at 6" title="Screenshot 2025-08-05 at 6" srcset="https://substackcdn.com/image/fetch/$s_!Jd6L!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96b631db-1651-4f29-afe4-0bc47689a773_1200x607.webp 424w, https://substackcdn.com/image/fetch/$s_!Jd6L!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96b631db-1651-4f29-afe4-0bc47689a773_1200x607.webp 848w, https://substackcdn.com/image/fetch/$s_!Jd6L!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96b631db-1651-4f29-afe4-0bc47689a773_1200x607.webp 1272w, https://substackcdn.com/image/fetch/$s_!Jd6L!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96b631db-1651-4f29-afe4-0bc47689a773_1200x607.webp 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>On GitHub we detect a repo&#8217;s primary language using <a href="https://github.com/github-linguist/linguist">linguist</a> (which can be <a href="https://github.com/github-linguist/linguist/blob/main/docs/overrides.md">customized or overwritten using git attributes</a>). However, many more repos feature more than one language. In orange (57%) are the repos that have a different primary language but feature some Nim, and below that is a breakdown of the most common additional languages:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!H2Le!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70afe327-aaa6-4c1f-bba9-32c02b76485d_1200x582.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!H2Le!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70afe327-aaa6-4c1f-bba9-32c02b76485d_1200x582.webp 424w, https://substackcdn.com/image/fetch/$s_!H2Le!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70afe327-aaa6-4c1f-bba9-32c02b76485d_1200x582.webp 848w, https://substackcdn.com/image/fetch/$s_!H2Le!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70afe327-aaa6-4c1f-bba9-32c02b76485d_1200x582.webp 1272w, https://substackcdn.com/image/fetch/$s_!H2Le!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70afe327-aaa6-4c1f-bba9-32c02b76485d_1200x582.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!H2Le!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70afe327-aaa6-4c1f-bba9-32c02b76485d_1200x582.webp" width="1200" height="582" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/70afe327-aaa6-4c1f-bba9-32c02b76485d_1200x582.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:582,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Screenshot 2025-08-05 at 6&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Screenshot 2025-08-05 at 6" title="Screenshot 2025-08-05 at 6" srcset="https://substackcdn.com/image/fetch/$s_!H2Le!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70afe327-aaa6-4c1f-bba9-32c02b76485d_1200x582.webp 424w, https://substackcdn.com/image/fetch/$s_!H2Le!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70afe327-aaa6-4c1f-bba9-32c02b76485d_1200x582.webp 848w, https://substackcdn.com/image/fetch/$s_!H2Le!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70afe327-aaa6-4c1f-bba9-32c02b76485d_1200x582.webp 1272w, https://substackcdn.com/image/fetch/$s_!H2Le!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70afe327-aaa6-4c1f-bba9-32c02b76485d_1200x582.webp 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!jpr8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F318c9870-e071-4e25-9f38-01ace41c0200_964x742.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!jpr8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F318c9870-e071-4e25-9f38-01ace41c0200_964x742.webp 424w, https://substackcdn.com/image/fetch/$s_!jpr8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F318c9870-e071-4e25-9f38-01ace41c0200_964x742.webp 848w, https://substackcdn.com/image/fetch/$s_!jpr8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F318c9870-e071-4e25-9f38-01ace41c0200_964x742.webp 1272w, https://substackcdn.com/image/fetch/$s_!jpr8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F318c9870-e071-4e25-9f38-01ace41c0200_964x742.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!jpr8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F318c9870-e071-4e25-9f38-01ace41c0200_964x742.webp" width="964" height="742" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/318c9870-e071-4e25-9f38-01ace41c0200_964x742.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:742,&quot;width&quot;:964,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Screenshot 2025-08-05 at 6&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Screenshot 2025-08-05 at 6" title="Screenshot 2025-08-05 at 6" srcset="https://substackcdn.com/image/fetch/$s_!jpr8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F318c9870-e071-4e25-9f38-01ace41c0200_964x742.webp 424w, https://substackcdn.com/image/fetch/$s_!jpr8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F318c9870-e071-4e25-9f38-01ace41c0200_964x742.webp 848w, https://substackcdn.com/image/fetch/$s_!jpr8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F318c9870-e071-4e25-9f38-01ace41c0200_964x742.webp 1272w, https://substackcdn.com/image/fetch/$s_!jpr8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F318c9870-e071-4e25-9f38-01ace41c0200_964x742.webp 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Putting this into perspective, we host<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a> significantly more than 100,000,000 repos that contain JavaScript (without even counting embedded in HTML, TypeScript or other variants). Still, at rank 212 of 711 recognized languages, Nim is slightly more popular than other niche languages, including LiveScript (27k), V (25k), Crystal (22k). OTOH, all of Zig, COBOL (yes, really), AppleScript, Coq, and Ada have a 2x or larger footprint than Nim. Maybe not surprising, given some of those have had a multi-decade headstart.</p><p>I should also mention that this is merely a snapshot. The growth rate for all languages is different, and so are distributions in active contributions. Once a year, we aggregate all of these factors into the Top 10 languages in the Octoverse report. <a href="https://github.blog/news-insights/octoverse/octoverse-2024/#the-most-popular-programming-languages">Needless to say, Nim doesn&#8217;t make the cut.</a></p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Again, source repos only, and also excluding self-hosted GitHub Server instances. This is an undercount, but the ratios are stable.</p><p></p></div></div>]]></content:encoded></item><item><title><![CDATA[Agentic AI & the Future of Coding]]></title><description><![CDATA[I joined Toloka AI's podcast to talk about GitHub's approach to building developer-first AI-powered security tools]]></description><link>https://pierretempel.com/p/agentic-ai-and-the-future-of-coding</link><guid isPermaLink="false">https://pierretempel.com/p/agentic-ai-and-the-future-of-coding</guid><dc:creator><![CDATA[Pierre]]></dc:creator><pubDate>Tue, 24 Jun 2025 01:16:00 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/177705091/37d4a676be211273a39e0921236c27ac.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<p>Original description: Exclusive interview with Pierre Tempel (@turbo), Director of Product at GitHub, diving deep into Copilot Autofix, autonomous coding agents, and how AI is transforming software development.</p><p>Originally published on <a href="https://www.youtube.com/watch?v=Ohrw4M_hIo0">YouTube</a>.</p>]]></content:encoded></item><item><title><![CDATA[Hashing Drawables with Love2D Compute Shaders]]></title><description><![CDATA[Skip expensive passes using a simple one first]]></description><link>https://pierretempel.com/p/hashing-drawables-with-love2d-compute</link><guid isPermaLink="false">https://pierretempel.com/p/hashing-drawables-with-love2d-compute</guid><dc:creator><![CDATA[Pierre]]></dc:creator><pubDate>Fri, 09 May 2025 16:10:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!t2o9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d1ce347-03ab-4697-8499-cd11a15161b1_640x551.gif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a href="https://pierretempel.com/p/finding-true-content-bounds-with">Last time</a> we looked at Love2D 12 compute shaders and basic synchronization primitives to coordinate threads within a work group, each accessing the same four values - a true content bounds (TCB) box - and expanding it if necessary.</p><p>Today, let&#8217;s push synchronization further by coordinating a single outcome for the entire shader run: a hash of the contents of a texture.</p><p>Like TCB, content hashing is important in effect pipeline optimization. TCB allows us to shed useless data and save memory, while content hashing allows us to skip pipeline steps entirely based on whether the content has changed.</p><p>What we&#8217;ll implement today is exact hashing, and it is very sensitive even to small changes, including sub-pixel anti-aliasing, even for seemingly identical code. In a real application, you&#8217;d extend this with additional &#8220;fuzzy&#8221; hashing (color histograms, FFT analysis, etc.) to get even more savings.</p><p>This once again plays into two strengths of compute shaders:</p><ul><li><p>many-to-few calculations, essentially reducing the input (millions of pixels) to just a few bytes of output</p></li><li><p>coordination: the ability to use memory barriers to coordinate between threads</p></li></ul><p>Doing content hashing on the CPU, especially in Lua, would be prohibitively expensive, whereas our hashing shader can comfortably run in real-time.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!t2o9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d1ce347-03ab-4697-8499-cd11a15161b1_640x551.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!t2o9!,w_424,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d1ce347-03ab-4697-8499-cd11a15161b1_640x551.gif 424w, https://substackcdn.com/image/fetch/$s_!t2o9!,w_848,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d1ce347-03ab-4697-8499-cd11a15161b1_640x551.gif 848w, https://substackcdn.com/image/fetch/$s_!t2o9!,w_1272,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d1ce347-03ab-4697-8499-cd11a15161b1_640x551.gif 1272w, https://substackcdn.com/image/fetch/$s_!t2o9!,w_1456,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d1ce347-03ab-4697-8499-cd11a15161b1_640x551.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!t2o9!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d1ce347-03ab-4697-8499-cd11a15161b1_640x551.gif" width="640" height="551" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2d1ce347-03ab-4697-8499-cd11a15161b1_640x551.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:551,&quot;width&quot;:640,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;demo&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="demo" title="demo" srcset="https://substackcdn.com/image/fetch/$s_!t2o9!,w_424,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d1ce347-03ab-4697-8499-cd11a15161b1_640x551.gif 424w, https://substackcdn.com/image/fetch/$s_!t2o9!,w_848,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d1ce347-03ab-4697-8499-cd11a15161b1_640x551.gif 848w, https://substackcdn.com/image/fetch/$s_!t2o9!,w_1272,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d1ce347-03ab-4697-8499-cd11a15161b1_640x551.gif 1272w, https://substackcdn.com/image/fetch/$s_!t2o9!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d1ce347-03ab-4697-8499-cd11a15161b1_640x551.gif 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Before we go to implement the hash, it helps to consider which algorithm we&#8217;ll base our shader on. We need absolutely minimal required math or bit operations, low memory overhead (no large LUTs), and as few sync barriers as possible.</p><p>One good choice is <strong><a href="https://stackoverflow.com/a/52056161">SplitMix</a></strong>, essentially a PRNG. Its core quality is a high degree of <em>avalanche</em>, meaning small changes in the input bits flip lots of output/intermediate bits. SplitMix relies on XORing values, which is one of the least expensive operations possible, and constant factor multiplication, which can also be optimized to more rudimentary operations. The constant factors in SplitMix are chosen to further maximize avalanche.</p><p>Without further ado, here&#8217;s our shader implementation. I&#8217;ll explain the thread synchronization later down below. You&#8217;ll also notice a &#8220;bounds&#8221; parameter. You can ignore this for now; it&#8217;s meant as an entry point for TCB to allow hashing of only the true content of a canvas. Not something we&#8217;ll implement today.</p><pre><code>/* hash_bounds.comp &#8211; 128-bit  */

layout(local_size_x = 16, local_size_y = 16) in;

/* inputs */
/* the canvas */
layout(rgba32f) readonly uniform image2D Src;
/* (minX, minY, maxX, maxY); maxX &lt; 0 -&gt; disabled */
uniform ivec4 Bounds;

/* output */
layout(std430) buffer Hash { uint h[]; };

/* work-group shared totals */
shared uint l0, l1, l2, l3;

void computemain() {
  ivec2 id  = ivec2(gl_GlobalInvocationID.xy);
  ivec2 dim = imageSize(Src);

  /* guard against ceil-dispatch overflow */
  if (id.x &gt;= dim.x || id.y &gt;= dim.y) return;

  /* skip pixels outside user-supplied bounds (if any) */
  if (Bounds.z &gt;= 0) {
      if (id.x &lt; Bounds.x || id.x &gt; Bounds.z ||
          id.y &lt; Bounds.y || id.y &gt; Bounds.w) return;
  }

  /* zero work-group shared memory exactly once */
  if (gl_LocalInvocationIndex == 0u)
      l0 = l1 = l2 = l3 = 0u;

  memoryBarrierShared(); 
  barrier();

  /* per-pixel mix */
  vec4  px = imageLoad(Src, id);
  uvec4 c  = uvec4(round(px * 255.0));

  /*
    These are the two magic multipliers from SplitMix32 &#8212; a compact, 
    well-studied integer hash by Sebastiano Vigna (derived from Guy Steele&#8217;s 
    SplitMix64 constants). They&#8217;re deliberately odd and have good avalanche: 
    flipping a single input bit flips ~16 output bits on average.
  */
  uint m0 = c.r ^ ((uint(id.x) &lt;&lt; 16) | uint(id.y));
  uint m1 = c.g ^ ((uint(id.y) &lt;&lt; 16) | uint(id.x));
  uint m2 = c.b ^ (uint(id.x) * 0x45d9f3bu);
  uint m3 = c.a ^ (uint(id.y) * 0x119de1f3u);

  atomicAdd(l0, m0);
  atomicAdd(l1, m1);
  atomicAdd(l2, m2);
  atomicAdd(l3, m3);

  barrier(); 
  memoryBarrierShared();

  if (gl_LocalInvocationIndex == 0u) {
    atomicAdd(h[0], l0);
    atomicAdd(h[1], l1);
    atomicAdd(h[2], l2);
    atomicAdd(h[3], l3);
  }
}</code></pre><p>First, let&#8217;s talk about Local Data Share, or LDS. LDS is a, well, local data store. It&#8217;s local to the GPU itself (not in VRAM) and enables fast read and write access for smaller amounts of data. Think of it like a really fast, small scratchpad. The per-group memory we allocate (<code>uint</code>s <code>l0</code> through <code>l3</code>) is only visible to the active work group (16x16 threads).</p><p>Our invocation index is a unique location within the current invocation and allows us to pin certain operations to an exact instance. In this case, for invocation <code>0</code> - the first - we use it to zero the memory <em>once</em>. LDS writes aren&#8217;t guaranteed to be flushed to memory immediately, so we do two things:</p><ul><li><p>use <code>memoryBarrierShared</code> to flush the data</p></li><li><p>use <code>barrier</code> to halt <em>all threads</em> until they reach this point</p></li></ul><p>This syncs all 256 threads to the same state. Barrier&#8217;d code ranges should be near-instant; otherwise, synchronization will massively degrade performance. Zeroing memory doesn&#8217;t cost anything.</p><p>But why do we have 4 LDS counters allocated? To ensure any channel in the input image (RGBA) can affect the output hash, we mix each channel&#8217;s data independently and use that to arrive at a 128-bit output hash.</p><p>Atomic operations, we&#8217;ve seen those in the last TCB example, are used to accumulate hash values in the LDS counters.</p><p>BTW, did you notice the different uses of <em>local</em> and <em>global</em> indexes? A local index refers to the current tile, the 2D work group of threads currently running. The global index is unique across <em>all invocations</em> and can be used for more heavy-handed synchronization - or to recover coordinates as we did last time.</p><p>The final batch of atomic adds targets the <code>h</code> array, which is the final output. Here we again need exactly <em>one</em> thread to perform the write. Barriers just before this ensure that the <em>read</em> from LDS contains all threads&#8217; latest data.</p><p>From a high-level perspective of a CPU-side developer, this might look like a lot of synchronization - surely this would lead to contention? But a GPU has such a massively parallel architecture, and LDS access is so fast (single cycle), this is completely inconsequential. In fact, only one thread per tile ever touches VRAM. And the copy-back of data to the CPU is just 128 bits in total.</p><p>Let&#8217;s set up the usual love app:</p><pre><code>function love.conf(t)
  t.version      = &#8220;12.0&#8221;
  t.window.title = &#8220;GPU Canvas Hash Demo&#8221;
  t.highdpi = true
  t.window.width = 500
  t.window.height = 400
end</code></pre><p>And draw a demo:</p><pre><code>local dpi      = love.graphics.getDPIScale()
local WCANVAS  = 512
local HCANVAS  = 512
local WG_SIZE  = 16           -- local_size in hash.comp

-- GPU resources
local canvas = love.graphics.newCanvas(WCANVAS, HCANVAS, {
  format       = &#8220;rgba32f&#8221;,
  computewrite = true
})
local cs   = love.graphics.newComputeShader(&#8221;hash.comp&#8221;)
local buf  = love.graphics.newBuffer(&#8221;uint32&#8221;, 4, { shaderstorage = true })

-- Demo scene state
local time      = 0
local animate   = true
local prevHash  = &#8220;&#8221;
local effectRan = false

-- Draw something (white rotating square + red circle) onto the texture
local function drawScene()
  love.graphics.setCanvas(canvas)
  love.graphics.clear(0, 0, 0, 0)

  -- rotating square
  love.graphics.push()
  love.graphics.translate(WCANVAS / 2, HCANVAS / 2)
  love.graphics.rotate(time)
  love.graphics.setColor(1, 1, 1, 1)
  love.graphics.rectangle(&#8221;fill&#8221;, -64, -64, 128, 128)
  love.graphics.pop()

  -- orbiting circle
  love.graphics.setColor(1, 0, 0, 0.8)
  love.graphics.circle(
    &#8220;fill&#8221;,
    WCANVAS / 2 + math.cos(time * 2) * 150,
    HCANVAS / 2 + math.sin(time * 2) * 150,
    40
  )

  love.graphics.setCanvas()
end

-- calculate pixel-space bounds &amp; hash within them
local function hashCanvas(canvas, rectPx)   
  -- rectPx = {x, y, w, h} in *logical* px
  local Wpix, Hpix = canvas:getPixelDimensions()
  local dpi        = love.graphics.getDPIScale()

  -- convert logical &#8594; physical; build (minX,minY,maxX,maxY)
  local bx, by, bx2, by2
  if rectPx then
    bx  = math.max(0,      math.floor(rectPx.x * dpi))
    by  = math.max(0,      math.floor(rectPx.y * dpi))
    bx2 = math.min(Wpix-1, math.floor((rectPx.x+rectPx.w-1) * dpi))
    by2 = math.min(Hpix-1, math.floor((rectPx.y+rectPx.h-1) * dpi))
  else
    bx, by, bx2, by2 = 0, 0, -1, -1        -- &#8220;disabled&#8221; sentinel
  end

  -- zero output &amp; send uniforms
  buf:setArrayData({0,0,0,0})
  cs:send(&#8221;Src&#8221;,    canvas)
  cs:send(&#8221;Hash&#8221;,   buf)
  cs:send(&#8221;Bounds&#8221;, {bx, by, bx2, by2})
  
  love.graphics.dispatchThreadgroups(
    cs,
    math.ceil(Wpix / WG_SIZE),
    math.ceil(Hpix / WG_SIZE), 
    1
  )

  -- read back 128-bit result
  local raw   = love.graphics.readbackBuffer(buf)
  local bstr  = raw:getString()
  local h0,h1,h2,h3 = love.data.unpack(&#8221;I4I4I4I4&#8221;, bstr)
  return string.format(&#8221;%08x%08x%08x%08x&#8221;, h0,h1,h2,h3)
end


function love.update(dt)
  if animate then time = time + dt end
end

function love.keypressed(k)
  if k == &#8220;space&#8221; then animate = not animate end
end

-- Draw to screen &amp; demonstrate the hash workflow
function love.draw()
  drawScene()

  local hash = hashCanvas(canvas)
  effectRan  = (hash ~= prevHash)
  prevHash   = hash

  -- Present result
  love.graphics.clear(0.15, 0.15, 0.15)

  love.graphics.setColor(1, 1, 1, 1)
  love.graphics.draw(canvas, 32, 32, 0, 1 / dpi, 1 / dpi)

  local yText = 32 + HCANVAS / dpi + 8
  love.graphics.setColor(1, 1, 0, 1)
  love.graphics.print(&#8221;128-bit hash:  &#8220; .. hash, 32, yText)

  love.graphics.setColor(effectRan and {0, 1, 0, 1} or {1, 0, 0, 1})
  love.graphics.print(effectRan and &#8220;-&gt; content changed &#8211; ran expensive effect&#8221;
                                  or &#8220;-&gt; identical &#8211; skipped effect&#8221;,
                      32, yText + 16)

  love.graphics.setColor(1, 1, 1, 1)
  love.graphics.print(&#8221;Press &lt;space&gt; to toggle animation&#8221;, 32, yText + 32)
  local ffps = love.timer.getFPS()
  love.graphics.print(&#8221;Current FPS: &#8220; .. ffps, 32, yText + 32 * 2)
end</code></pre><p>Get the entire code here: <strong><a href="https://github.com/turbo/love12-hash-shader">turbo/love12-hash-shader</a></strong></p>]]></content:encoded></item><item><title><![CDATA[Finding True Content Bounds with Love2D Compute Shaders]]></title><description><![CDATA[Focus compute and render passes on what matters]]></description><link>https://pierretempel.com/p/finding-true-content-bounds-with</link><guid isPermaLink="false">https://pierretempel.com/p/finding-true-content-bounds-with</guid><dc:creator><![CDATA[Pierre]]></dc:creator><pubDate>Sat, 03 May 2025 16:00:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!AK6L!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd146ae0-9a12-49cd-ab22-20c3bb7bd9f4_800x400.gif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>While docs are still being written for Love 12, I set up <a href="https://github.com/turbo/love12-compute">a basic project</a> to demonstrate data transport between GPU and CPU. Today, let&#8217;s build on that and do something useful but not terribly complex: true content bounds (TCB) detection.</p><p>TCB are vital in compositing: there might be expensive shader effects applied to the contents of a <code>Canvas</code> (<code>Texture</code>), but the canvas itself is larger than it needs to be. For example, if we want to apply effects to the drawing operations performed on the main window, we&#8217;d first target them towards a canvas, then apply the effect. But the canvas size is likely much larger than the content, with the rest being transparent nothingness. Dragging this emptiness through the next compositing passes wastes GPU cycles and, more importantly, memory.</p><p>Ideally, we&#8217;d trim the excess at the start of the pipeline, operate on the content only, and then return the composite, with its own bounds and origin offset. This also allows us to cache effect operations based on the content, and avoid recomputing it if the content merely moves or scales.</p><p>Here&#8217;s a visual demonstration of the love app we&#8217;re going to build:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!AK6L!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd146ae0-9a12-49cd-ab22-20c3bb7bd9f4_800x400.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!AK6L!,w_424,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd146ae0-9a12-49cd-ab22-20c3bb7bd9f4_800x400.gif 424w, https://substackcdn.com/image/fetch/$s_!AK6L!,w_848,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd146ae0-9a12-49cd-ab22-20c3bb7bd9f4_800x400.gif 848w, https://substackcdn.com/image/fetch/$s_!AK6L!,w_1272,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd146ae0-9a12-49cd-ab22-20c3bb7bd9f4_800x400.gif 1272w, https://substackcdn.com/image/fetch/$s_!AK6L!,w_1456,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd146ae0-9a12-49cd-ab22-20c3bb7bd9f4_800x400.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!AK6L!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd146ae0-9a12-49cd-ab22-20c3bb7bd9f4_800x400.gif" width="800" height="400" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bd146ae0-9a12-49cd-ab22-20c3bb7bd9f4_800x400.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:400,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;demo tcb&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="demo tcb" title="demo tcb" srcset="https://substackcdn.com/image/fetch/$s_!AK6L!,w_424,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd146ae0-9a12-49cd-ab22-20c3bb7bd9f4_800x400.gif 424w, https://substackcdn.com/image/fetch/$s_!AK6L!,w_848,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd146ae0-9a12-49cd-ab22-20c3bb7bd9f4_800x400.gif 848w, https://substackcdn.com/image/fetch/$s_!AK6L!,w_1272,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd146ae0-9a12-49cd-ab22-20c3bb7bd9f4_800x400.gif 1272w, https://substackcdn.com/image/fetch/$s_!AK6L!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd146ae0-9a12-49cd-ab22-20c3bb7bd9f4_800x400.gif 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>We also need to make sure this works for love apps in 1:1 and HiDPI rendering modes. This is a pretty nasty pitfall, because we&#8217;re crossing the boundary between Love&#8217;s logical pixels and the shader&#8217;s physical pixels!</p><p>Let&#8217;s start by declaring what we need:</p><pre><code>local dpi = love.graphics.getDPIScale()

local canvas  = love.graphics.newCanvas(512, 512, {
  format       = &#8220;rgba32f&#8221;,
  computewrite = true
})

local quad          = love.graphics.newQuad(0, 0, 1, 1, canvas)
local time          = 0
local workGroupSize = 16
local cs            = love.graphics.newComputeShader(&#8221;bounds.comp&#8221;)
local buf           = love.graphics.newBuffer(
  &#8220;uint32&#8221;,
  4,
  { shaderstorage = true }
)</code></pre><p>(Note: in Love 12, <code>Canvas</code> and <code>Image</code> will both become just <code>Texture</code>)</p><p>Our demo rendering area will be 512x512 logical pixels big. For later rendering, we also declare a <code>Quad</code> (so we don&#8217;t have to copy into a &#8220;trimmed&#8221; <code>Canvas</code>), and load our shader. The shader output will be the physical pixel TCB, for which we just need 4 <code>uint</code>s (other types would also work).</p><p><code>workGroupSize</code> determines the 2D edge length of the work group cube in the compute shader. GPUs organize the shader invocations (runs of the main entry point) into <em>work groups</em> of threads. The thread group has 1, 2 or 3 dimensions (the missing ones default to 1). We&#8217;ll work with a 16x16 2D work group size, which means that 256 threads invoke the main entry point in parallel. The GPU hardware can then also split these threads across cores, often in smaller packages of 32/64, etc. Threads within the same work group can also utilize synchronization primitives to coordinate with each other.</p><p>The group size is set at compile time (you&#8217;ll see later in the shader code), and at runtime we have to make sure to schedule enough <em>groups</em> to cover our data. In our example, we&#8217;re working with image data, which is 2D. So we can choose a 2D work group so that each thread maps to one pixel. The dimensions don&#8217;t have to match, and you need to profile your shader + hardware + data together to find the right parameters.</p><p>Let&#8217;s write the function to invoke our shader:</p><pre><code>function getContentBounds(canvas, dpi)
  local W, H = canvas:getPixelDimensions()

  -- [minX,minY,maxX,maxY] &#8592; [W-1,H-1,0,0]
  buf:setArrayData({ W - 1, H - 1, 0, 0 })

  cs:send(&#8221;Src&#8221;,    canvas)
  cs:send(&#8221;Bounds&#8221;, buf)

  love.graphics.dispatchThreadgroups(
    cs,
    math.ceil(W / workGroupSize),
    math.ceil(H / workGroupSize),
    1
  )

  local raw  = love.graphics.readbackBuffer(buf) -- ByteData
  local data = raw:getString()                   -- 16-byte string

  local minX, minY, maxX, maxY = love.data.unpack(&#8221;I4I4I4I4&#8221;, data)

  if minX &gt; maxX or minY &gt; maxY then return nil end   -- fully transparent

  return minX / dpi, minY / dpi, (maxX - minX + 1) / dpi, (maxY - minY + 1) / dpi
end</code></pre><p>This needs to be DPI-aware. This is done by:</p><ul><li><p>reading the physical pixel dimension of the Canvas, not the logical size</p></li><li><p>scaling the output bounds back to logical pixels using the DPI parameter (e.g. <code>2.0</code> on a Retina display)</p></li></ul><p>Other than that, we pass the parameters to the shader, schedule enough work groups, and parse the output.</p><p>The core idea of the shader is to continually refine the bounds stored in <code>buf</code>. Inside the shader, the <code>buf</code> SSBO is expanded by each execution:</p><pre><code>/* bounds.comp &#8211; find min/max XY of pixels whose alpha &gt; 0 */

layout(local_size_x = 16, local_size_y = 16) in;

/* Texture to scan */
layout(rgba32f) readonly uniform image2D Src;

/* SSBO with four uints: minX, minY, maxX, maxY */
layout(std430) buffer Bounds { uint b[]; };

void computemain()
{
    ivec2 id = ivec2(gl_GlobalInvocationID.xy);
    vec4  p  = imageLoad(Src, id);

    if (p.a &gt; 0.0) {
        atomicMin(b[0], id.x);
        atomicMin(b[1], id.y);
        atomicMax(b[2], id.x);
        atomicMax(b[3], id.y);
    }
}</code></pre><p>What&#8217;s going on here:</p><ul><li><p>declare the work group layout: 2D 16x16</p></li><li><p>declare the readonly input canvas</p></li><li><p>declare the mutable <code>buf</code>, now <code>Bounds</code> as an unsized array</p></li><li><p>the entrypoint to a compute shader is always <code>computemain</code></p></li><li><p><code>gl_GlobalInvocationID</code> is the global 3D-based index of this thread</p><ul><li><p>since our work group dimensions align with the input data (2D image), we can grab the x and y components and treat them as pixel coordinates directly</p></li></ul></li><li><p><code>imageLoad</code> then uses those coordinates to pull from the input</p></li><li><p><code>0.0</code> is the alpha threshold for content</p><ul><li><p>content may be blurred, anti-aliased, etc and <em>any</em> active pixel should count as content</p></li></ul></li><li><p><code>atomicMin</code>/<code>atomicMax</code> are atomic compare-and-replace operations</p><ul><li><p>even though 256 threads run in parallel, they fast-sync on these operations</p></li></ul></li></ul><p>So we move across the image data one 16x16 tile at a time, atomically expanding the TCB box.</p><p>Finally, we can call it and render a demo:</p><pre><code>local function drawToCanvas()
  love.graphics.setCanvas(canvas)
  love.graphics.clear(0, 0, 0, 0)

  -- rotating white square
  love.graphics.push()
    love.graphics.translate(256, 256)
    love.graphics.rotate(time)
    love.graphics.setColor(1, 1, 1, 1)
    love.graphics.rectangle(&#8221;fill&#8221;, -64, -64, 128, 128)
  love.graphics.pop()

  -- moving red circle
  love.graphics.setColor(1, 0, 0, 0.8)
  love.graphics.circle(&#8221;fill&#8221;,
      256 + math.cos(time * 2) * 150,
      256 + math.sin(time * 2) * 150,
      40)

  love.graphics.setCanvas()                -- back to the screen
end

function love.update(dt)
  time = time + dt * 0.25
end

function love.draw()
  love.graphics.clear(0.25, 0.25, 0.25)

  drawToCanvas()

  -- trimmed region
  local x, y, w, h = getContentBounds(canvas, dpi)
  if x then
    quad:setViewport(x, y, w, h)

    love.graphics.setColor(1, 1, 1, 1)
    love.graphics.draw(canvas, quad, 32 + x, 32 + y)

    love.graphics.setColor(1, 0, 1, 1)
    love.graphics.rectangle(&#8221;line&#8221;, 32 + x, 32 + y, w, h)

    love.graphics.setColor(1, 1, 1, 1)
    love.graphics.print(
          (&#8221;trimmed %dx%d @ (%d,%d)&#8221;):format(w, h, x, y),
          32, 16)
  else
    love.graphics.print(&#8221;Canvas fully transparent&#8221;, 32, 16)
  end

  -- full canvas
  love.graphics.setColor(1, 1, 1, 1)
  love.graphics.draw(canvas, 600, 32)
  love.graphics.rectangle(&#8221;line&#8221;, 600, 32,
      canvas:getWidth(), canvas:getHeight())
end</code></pre><p>You can find the full project here: <strong><a href="https://github.com/turbo/love12-true-content-bounds">turbo/love12-true-content-bounds</a></strong> - be sure to play with the <code>highdpi</code> setting in <code>conf.lua</code>!</p><p>There are many ways to calculate TCB, this is but one of them. But it serves as a nice introduction to basic compute shader concepts.</p>]]></content:encoded></item><item><title><![CDATA[A minimal variable font viewer using HarfBuzz]]></title><description><![CDATA[SDL, FreeType, HarfBuzz]]></description><link>https://pierretempel.com/p/a-minimal-variable-font-viewer-using</link><guid isPermaLink="false">https://pierretempel.com/p/a-minimal-variable-font-viewer-using</guid><dc:creator><![CDATA[Pierre]]></dc:creator><pubDate>Fri, 25 Apr 2025 17:51:00 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/9d33954f-8890-432c-abc6-a1ce70d15226_1920x1440.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Supporting a feature request submitted to Love2D, I created an MVP example of how to load variable fonts, detect their (common) axis limits and style sets, and render them with FreeType and SDL3. As you may have noticed, all of those libraries are what Love (12+ anyway) is based on.</p><p>Try it out; it even includes a few example fonts, featuring GitHub&#8217;s very own <em>Mona Sans</em>:</p><p><strong><a href="https://github.com/turbo/harfbuzz-variable">turbo/harfbuzz-variable</a></strong></p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!MOkc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F972f444f-a852-4ab8-8511-e2a40dab2770_640x234.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!MOkc!,w_424,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F972f444f-a852-4ab8-8511-e2a40dab2770_640x234.gif 424w, https://substackcdn.com/image/fetch/$s_!MOkc!,w_848,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F972f444f-a852-4ab8-8511-e2a40dab2770_640x234.gif 848w, https://substackcdn.com/image/fetch/$s_!MOkc!,w_1272,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F972f444f-a852-4ab8-8511-e2a40dab2770_640x234.gif 1272w, https://substackcdn.com/image/fetch/$s_!MOkc!,w_1456,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F972f444f-a852-4ab8-8511-e2a40dab2770_640x234.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!MOkc!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F972f444f-a852-4ab8-8511-e2a40dab2770_640x234.gif" width="640" height="234" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/972f444f-a852-4ab8-8511-e2a40dab2770_640x234.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:234,&quot;width&quot;:640,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;demo&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="demo" title="demo" srcset="https://substackcdn.com/image/fetch/$s_!MOkc!,w_424,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F972f444f-a852-4ab8-8511-e2a40dab2770_640x234.gif 424w, https://substackcdn.com/image/fetch/$s_!MOkc!,w_848,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F972f444f-a852-4ab8-8511-e2a40dab2770_640x234.gif 848w, https://substackcdn.com/image/fetch/$s_!MOkc!,w_1272,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F972f444f-a852-4ab8-8511-e2a40dab2770_640x234.gif 1272w, https://substackcdn.com/image/fetch/$s_!MOkc!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F972f444f-a852-4ab8-8511-e2a40dab2770_640x234.gif 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div>]]></content:encoded></item><item><title><![CDATA[Recreating Serato's spectral waveforms in C]]></title><description><![CDATA[See inside the sound]]></description><link>https://pierretempel.com/p/recreating-seratos-spectral-waveforms</link><guid isPermaLink="false">https://pierretempel.com/p/recreating-seratos-spectral-waveforms</guid><dc:creator><![CDATA[Pierre]]></dc:creator><pubDate>Sat, 12 Apr 2025 17:54:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!QmMx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bbded2a-8e9e-404f-8aa9-813aa153e576_1200x884.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I&#8217;ve been spending a lot of time in Serato Studio lately. Its simplicity is enticing, and I&#8217;m a lifetime license customer (we&#8217;ll see how long a lifetime is for <em>this</em> company).</p><p>If the (audio) job calls for bigger guns, I move to other tools:</p><ul><li><p>Analysis: deCoda, RipX DAW, BeatNet, Acoustica, etc.</p></li><li><p>linear recording: LUNA, Logic Pro</p></li><li><p>Sequencing: Maschine 3</p></li></ul><p>What bugs me about all of them is that the waste of pixels on screen for their waveform rendering. You just get a solid, symmetrical plot - in 2025 (exception: RipX, which is space-age tech)! Sure, you can switch some of them to show spectrograms, but that&#8217;s also not helpful.</p><p>Meanwhile, DJs have enjoyed the best visualization - spectral waveforms - and it hasn&#8217;t made the jump yet to mainstream software.</p><p>So for my own sanity and maybe to inspire someone else, <a href="https://github.com/turbo/libdjwaveform">here&#8217;s libdjwaveform</a>, a minimal C reference implementation using FFTW that can render spectral waveforms with plenty of options.</p><p>Is it production-ready? Well, do you feel lucky?</p><p>Demo output:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!QmMx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bbded2a-8e9e-404f-8aa9-813aa153e576_1200x884.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!QmMx!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bbded2a-8e9e-404f-8aa9-813aa153e576_1200x884.webp 424w, https://substackcdn.com/image/fetch/$s_!QmMx!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bbded2a-8e9e-404f-8aa9-813aa153e576_1200x884.webp 848w, https://substackcdn.com/image/fetch/$s_!QmMx!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bbded2a-8e9e-404f-8aa9-813aa153e576_1200x884.webp 1272w, https://substackcdn.com/image/fetch/$s_!QmMx!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bbded2a-8e9e-404f-8aa9-813aa153e576_1200x884.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!QmMx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bbded2a-8e9e-404f-8aa9-813aa153e576_1200x884.webp" width="1200" height="884" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2bbded2a-8e9e-404f-8aa9-813aa153e576_1200x884.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:884,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;comparison&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="comparison" title="comparison" srcset="https://substackcdn.com/image/fetch/$s_!QmMx!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bbded2a-8e9e-404f-8aa9-813aa153e576_1200x884.webp 424w, https://substackcdn.com/image/fetch/$s_!QmMx!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bbded2a-8e9e-404f-8aa9-813aa153e576_1200x884.webp 848w, https://substackcdn.com/image/fetch/$s_!QmMx!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bbded2a-8e9e-404f-8aa9-813aa153e576_1200x884.webp 1272w, https://substackcdn.com/image/fetch/$s_!QmMx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bbded2a-8e9e-404f-8aa9-813aa153e576_1200x884.webp 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>]]></content:encoded></item><item><title><![CDATA[Advances in AI-Powered Code Security (LLMSA)]]></title><description><![CDATA[I invited Chengpeng's team for a GitHub Day of Learning talk to discuss LLM-powered security analysis]]></description><link>https://pierretempel.com/p/advances-in-ai-powered-code-security</link><guid isPermaLink="false">https://pierretempel.com/p/advances-in-ai-powered-code-security</guid><dc:creator><![CDATA[Pierre]]></dc:creator><pubDate>Sat, 25 Jan 2025 16:23:00 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/177738065/6b81839c6d5784f33228b99a8c83af45.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<p>Joining live us from Purdue University, Chengpeng Wang and Prof. Xiangyu Zhang will share their latest research on AI-powered code security analysis and how their work has led to the discovery of new vulnerabilities in real-world projects. </p><p>Through these discussions, they hope to showcase a vision for static analysis and bug detection, particularly how LLMs can address the challenges of traditional static analysis techniques, including customization, compilation-free analysis, multi-language support, and multi-modal analysis. </p><p>Starting with their previous work on agentic &amp; agentless LLM-powered dataflow modeling and security testing (LLMDFA and LLMSAN), this session is focused on sharing recent advances that have been published in December 2024 as LLMSA: A Compositional Neuro-Symbolic Approach to Compilation-free and Customizable Static Analysis.</p><p>Originally published on <a href="https://www.youtube.com/watch?v=nOS56VC0FTQ">YouTube</a>.</p>]]></content:encoded></item><item><title><![CDATA[How code scanning in GitHub Advanced Security burns down security debt]]></title><description><![CDATA[I joined Microsoft's Reactor live stream series to dive deeper into GitHub Code Security and Copilot Autofix]]></description><link>https://pierretempel.com/p/how-code-scanning-in-github-advanced</link><guid isPermaLink="false">https://pierretempel.com/p/how-code-scanning-in-github-advanced</guid><dc:creator><![CDATA[Pierre]]></dc:creator><pubDate>Wed, 18 Dec 2024 02:06:00 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/177704840/0781c7a0b7b39096a3fc3a368c520d98.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<p>Join me for a demo and GitHub Advanced Security 101 session. You&#8217;ll see how code scanning seamlessly integrates vulnerability prevention and remediation into your development workflow and experience the power of Copilot Autofix, which helps fix vulnerabilities up to 3x faster through AI-powered fix suggestions. These features are designed to enhance collaboration and empower both developers and security professionals to build the best and most secure software.</p><p>Originally streamed/published on <a href="https://www.youtube.com/watch?v=CujF7z8JVnY">YouTube</a>.</p>]]></content:encoded></item><item><title><![CDATA[dmon.nim - cross-platform native directory/file monitoring]]></title><description><![CDATA[Get notified about file changes]]></description><link>https://pierretempel.com/p/dmonnim-cross-platform-native-directoryfile</link><guid isPermaLink="false">https://pierretempel.com/p/dmonnim-cross-platform-native-directoryfile</guid><dc:creator><![CDATA[Pierre]]></dc:creator><pubDate>Wed, 24 Jan 2024 19:45:00 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/d4a29eca-4d14-48e1-85ad-714f10c4b223_1920x1280.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I created a set of minimal Nim bindings for <a href="https://github.com/septag/dmon">dmon</a>: <strong><a href="https://github.com/turbo/dmon.nim">turbo/dmon.nim</a></strong></p><p>dmon is a tiny C library that monitors changes in a directory. It provides a unified solution to multiple system APIs that exist for each OS. It can also monitor directories recursively.</p>]]></content:encoded></item><item><title><![CDATA[Towards full-stack apps with Nim, Alpine and the Nim JS backend]]></title><description><![CDATA[Towards full-stack apps with Nim, Alpine and the Nim JS backend]]></description><link>https://pierretempel.com/p/towards-full-stack-apps-with-nim</link><guid isPermaLink="false">https://pierretempel.com/p/towards-full-stack-apps-with-nim</guid><dc:creator><![CDATA[Pierre]]></dc:creator><pubDate>Wed, 27 Dec 2023 18:32:00 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/444c3e0d-1348-4a71-834e-2316a4180f30_7205x4809.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Nowadays, I&#8217;m building more and more services using the MASH stack: <a href="https://github.com/guzba/mummy">Mummy</a>, <a href="https://alpinejs.dev/">Alpine.js</a>, <a href="https://sqlite.org/index.html">SQLite</a>, and <a href="https://htmx.org/">htmx</a>. This stack provides a performant, pleasant, and straightforward way to develop web applications.</p><p>In my first attempt, <a href="https://summarity.com/findsight">Findsight</a>, the frontend HTML was generated using server-side-rendered <a href="https://github.com/karaxnim/karax">karax</a>. Note that this was not utilizing the karax reactive framework, but its <a href="https://github.com/karaxnim/karax?tab=readme-ov-file#server-side-html-rendering">DOM-builder DSL</a>. However, the Alpine component code, state management, various app startup code, and htmx event handlers were all written using inline JS, which proved to be unwieldy as my projects grew.</p><p>In this article, we&#8217;ll explore how to use Nim as a full-stack language for type safety and maintainability. We&#8217;ll build a complete example of frontend and backend code, which will demonstrate the important concepts to keep in mind when using Nim with Alpine.</p><div><hr></div><h2>Alpine Primer</h2><p>Consider a single binding:</p><pre><code>&lt;<strong>span</strong> x-show="open"&gt;Content&lt;/<strong>span</strong>&gt;</code></pre><p>That&#8217;s fine, as we&#8217;re including the component property <code>open</code> by itself and there&#8217;s no active JS code in here. However, even a tiny component store violates this by including a JS object:</p><pre><code>&lt;<strong>div</strong> x-data="{ open: false }"&gt;</code></pre><p>Or, staying with the same example, an event binding that contains code:</p><pre><code>&lt;<strong>button</strong> x-on:click="open = true"&gt;Expand&lt;/<strong>button</strong>&gt;</code></pre><p>Side note: <code>x-on</code> can be shortened to <code>@</code>.</p><p>These are smaller examples, but in a sufficiently complex app, the complexity of these little inline snippets grows with it.</p><p>Fortunately, Alpine provides a way to extract the component (<code>x-data</code>) into a standalone JS object. Here&#8217;s an example straight from the documentation:</p><pre><code>&lt;<strong>div</strong> x-data="dropdown"&gt;
    &lt;<strong>button</strong> x-on:click="toggle"&gt;...&lt;/<strong>button</strong>&gt;
 
    &lt;<strong>div</strong> x-show="open"&gt;...&lt;/<strong>div</strong>&gt;
&lt;/<strong>div</strong>&gt;
 
&lt;<strong>script</strong>&gt;
    document.addEventListener('alpine:init', () =&gt; {
        Alpine.data('dropdown', () =&gt; ({
            open: <strong>false</strong>,
 
            toggle() {
                <strong>this</strong>.open = ! <strong>this</strong>.open
            }
        }))
    })
&lt;/<strong>script</strong>&gt;</code></pre><p>And <code>x-bind</code> enables the same for any template directive. Both approaches combined keep the HTML clean.</p><p>But now we&#8217;d have to write JavaScript, and I&#8217;d like to stay in Nim. Nim does have a JS backend, and there are just a few steps needed to create those components in pure Nim.</p><h2>Frontend</h2><p>First create a <code>frontend</code> directory. We&#8217;ll put all client-side code in here, which will be compiled to JS from Nim, and then later minified and optimized.</p><h3>Project Structure</h3><pre><code>.
&#9500;&#9472;&#9472; frontend
&#9474;   &#9500;&#9472;&#9472; alpine.nim
&#9474;   &#9500;&#9472;&#9472; app.nimble
&#9474;   &#9500;&#9472;&#9472; app.nim
&#9474;   &#9500;&#9472;&#9472; components.nim
&#9474;   &#9492;&#9472;&#9472; nim.cfg</code></pre><p>The Nim config <code>nim.cfg</code> sets our backend, which is picked up by <code>nimsuggest</code> in VS Code to provide backend-relevant error checks:</p><pre><code>backend:js
-d:release</code></pre><p>It also forces a release build. Nim-compiled JS, depending on the libraries used, is extremely verbose and the debugging info that is generated is less useful in JS code. By compiling in release mode, we end up with more readable JS.</p><p>The resulting JS is still too large to ship directly to users, so we need to optimize it. For this, I&#8217;m using <a href="https://terser.org/">Terser</a>, which is the best standalone optimizing minifier (<a href="https://esbuild.github.io/api/#minify-considerations">esbuild</a> and the <a href="https://github.com/google/closure-compiler#readme">Google Closure Compiler</a> both have similar functionality, but are a less effective). Install it system-wide via:</p><pre><code>npm install -g terser</code></pre><p>Or use <code>npx</code> to invoke it later without prior installation.</p><p>Now add it as a build task to <code>app.nimble</code>:</p><pre><code><strong>import</strong> strformat

bin           = @["app"]
backend       = "js"

requires "nim &gt;= 2.0.0"

after build:
  exec &amp;"terser --compress hoist_funs,keep_fargs=false,unsafe --mangle toplevel {bin[0]}.js -o {bin[0]}.dist.js"</code></pre><p>Other than the default terser options, we enable:</p><ul><li><p>additional compression options</p><ul><li><p><code>hoist_funs</code>: hoists function declarations</p></li><li><p><code>keep_fargs=false</code>: discards unused function arguments (breaks code that relies on Function.length)</p></li><li><p><code>unsafe</code>: enables all <a href="https://terser.org/docs/miscellaneous/#the-unsafe-compress-option">unsafe optimizations</a> (which aren&#8217;t actually that unsafe)</p></li></ul></li><li><p>global name mangling (<code>toplevel</code>) which enables terser to mangle some of the support functions that Nim emits for e.g. string formatting which would otherwise have quite long gensym names</p></li></ul><p>These options are optimized for this use case and will be different if you&#8217;re working on a Nim-JS project that isn&#8217;t Alpine. For example, you wouldn&#8217;t use global name mangling for JS libraries. Since Alpine component code depends on object properties and methods, they&#8217;re unaffected by the mangling algorithm.</p><h3>Component Contract</h3><p>In <code>components.nim</code>, let&#8217;s define the type interface for both the component state (used with <code>Alpine.data</code>) and the behavior (used with <code>x-bind</code>):</p><pre><code><strong>import</strong> std / [
  dom
]

<strong>type</strong>
  DropDown* = <strong>ref</strong> <strong>object</strong>
    init*: proc()
    destroy*: proc()

    open*: bool
    toggle*: proc(msg: cstring)
    timer*: Interval
    counter*: cint
    
  DropButton* = <strong>ref</strong> <strong>object</strong>
    `<strong>type</strong>`* = "button"
    `@click`* {.exportc: "['$1']".}: proc()
    `disabled`* {.exportc: "[':$1']".}: proc(): bool</code></pre><p>To break down what&#8217;s going on here:</p><p><code>std/dom</code> provides us with access to plenty of FFI types for interacting with the browser&#8217;s object implementations.</p><p>The object types are declared as <code>ref</code>, which is the default for JavaScript. Without <code>ref</code>, Nim will use copy semantics, and emit large helper functions for deep-copying, which are not required in this case.</p><p><code>DropDown</code> holds the component state:</p><ul><li><p><code>init</code> and <code>destroy</code> are object methods (in the JS sense) that are invoked by Alpine when the component is created and destroyed</p></li><li><p><code>open</code> will hold the current open/collapsed state</p></li><li><p><code>toggle</code> is the method that will change the <code>open</code> state, and also demos parameter passing to object methods</p></li><li><p><code>timer</code> demos a component property that refers to a native browser type, in this case a time interval</p></li><li><p><code>counter</code> is another property that will be used later to explain object property access from within methods</p></li></ul><p><code>DropButton</code> holds the binding:</p><ul><li><p><code>type</code> is an Alpine attribute that determines the DOM type of the bound element</p></li><li><p><code>@click</code> (or <code>x-on:click</code>) binds to the event (<code>x-on</code>) <code>click</code></p></li><li><p><code>:disabled</code> is a direct attribute binding that maps the given DOM element attribute value to a component method/property</p></li></ul><p>You may notice the <code>exportc</code> pragmas on the last two properties. This is a necessary workaround for Nim&#8217;s name mangling. While some identifiers in Nim can be escaped with <code>name</code>, it&#8217;s not nearly as flexible as JS&#8217;s <code>[&#8217;name&#8217;]</code>, so we have to cheat a bit with <code>exportc</code>&#8216;s built-in string formatting:</p><ul><li><p>For <code>@click</code>, the <code>&#8220;[&#8217;$1&#8217;]&#8221;</code> format produces <code>[&#8217;@click&#8217;]</code></p></li><li><p>For <code>disabled</code>, the <code>[&#8217;:$1&#8217;]</code> format produces <code>[&#8217;:disabled&#8217;]</code></p></li></ul><h3>Alpine Abstraction</h3><p>We&#8217;re almost ready to dive into the component implementation, but we need a few FFI helpers first. In <code>alpine.nim</code>:</p><pre><code><strong>proc</strong> registerComponent*(name: cstring, component: pointer) 
  {.importjs: "Alpine.data(#, #)".}

<strong>proc</strong> bindElement*(binding: cstring, actions: pointer)
  {.importjs: "Alpine.bind(#, #)".}

<strong>proc</strong> env*[T](fn: (proc()), bar: T): (proc())
  {. importjs: "(#).bind(#)" .}

<strong>template</strong> this*(T: typedesc) =
  var this {.importc, nodecl, inject.}: T</code></pre><ul><li><p><code>registerComponent</code> and <code>bindElement</code> map to the respective Alpine functions</p></li><li><p><code>env</code> is a mapping of JavaScript&#8217;s <code>bind</code> function, <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_objects/Function/bind">which allows a caller to set the inner </a><code>this</code> - this may be confusing at first but is required later as we get into closures over object instances from within methods</p></li><li><p><code>this</code> allows us to make Nim aware of the implicit <code>this</code> in a method, and give it a strong type to make <code>nimsuggest</code> and compiler messages more helpful. <code>importc</code> and <code>inject</code> ensure that the code emitted by Nim refers to <code>this</code> literally</p></li></ul><h3>Component Implementation: State</h3><p>In <code>app.nim</code>, we&#8217;ll place the actual implementation, starting with the component state and methods:</p><pre><code><strong>import</strong> std / [
  dom,
  strformat
]

<strong>import</strong> alpine
<strong>import</strong> components

<strong>proc</strong> dropdown(initialOpenState = false): DropDown =
  DropDown(
    init: (proc() =
      this DropDown
      
      this.timer = setInterval(
        (proc() = 
          this DropDown
          inc this.counter
          echo &amp;"Counter is now at {this.counter}"
        ).env(this), <em># explicit binding</em>
        1_000
      )
      
      echo "Component initialized and timer started"
    ),

    destroy: (proc() =
      this DropDown
      clearInterval(this.timer)
    ),

    open: initialOpenState,
    
    toggle: (proc(msg: cstring) =
      this DropDown
      this.open = <strong>not</strong> this.open
      echo &amp;"Parameter passed to toggle: {msg}"
    )
  )</code></pre><p>There&#8217;s a lot more behavior in here than required for a normal dropdown, mostly because I want to show you how to implement other common patterns as well. If you understand what&#8217;s happening in this component, you&#8217;ll be able to create components of virtually any complexity.</p><p>Let&#8217;s start with the proc signature:</p><pre><code><strong>proc</strong> dropdown(initialOpenState = false): DropDown;</code></pre><p><code>Alpine.data</code> (see the example at the beginning of this article) expects a function, which <em>returns</em> a component: a JS object with methods. This is that function. But Alpine also allows parameter passing, usually used for initial state. Here we receive a boolean that indicates the initial state of the dropdown and later copy this into the <code>open</code> property.</p><p>All other properties of the <code>Dropdown</code> component are methods. They&#8217;re declared inline for brevity, but that is not required. All of them open with <code>this Dropdown</code>, which declares the Nim identifier <code>this</code> with the given type.</p><p>In <code>init</code>, on component creation, the <code>timer</code> property is initialized with a new interval timer using the JS function <code>setInterval</code>. Here we have to be careful with scoping. We need to ensure that the inline proc passed to <code>setInterval</code> closes over the correct <code>this</code>. We can&#8217;t use <code>this Dropdown</code> on its own in the timer proc because it is not invoked from the Alpine environment, but from the browser timer callback. Instead, we need to explicitly close over the <em>outer</em> <code>this</code>. There are two ways to do this:</p><ul><li><p>use the <code>env</code> binding we declared before as shown here - this will create a new function that wraps the inner function in a closure environment with the supplied <code>this</code> value</p></li><li><p>declare a new variable in the method that the callback can close over</p></li></ul><p>For the second variant, we&#8217;d have to add a line like <code>var that = this</code> below the <code>this Dropdown</code> in the <code>init</code> proc body, and then refer to <code>that</code> instead of <code>this</code> from the timer callback proc. Personally, I consider <code>env</code> to be a cleaner way of achieving this.</p><p>Other than that, the <code>init</code> method is simple: create a timer that keeps incrementing the <code>counter</code> and print new values to the console (<code>echo</code> becomes <code>console.log</code>). <code>destroy</code> ensures the timer is deregistered.</p><p><code>toggle</code> is the core functionality of flipping the dropdown&#8217;s internal state. It also demonstrates the passing of parameters to component methods.</p><h3>Component Implementation: Binding</h3><p>The last thing we have to implement on the frontend is the binding, to keep the HTML as clean as possible:</p><pre><code><strong>proc</strong> dropbutton(): DropButton =
  DropButton(
    "@click": (proc() =
      this DropDown
      this.toggle("boo!")
    ),

    `disabled`: (proc(): bool =
      this DropDown
      <strong>return</strong> this.counter &gt; 10
    )
  )</code></pre><p>On a mouse click event, the <code>toggle</code> method is invoked with a parameter. After the counter reaches 11 (so after 10s), the toggle button will be disabled.</p><p>Finally, we register the component:</p><pre><code>document.addEventListener("alpine:init", proc(e: Event) =
  registerComponent("dropdown", dropdown)
  bindElement("dropbutton", dropbutton)
)</code></pre><h3>Building the Frontend</h3><p>A simple <code>nimble build</code> in <code>frontend/</code> now produces two files:</p><pre><code>-rw-r--r--@ 1 turbo  staff    11K Dec 29 23:43 app.dist.js
-rw-r--r--@ 1 turbo  staff    34K Dec 21 18:32 app.js</code></pre><p>You may be surprised at how big the compiled artifact is. 34k for a simple dropdown?! What&#8217;s actually happened here though is that Nim has emitted standard library functions we&#8217;ve used, e.g. for string formatting. That will only happen once. As you add more functionality, those functions are re-used, so the output size doesn&#8217;t grow at this rate!</p><p>You can also see Terser has done its job and the final artifact is only a third of the size and looks like this:</p><pre><code><strong>function</strong> n(n){<strong>var</strong> l=m(n.length),e=0,t=0;n:<strong>for</strong>(;e&lt;n.length;){<strong>var</strong> a=n[e];<strong>if</strong>(a&lt;128)l[t]=String.fromCharCode(a),e+=1;<strong>else</strong>{<strong>var</strong> u=m(0);l:e:<strong>for</strong>(;;){0;<strong>var</strong> i=a.toString(16);<strong>if</strong>(1==(<strong>null</strong>==i?0:i.length)?u.push(&#8221;%0&#8221;):u.push(&#8221;%&#8221;),u.push(i),e+=1,n.length&lt;=e||n[e]&lt;128) <em>// and so on</em></code></pre><p>If you&#8217;re size sensitive, you should reduce the reliance on <code>std</code> libraries and bundle JS libraries instead, that you then access via FFI. For example, if I remove the <code>strformat</code> import and the <code>echo</code> statements from the <code>app.nim</code> module, we end up with a much smaller file:</p><pre><code>-rw-r--r--@ 1 turbo  staff   3.0K Dec 29 23:49 app.dist.js
-rw-r--r--@ 1 turbo  staff   7.6K Dec 29 23:49 app.js</code></pre><h2>Backend</h2><p>Our frontend project is now complete, and we have to serve it. I&#8217;m not going to show you how to build a full MASH app, just the basics of SSR HTML that uses the component we built above.</p><p>Here&#8217;s the project structure:</p><pre><code>.
&#9500;&#9472;&#9472; backend
&#9474;   &#9492;&#9472;&#9472; server.nim
&#9500;&#9472;&#9472; frontend
&#9474;   ...
&#9492;&#9472;&#9472; server.nimble</code></pre><p>The nimble file will need to build the frontend first:</p><pre><code>srcDir        = "backend"
bin           = @["server"]
backend       = "c"

requires "nim &gt;= 2.0.0"

requires "karax#head"
requires "mummy &gt;= 0.3.5"

before build:
  withDir "frontend":
    exec "nimble build"
</code></pre><p>We&#8217;ll also pull in mummy and karax. The backend for this is <code>c</code> (or any other native backend you want).</p><h3>Statically including the Frontend Code</h3><p>I prefer to do this, you may also choose to read it from disk or serve it from a CDN:</p><pre><code><strong>import</strong> std / [
  strutils,
  strformat
]

<strong>import</strong> mummy, mummy/routers
<strong>import</strong> karax / [karaxdsl, vdom, vstyles]

<strong>const</strong> appJs = "./frontend/app.dist.js".readFile</code></pre><h3>Rendering the HTML</h3><p>First, let&#8217;s build the <code>&lt;head&gt;</code> section:</p><pre><code><strong>proc</strong> rHead(): VNode =
  buildHtml head:
    meta(charset = "UTF-8")
    meta(
      name = "viewport", 
      content = @[
        "width=device-width", 
        "initial-scale=1", 
        "maximum-scale=1", 
        "user-scalable=0",
      ].join(", ")
    )
    meta("http-equiv" = "X-UA-Compatible", content = "ie=edge")
    script(
      src = "https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js",
      "defer" = ""
    )</code></pre><p>The various <code>meta</code> directives are the minimum required to make sure our page renders fine on all device sizes with sensible defaults.</p><p>We also import the Alpine release. The <code>defer</code> attribute causes the browser to fetch the remote script immediately after parsing the <code>&lt;script&gt;</code> tag, but delay execution until after the entire HTML has been parsed. This is important for our <code>init</code> directives to fire deterministically. Usually boolean attributes are activated like <code>attrib = true</code> in karax, but since it conflicts with the built-in Nim keyword, we need to use the string version.</p><p>Now let&#8217;s build the <code>&lt;body&gt;</code>:</p><pre><code>proc rBody(): VNode =
  buildHtml body:
    tdiv("x-data" = "dropdown(true)"):
      button("x-bind" = "dropbutton"):
        text "Toggle Me"
      tdiv("x-show" = "open"):
        p: text "I&#8217;m inside!"

    verbatim &amp;"""
      &lt;script&gt;
        {appJs}
      &lt;/script&gt;
    """</code></pre><p>A few notes:</p><ul><li><p><code>tdiv</code> is karax&#8217; way of avoiding conflicts with the Nim operator <code>div</code></p></li><li><p><code>x-data</code> is the component initializer, also passing the initial open state to our new dropdown</p></li><li><p><code>x-bind</code> means the <code>&lt;button&gt;</code> will inherit all behavior from the binding we created in Nim</p></li><li><p><code>x-show</code> ensures the inner paragraph is only shown when the <code>open</code> property is true (or truthy)</p></li></ul><p><code>verbatim</code> includes the bundled frontend code inline. Again, this is a personal preference (avoid another request), you don&#8217;t have to do this.</p><p>Now emit the entire HTML file as a string:</p><pre><code><strong>proc</strong> home*(): string =
  <strong>let</strong> vnode = buildHtml html(lang = "en"):
    rHead()
    rBody()

  result = "&lt;!DOCTYPE html&gt;<strong>\n</strong>" &amp; $vnode</code></pre><h3>Serve it</h3><p>The very last thing to do is to serve this via mummy:</p><pre><code><strong>proc</strong> indexHandler(request: Request) =
  <strong>var</strong> headers: HttpHeaders
  headers["Content-Type"] = "text/html; charset=utf-8"
  request.respond(200, headers, home())

<strong>var</strong> router: Router
router.get("/", indexHandler)

<strong>let</strong> server = newServer(router)
echo "Serving on http://localhost:8080"
server.serve(Port(8080))</code></pre><h2>Conclusion</h2><p>After <code>nimble run</code>, we can view the result at <code>http://localhost:8080</code></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!0Lqo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F40e7abb6-99d7-4eb5-b8ec-866472ec6136_960x712.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0Lqo!,w_424,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F40e7abb6-99d7-4eb5-b8ec-866472ec6136_960x712.gif 424w, https://substackcdn.com/image/fetch/$s_!0Lqo!,w_848,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F40e7abb6-99d7-4eb5-b8ec-866472ec6136_960x712.gif 848w, https://substackcdn.com/image/fetch/$s_!0Lqo!,w_1272,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F40e7abb6-99d7-4eb5-b8ec-866472ec6136_960x712.gif 1272w, https://substackcdn.com/image/fetch/$s_!0Lqo!,w_1456,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F40e7abb6-99d7-4eb5-b8ec-866472ec6136_960x712.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!0Lqo!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F40e7abb6-99d7-4eb5-b8ec-866472ec6136_960x712.gif" width="960" height="712" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/40e7abb6-99d7-4eb5-b8ec-866472ec6136_960x712.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:712,&quot;width&quot;:960,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!0Lqo!,w_424,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F40e7abb6-99d7-4eb5-b8ec-866472ec6136_960x712.gif 424w, https://substackcdn.com/image/fetch/$s_!0Lqo!,w_848,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F40e7abb6-99d7-4eb5-b8ec-866472ec6136_960x712.gif 848w, https://substackcdn.com/image/fetch/$s_!0Lqo!,w_1272,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F40e7abb6-99d7-4eb5-b8ec-866472ec6136_960x712.gif 1272w, https://substackcdn.com/image/fetch/$s_!0Lqo!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F40e7abb6-99d7-4eb5-b8ec-866472ec6136_960x712.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>]]></content:encoded></item><item><title><![CDATA[Implementing an auto fallback page in Caddy]]></title><description><![CDATA[Simple static maintenance page]]></description><link>https://pierretempel.com/p/implementing-an-auto-fallback-page</link><guid isPermaLink="false">https://pierretempel.com/p/implementing-an-auto-fallback-page</guid><dc:creator><![CDATA[Pierre]]></dc:creator><pubDate>Tue, 26 Dec 2023 18:38:00 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/6be32d9b-73e2-4663-9ab1-eb30858df79f_3872x2592.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I host lots of private (and a few public) projects behind <a href="https://caddyserver.com/">Caddy</a>. I do like its simplicity and obvious happy path. Something I did struggle with at the start of my journey with Caddy, and something I routinely forget again, is how to set up a fallback page. This is a good idea if there&#8217;s a single-instanced service that might be taken offline for a few moments due to scheduled or unscheduled maintenance. The solution is to create a second, static <a href="https://caddyserver.com/docs/caddyfile/directives/reverse_proxy#upstreams">upstream</a>.</p><p>Let&#8217;s take a look at the Caddyfile for <a href="https://findsight.ai/">Findsight</a>:</p><pre><code>:9999 {
&#9;respond "We&#8217;ll be right back (usually within 3 minutes)!"
}

www.findsight.ai {
&#9;redir https://findsight.ai{uri}
}

findsight.ai {
&#9;handle {
&#9;&#9;reverse_proxy localhost:8080 :9999 {
&#9;&#9;&#9;lb_policy first
&#9;&#9;&#9;lb_try_duration 1s
&#9;&#9;&#9;fail_duration 30s
&#9;&#9;&#9;transport http {
&#9;&#9;&#9;&#9;dial_timeout 500ms
&#9;&#9;&#9;}
&#9;&#9;}
&#9;}
}</code></pre><p>Here&#8217;s what&#8217;s happening:</p><p>In normal operation, Caddy reverse proxies requests to <code>localhost:8080</code> where the main Findsight app listens. The entry point is a domain (<code>findsight.ai</code>), and Caddy will automatically handle certificate provisioning and TLS termination. Additionally, there&#8217;s a request rewrite for the <code>www</code> subdomain.</p><p>If <code>localhost:8080</code> were the only upstream defined and the server goes down (e.g. for database migration) Cloudflare, which is the CDN for Findsight, would display a generic &#8220;upstream dead&#8221; error and that&#8217;s not a good look. It leaves the user guessing: why is it down? When will it be back?</p><p>We need to define a fallback. If Caddy considers an entry for <code>reverse_proxy</code> to be dead, it moves on to the next one. That behavior is defined by the <code>lb</code> (<a href="https://caddyserver.com/docs/caddyfile/directives/reverse_proxy#load-balancing">load balancing</a>) policy <code>first</code>. Dead upstreams remain dead for 30s. This duration can be tuned to taste, I chose it mainly to avoid requests hitting the server during start-up after a crash, when the port is reserved, but requests won&#8217;t be processed yet.</p><p>Now requests hit the internal <code>:9999</code> upstream, which serves a <a href="https://caddyserver.com/docs/caddyfile/directives/respond">static response</a> directly from Caddy. Here, one could alternatively display a full HTML page.</p>]]></content:encoded></item><item><title><![CDATA[Building a Hybrid/AI Search Engine for Non-fiction Content]]></title><description><![CDATA[How I built Findsight AI]]></description><link>https://pierretempel.com/p/building-a-hybridai-search-engine</link><guid isPermaLink="false">https://pierretempel.com/p/building-a-hybridai-search-engine</guid><dc:creator><![CDATA[Pierre]]></dc:creator><pubDate>Tue, 08 Aug 2023 01:30:00 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/177705595/939042eb4a6b64b96b842cf5c758faa7.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<p>The technical details behind <a href="https://findsight.ai">findsight.ai</a></p>]]></content:encoded></item><item><title><![CDATA[Enforcing rate limits with a single SQLite statement]]></title><description><![CDATA[Good-enough rate limits for basic applications]]></description><link>https://pierretempel.com/p/enforcing-rate-limits-with-a-single</link><guid isPermaLink="false">https://pierretempel.com/p/enforcing-rate-limits-with-a-single</guid><dc:creator><![CDATA[Pierre]]></dc:creator><pubDate>Sun, 02 Apr 2023 15:41:00 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/0efb3387-2b1a-4b79-bf7e-831702be02ec_1920x1280.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Application-layer rate limiting is a good way to enforce client resource limits, while at the same time allowing you to place the rate limiting configuration and current statistics next to the application data.</p><p>This approach should not be employed for defending against application layer or network layer (D)DoS attacks; rather, such defense should be pushed towards the edge, utilizing Web Application Firewalls or analogous policy agents that can drop suspicious traffic with minimal overhead.</p><p>In contrast, this approach is more suitable for enforcing fair use limits or limits that are part of a license, which restricts the number of operations an actor can perform.</p><p>SQLite is used to:</p><ul><li><p>store the limits configuration, each with a maximum count per configurable time period</p></li><li><p>store instances of active counters for actors (we&#8217;ll use &#8220;visitors&#8221; for this example)</p></li><li><p>manage expiration</p></li></ul><h2>Interface</h2><p>The goal is that with a single query executed for every operation, rate limiting calculations are performed automatically:</p><pre><code>INSERT INTO visitor_limits(visitor_id, limit_id)
VALUES (1, 1)
ON CONFLICT DO UPDATE SET count = count + 1
RETURNING denied, resets_at;</code></pre><ul><li><p>We inform the database that visitor <code>1</code> is attempting to perform an operation limited by limit <code>1</code>.</p></li><li><p>If the limit is not yet reached, the current counter is incremented</p></li><li><p>If the limit is reached, <code>denied</code> will be <code>1</code> (SQLite has no booleans), and <code>resets_at</code> is defined as the next UTC unix epoch timestamp after which the limit resets. <code>resets_at</code> is not defined when the request isn&#8217;t denied.</p></li></ul><p>This allows the counter to be incremented by other positive integer values, for example, if the &#8220;cost&#8221; between operations varies, which is useful for consumption-based enforcement.</p><h2>Database Setup</h2><h3>Limits</h3><p>The schema for this is quite simple. First, we define a static configuration table for the limits:</p><pre><code>CREATE TABLE IF NOT EXISTS limits (
  id INTEGER PRIMARY KEY ASC
  ,name TEXT UNIQUE NOT NULL
  ,maximum INTEGER NOT NULL CHECK ( maximum &gt; 0 )
  ,period_minutes INTEGER NOT NULL CHECK ( period_minutes &gt;= 1 )

  ,UNIQUE (name, maximum)
) STRICT;</code></pre><ul><li><p><code>name</code> uniquely identifies the limit; this should be the limited operation, e.g. <code>send_message</code></p></li><li><p><code>maximum</code> is the maximum number of allowed operations within the period. This is inclusive, so a maximum of 5 allows 5 requests in a period and denies the 6th</p></li><li><p><code>period_minutes</code> defines the time between counter resets.</p></li></ul><p>The period can alternatively be expressed in seconds, which is the highest usable time resolution in SQLite.</p><p>To add a new limit:</p><pre><code>-- allow 5 messages per 2 minutes
INSERT INTO limits(name, maximum, period_minutes)
VALUES (&#8217;send_message&#8217;, 5, 2);</code></pre><h3>Counters</h3><p>Next, create a table for the counter instances. These will be lazily populated - if a visitor never executes a given operation, there&#8217;s no need to create a row for it:</p><pre><code>CREATE TABLE IF NOT EXISTS visitor_limits (
  id INTEGER PRIMARY KEY ASC
  ,visitor_id INTEGER NOT NULL REFERENCES visitors(id)
  ,limit_id INTEGER NOT NULL REFERENCES limits(id)

  ,denied INTEGER NOT NULL DEFAULT 0 CHECK ( denied IN (0, 1) )

  -- set by trigger
  ,count INTEGER CHECK ( count &gt;= 0 )
  ,resets_at INTEGER CHECK ( resets_at &gt; 1680373723 )

  ,UNIQUE (visitor_id, limit_id)
) STRICT;</code></pre><p>Note: this references another table <code>visitors</code>, this may not be the case in your application. Use whichever actor identifier fits your model best.</p><p>A sanity check is added to <code>resets_at</code> to protect against logic bugs, using a reasonably recent timestamp and making it clear in the DDL that this column will be a unix epoch in seconds.</p><h3>Triggers</h3><p>To implement the add and reset logic, two triggers are required:</p><pre><code>CREATE TRIGGER IF NOT EXISTS init_visitor_limit_counter
AFTER INSERT
ON visitor_limits
WHEN (NEW.count IS NULL OR NEW.resets_at IS NULL)
BEGIN
  UPDATE visitor_limits
    SET
      count = 1
      ,resets_at = (unixepoch() + (60 * (SELECT period_minutes FROM limits l WHERE l.id = NEW.limit_id)))
      ,denied = 0
  WHERE id = NEW.id;
END;</code></pre><p>This runs after the <code>INSERT</code> statement in our final interface query. If there wasn&#8217;t a counter for this operation and visitor, we initialize the counter, flag, and first reset timestamp.</p><p>For any <code>INSERT</code>s after this, we need to maintain the flag and reset the counter if necessary using a second trigger:</p><pre><code>CREATE TRIGGER IF NOT EXISTS update_visitor_limit_counter_breaker
BEFORE INSERT
ON visitor_limits
BEGIN
  -- trip breaker for non-expired
  UPDATE visitor_limits
    SET denied = visitor_limits.count &gt;= (SELECT maximum FROM limits l WHERE l.id = NEW.limit_id)
  WHERE visitor_limits.visitor_id = NEW.visitor_id
    AND visitor_limits.limit_id = NEW.limit_id;

  -- or reset if expired
  UPDATE visitor_limits
    SET
      count = 0
      ,resets_at = unixepoch() + (60 * (SELECT period_minutes FROM limits l WHERE l.id = NEW.limit_id))
      ,denied = 0
  WHERE visitor_limits.visitor_id = NEW.visitor_id
    AND visitor_limits.limit_id = NEW.limit_id
    AND unixepoch() &gt; visitor_limits.resets_at;
END;</code></pre><p>This runs before the <code>INSERT</code> to make sure the <code>denied</code> flag that is returned is up to date. In our example, the 6th message should be denied, not the 7th.</p><h2>Testing</h2><p>That&#8217;s it. We can test this now by running the same interface query repeatedly:</p><pre><code>INSERT INTO visitor_limits(visitor_id, limit_id)
VALUES (1, 1)
ON CONFLICT DO UPDATE SET count = count + 1
RETURNING denied, resets_at;

-- send 6 messages in &lt; 2 minutes:

-- Run 1 returns: 0, NULL
-- Run 2, 3, 4, 5 return: 0, 1680441289
-- Run 6 returns: 1, 1680441289

-- ... wait for 2 minutes ...

-- Run 7 returns: 0, 1680441422</code></pre><p>As the limit is reached, the counter will continue to increment; which can be leveraged to gain insights into the behavior of limited users. A minor overage is to be expected, but a drastic surge in requests beyond the set limit could signify a bug or the presence of a malicious bot.</p>]]></content:encoded></item><item><title><![CDATA[Hypothetical Embeddings Explained]]></title><description><![CDATA[Using Large Language Models to explore knowledge without the risk of hallucinations]]></description><link>https://pierretempel.com/p/hypothetical-embeddings-explained</link><guid isPermaLink="false">https://pierretempel.com/p/hypothetical-embeddings-explained</guid><dc:creator><![CDATA[Pierre]]></dc:creator><pubDate>Sat, 11 Mar 2023 18:42:00 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/05e58056-f594-4289-bac1-54317e61c38c_6240x4160.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a href="https://en.wikipedia.org/wiki/Wikipedia:Large_language_models">Large Language Models</a> (LLM) can be combined with <a href="https://en.wikipedia.org/wiki/Sentence_embedding">embeddings</a> to create accurate knowledge retrieval systems. Hypothetical Document Embeddings, or HyDE<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>, can be used to provide a question-answering interface for a given database of facts that feels like an LLM-powered chatbot, but without the risk of hallucinations.</p><p>In LLM-only systems, such as search engines powered by <a href="https://huggingface.co/blog/rlhf">RLHF</a>-tuned models, there&#8217;s a risk of the core model confidently stating wrong facts or making assertions that go beyond or even contradict the available ground truths (e.g. a search index). This problem is more commonly known as <a href="https://en.wikipedia.org/wiki/Hallucination_%28artificial_intelligence%29">hallucination</a>.</p><p>There are <a href="https://cobusgreyling.medium.com/preventing-llm-hallucination-with-contextual-prompt-engineering-an-example-from-openai-7e7d58736162">several ways</a> to engineer prompts to minimize this effect, and combined with <a href="https://arxiv.org/abs/2210.11416">instruction fine-tuning</a> are generally successful in limiting hallucinations. In systems where fact retrieval is the primary use case, though, this issue can be almost eliminated.</p><p>HyDE-powered search relies on embeddings, dense vector representations of (in this case) textual data. <a href="https://platform.openai.com/docs/guides/embeddings">OpenAI embeddings</a> for example, can effectively encode abstract qualities of short and long-form text (up to ~8,000 tokens per embedding) in ~1600-dimensional vectors. These vectors can then be stored in vector-similarity (VSS) indexes (e.g. <a href="https://redis.io/docs/stack/search/reference/vectors/#querying-vector-fields">HNSW/RediSearch</a>) that facilitate efficient VSS or <a href="https://redis.io/docs/stack/search/reference/vectors/#hybrid-knn-queries">hybrid VSS and full-text queries</a>.</p><p>However, these systems are then limited to retrieving similar facts. A user interface could prompt the user for a fact, which is then embedded and used as a query vector to retrieve the nearest (most similar) facts from the index. This requires the user to already have a possible answer in mind, which is much less intuitive than asking a question.</p><p>Instead of embedding the user input directly, a sufficiently trained LLM can be used to generate <em>hypothetical</em> answers, which are in turn embedded and used for retrieval. The LLM will likely hallucinate details for its answers, but since these answers are not relayed to the user as fact, these errors are inconsequential. The embedding is much more sensitive to the structure and language used in the answers, rather than their correctness.</p><p>An example prompt (here using LLaMa-65B) may look like this:</p><pre><code>Three possible answers to the question &#8216;what are the negative effects of the war on drugs?&#8217; are:
1.</code></pre><p>with an example output:</p><pre><code>1. Incarcerated people in the U.S. have exceeded two million and the great majority of the people in jail are there for drug related crimes. In many cases, these people don&#8217;t get to go to school and learn the skills they need to lead healthy, productive lives.
2. People all over the world die daily, in violent confrontations, related to the trade of illegal drugs.
3. Millions of people, including children, are sick with diseases caused by using contaminated, illegal drugs. These people are spreading disease to others.</code></pre><p>Depending on the performance of the embedding and the difference between the answers, one embedding per answer (then averaged) or one embedding for the entire answer list can be used. For non-fiction fact retrieval (excerpts from books), I found it helpful to include the question itself in the embedding for each HyDE, since this type of content often poses a question only to immediately answer it.</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>HyDE was first introduced in <a href="https://arxiv.org/abs/2212.10496">Precise Zero-Shot Dense Retrieval without Relevance Labels</a>, where InstructGPT had been fine tuned to generate HyDE.</p><p></p></div></div>]]></content:encoded></item><item><title><![CDATA[Installing Nim 2.0+ on Apple Silicon]]></title><description><![CDATA[A good programming language gets a great update!]]></description><link>https://pierretempel.com/p/installing-nim-20-on-apple-silicon</link><guid isPermaLink="false">https://pierretempel.com/p/installing-nim-20-on-apple-silicon</guid><dc:creator><![CDATA[Pierre]]></dc:creator><pubDate>Sun, 15 Jan 2023 18:46:00 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/cc942e0f-73e2-414d-9d62-adc054876c90_1100x733.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em><strong>Update</strong>: this still applies to Nim 2.2.6 (October 2025).</em></p><p>The Nim 2.0 release candidate <a href="https://nim-lang.org/blog/2022/12/21/version-20-rc.html">is out</a>, and includes a whole host of amazing changes and additions. I had been holding off on exploring Nim because I was waiting for <a href="https://nim-lang.org/blog/2020/12/08/introducing-orc.html">ORC</a> and multi-threading to become the default. They have been available for a long time, but defaults matter - it&#8217;s what first-time and first-time-in-a-while users experience. It forms their opinion of what the language is about.</p><p>Installing Nim on Apple Silicon Macs is pretty straightforward. The official installer sets up <code>choosenim</code> and a default stable release:</p><pre><code>curl https://nim-lang.org/choosenim/init.sh -sSf | sh</code></pre><p>However, both Nim itself and the compiler output will be <code>amd64</code> (Intel) binaries. To go native, and to benefit from Nim 2 right now, more configuration is required.</p><h2>Set up Nim 2 natively</h2><p>We need a source build, and thankfully that&#8217;s easy:</p><pre><code>git clone https://github.com/nim-lang/Nim
cd Nim
git checkout version-2-0
sh build_all.sh</code></pre><p>Yep, that&#8217;s it. We don&#8217;t have to mess with our <code>PATH</code> either. Instead, we can now point <code>choosenim</code> at the new build:</p><pre><code>choosenim &lt;location&gt;/Nim</code></pre><p>And confirm the architecture with:</p><pre><code>$ nim -v
Nim Compiler Version 1.9.1 [MacOSX: arm64]
Compiled at 2023-01-14
Copyright (c) 2006-2022 by Andreas Rumpf</code></pre><p>(2.0 RC self-reports as 1.9.1 as of Jan 15 2023)</p><p>To switch back, use <code>choosenim stable</code>.</p><h2>Native &amp; universal binaries</h2><p>Building a project with this toolchain will produce an Intel binary. To get a full build on macOS, we have to follow <a href="https://developer.apple.com/documentation/apple-silicon/building-a-universal-macos-binary">Apple&#8217;s guidelines</a> and set the correct compiler and linker targets. In your project&#8217;s nimble (<code>nimble init</code> if you don&#8217;t have one yet), add a new target:</p><pre><code>task release_clang, "Build a production release (macOS)":
  --verbose
  --forceBuild:on
  --cc:clang
  --define:release
  --deepcopy:on
  --cpu:arm64
  --passC:"-flto -target arm64-apple-macos11"
  --passL:"-flto -target arm64-apple-macos11"
  --hints:off
  --outdir:"."
  setCommand "c", "hello.nim"</code></pre><ul><li><p><code>-flto</code> is required because clang doesn&#8217;t like Nim&#8217;s default passing of LTO parameters</p></li><li><p>note the <code>cpu</code> and <code>-target</code> parameters</p></li><li><p>the rest are standard release flags</p></li></ul><p>ORC and <code>threads</code> are default now, so no need to enable them here anymore. It&#8217;s a good idea to pin the Nim version in the nimble file, in this case to <code>&gt;= 1.9.1</code>.</p><p>Running <code>nimble release_clang</code> now gives us a nice native binary:</p><pre><code>$ file hello
hello: Mach-O 64-bit executable arm64</code></pre><p>If you want to build an Intel binary, add another task and change the target to <code>x86_64-apple-macos10.12</code>. If you need a universal binary, build both the Intel and ARM targets first, then merge them using <code>lipo</code>:</p><pre><code>lipo -create -output universal_app x86_app arm_app</code></pre><p><strong>A final note</strong>: you should always also use <code>-d:malloc</code>. It will be slower, but default Nim (as of October 2025) leaks memory.</p>]]></content:encoded></item><item><title><![CDATA[Baking Vertex Animations into Alembic Files]]></title><description><![CDATA[Convert OBJ sequences to single Alembic files, transport animation data between apps]]></description><link>https://pierretempel.com/p/baking-vertex-animations-into-alembic</link><guid isPermaLink="false">https://pierretempel.com/p/baking-vertex-animations-into-alembic</guid><dc:creator><![CDATA[Pierre]]></dc:creator><pubDate>Sat, 31 Dec 2022 19:06:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!4AYK!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbaeaee44-7b0a-4673-9566-135e03f837e0_3356x1956.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This continues the series on baking dynamics. Last time, we looked at <a href="https://pierretempel.com/p/pre-baking-dynamics-into-fbx-animations">baking rigid body dynamics into FBX f-curves</a>, for rendering in applications that don&#8217;t support dynamics (such as mo2).</p><p>In that post, I mentioned that baking soft-body dynamics is a lot harder. There are several reasons for that:</p><ul><li><p>Soft body deformations rely on per-vertex transformations</p></li><li><p>Apart from Alembic, there are no standard ways of transporting point-based animations between 3D software</p></li><li><p>Even then, some simulations (e.g. fluids) may change the vertex and thus poly count, something barely any software can deal with</p></li></ul><p>This problem isn&#8217;t new, and the industry has come up with two methods to address this:</p><ol><li><p>Portable dynamics bake caches (take a look at the <a href="https://code.tc/baking-fbx-dynamics/">previous post</a> for an explanation of dynamics baking). This is a way of transporting the internal bake cache between applications without &#8220;rendering&#8221; the transformations to a mesh. Notable software includes e.g. <a href="https://www.lightwave3d.com/chronosculpt/">ChronoSculpt</a>, which can work with three major cache formats (incl. Alembic)</p></li><li><p>The brute force method: Bake a new mesh for every single frame. This is supported by many apps as &#8220;OBJ sequences&#8221; or &#8220;mesh sequences&#8221;, notably AE Element3D. As the name implies, the export format is a folder full of single OBJ files, one for each tick (a frame). The benefit of this method is the wide compatibility: everything supports OBJs (though not necessarily sequences), and changing poly counts are a non-issue since we exchange the entire mesh every tick.</p></li></ol><p>So mesh sequences address a similar problem to Alembic, but how can we convert between them? I needed to do this recently, to transport soft-body physics baked in Cheetah to Marmoset Toolbag 4 for rendering. Cheetah supports nothing - it stores its bake cache in the main scene file but the format is undocumented, and there isn&#8217;t any support for exporting Alembics. Toolbag on the other hand only supports Alembic, but not mesh sequences.</p><p>The first issue was easy enough to address, and I won&#8217;t go into much detail because you&#8217;re likely not using Cheetah and are here to learn how to do the conversion from sequences to Alembic. In short, there&#8217;s both an <a href="https://www.cheetah3d.com/forum/index.php?threads/2110/post-14628">import</a> and an <a href="https://www.cheetah3d.com/forum/index.php?threads/2110/post-82310">export</a> script for OBJ sequences for Cheetah, created by forum users.</p><p>Baking and exporting leaves us with a folder full of OBJs:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4AYK!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbaeaee44-7b0a-4673-9566-135e03f837e0_3356x1956.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4AYK!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbaeaee44-7b0a-4673-9566-135e03f837e0_3356x1956.png 424w, https://substackcdn.com/image/fetch/$s_!4AYK!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbaeaee44-7b0a-4673-9566-135e03f837e0_3356x1956.png 848w, https://substackcdn.com/image/fetch/$s_!4AYK!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbaeaee44-7b0a-4673-9566-135e03f837e0_3356x1956.png 1272w, https://substackcdn.com/image/fetch/$s_!4AYK!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbaeaee44-7b0a-4673-9566-135e03f837e0_3356x1956.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4AYK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbaeaee44-7b0a-4673-9566-135e03f837e0_3356x1956.png" width="1456" height="849" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/baeaee44-7b0a-4673-9566-135e03f837e0_3356x1956.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:849,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!4AYK!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbaeaee44-7b0a-4673-9566-135e03f837e0_3356x1956.png 424w, https://substackcdn.com/image/fetch/$s_!4AYK!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbaeaee44-7b0a-4673-9566-135e03f837e0_3356x1956.png 848w, https://substackcdn.com/image/fetch/$s_!4AYK!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbaeaee44-7b0a-4673-9566-135e03f837e0_3356x1956.png 1272w, https://substackcdn.com/image/fetch/$s_!4AYK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbaeaee44-7b0a-4673-9566-135e03f837e0_3356x1956.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!7udy!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c794e8b-3ae8-467c-abaf-18800efeab20_1514x974.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!7udy!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c794e8b-3ae8-467c-abaf-18800efeab20_1514x974.png 424w, https://substackcdn.com/image/fetch/$s_!7udy!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c794e8b-3ae8-467c-abaf-18800efeab20_1514x974.png 848w, https://substackcdn.com/image/fetch/$s_!7udy!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c794e8b-3ae8-467c-abaf-18800efeab20_1514x974.png 1272w, https://substackcdn.com/image/fetch/$s_!7udy!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c794e8b-3ae8-467c-abaf-18800efeab20_1514x974.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!7udy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c794e8b-3ae8-467c-abaf-18800efeab20_1514x974.png" width="614" height="395.135989010989" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5c794e8b-3ae8-467c-abaf-18800efeab20_1514x974.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:937,&quot;width&quot;:1456,&quot;resizeWidth&quot;:614,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!7udy!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c794e8b-3ae8-467c-abaf-18800efeab20_1514x974.png 424w, https://substackcdn.com/image/fetch/$s_!7udy!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c794e8b-3ae8-467c-abaf-18800efeab20_1514x974.png 848w, https://substackcdn.com/image/fetch/$s_!7udy!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c794e8b-3ae8-467c-abaf-18800efeab20_1514x974.png 1272w, https://substackcdn.com/image/fetch/$s_!7udy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c794e8b-3ae8-467c-abaf-18800efeab20_1514x974.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Now we need to convert those into one Alembic file. I&#8217;ve searched a while for a simple way to do this, but I&#8217;m afraid there isn&#8217;t one. We have to involve another piece of software, but thankfully it&#8217;s free: good old <a href="https://www.blender.org/">Blender</a>.</p><p>Users of Blender have tried to solve this problem in the past, and a few have had success with slightly abusing the <a href="https://blenderartists.org/t/stop-motion-obj-obj-stl-ply-sequence-importer-v2-1-1/670105/86">Stop Motion OBJ</a> plugin, though that workaround seems to no longer work in Blender 3.</p><p>Enter <a href="https://www.artstation.com/vaticinator">Barnaba Miku&#322;owski</a>, who graciously provided a plugin-free conversion setup based on a geometry node network in Blender. I&#8217;ve adapted his original file to be a more generic template, and you can <strong><a href="https://avm-cdn.com/files/obj-to-abc-empty.blend">download it here (3 MB)</a></strong>.</p><p>Open the Blender scene (I used Blender 3, I recommend you do the same). Click to highlight the &#8220;Import Sequence Here&#8221; collection and enable visibility if required.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!L6WL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fccd35db1-1f19-4368-8ad1-7d16b283718a_642x300.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!L6WL!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fccd35db1-1f19-4368-8ad1-7d16b283718a_642x300.png 424w, https://substackcdn.com/image/fetch/$s_!L6WL!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fccd35db1-1f19-4368-8ad1-7d16b283718a_642x300.png 848w, https://substackcdn.com/image/fetch/$s_!L6WL!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fccd35db1-1f19-4368-8ad1-7d16b283718a_642x300.png 1272w, https://substackcdn.com/image/fetch/$s_!L6WL!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fccd35db1-1f19-4368-8ad1-7d16b283718a_642x300.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!L6WL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fccd35db1-1f19-4368-8ad1-7d16b283718a_642x300.png" width="396" height="185.0467289719626" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ccd35db1-1f19-4368-8ad1-7d16b283718a_642x300.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:300,&quot;width&quot;:642,&quot;resizeWidth&quot;:396,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!L6WL!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fccd35db1-1f19-4368-8ad1-7d16b283718a_642x300.png 424w, https://substackcdn.com/image/fetch/$s_!L6WL!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fccd35db1-1f19-4368-8ad1-7d16b283718a_642x300.png 848w, https://substackcdn.com/image/fetch/$s_!L6WL!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fccd35db1-1f19-4368-8ad1-7d16b283718a_642x300.png 1272w, https://substackcdn.com/image/fetch/$s_!L6WL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fccd35db1-1f19-4368-8ad1-7d16b283718a_642x300.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Go to File -&gt; Import -&gt; Wavefront (.obj) &#8211; don&#8217;t use the legacy option</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_ndi!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4125b00-8047-4cd9-9b04-4cfabf12527f_1108x862.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_ndi!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4125b00-8047-4cd9-9b04-4cfabf12527f_1108x862.png 424w, https://substackcdn.com/image/fetch/$s_!_ndi!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4125b00-8047-4cd9-9b04-4cfabf12527f_1108x862.png 848w, https://substackcdn.com/image/fetch/$s_!_ndi!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4125b00-8047-4cd9-9b04-4cfabf12527f_1108x862.png 1272w, https://substackcdn.com/image/fetch/$s_!_ndi!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4125b00-8047-4cd9-9b04-4cfabf12527f_1108x862.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_ndi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4125b00-8047-4cd9-9b04-4cfabf12527f_1108x862.png" width="1108" height="862" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b4125b00-8047-4cd9-9b04-4cfabf12527f_1108x862.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:862,&quot;width&quot;:1108,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!_ndi!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4125b00-8047-4cd9-9b04-4cfabf12527f_1108x862.png 424w, https://substackcdn.com/image/fetch/$s_!_ndi!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4125b00-8047-4cd9-9b04-4cfabf12527f_1108x862.png 848w, https://substackcdn.com/image/fetch/$s_!_ndi!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4125b00-8047-4cd9-9b04-4cfabf12527f_1108x862.png 1272w, https://substackcdn.com/image/fetch/$s_!_ndi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4125b00-8047-4cd9-9b04-4cfabf12527f_1108x862.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Navigate to your mesh sequence folder and click-shift-select all files that are part of the sequence (make sure the naming produces proper ordering!)</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!QxuT!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb7fe9ac-efa6-47a8-a409-bd577b332b2d_1668x1264.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!QxuT!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb7fe9ac-efa6-47a8-a409-bd577b332b2d_1668x1264.png 424w, https://substackcdn.com/image/fetch/$s_!QxuT!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb7fe9ac-efa6-47a8-a409-bd577b332b2d_1668x1264.png 848w, https://substackcdn.com/image/fetch/$s_!QxuT!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb7fe9ac-efa6-47a8-a409-bd577b332b2d_1668x1264.png 1272w, https://substackcdn.com/image/fetch/$s_!QxuT!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb7fe9ac-efa6-47a8-a409-bd577b332b2d_1668x1264.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!QxuT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb7fe9ac-efa6-47a8-a409-bd577b332b2d_1668x1264.png" width="1456" height="1103" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fb7fe9ac-efa6-47a8-a409-bd577b332b2d_1668x1264.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1103,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!QxuT!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb7fe9ac-efa6-47a8-a409-bd577b332b2d_1668x1264.png 424w, https://substackcdn.com/image/fetch/$s_!QxuT!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb7fe9ac-efa6-47a8-a409-bd577b332b2d_1668x1264.png 848w, https://substackcdn.com/image/fetch/$s_!QxuT!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb7fe9ac-efa6-47a8-a409-bd577b332b2d_1668x1264.png 1272w, https://substackcdn.com/image/fetch/$s_!QxuT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb7fe9ac-efa6-47a8-a409-bd577b332b2d_1668x1264.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Click import, you should now see all OBJs imported as children of the import collection, in proper order, and rendered in the viewport.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Jufm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facb232fc-cb45-4abc-b60a-663459c983e5_1304x780.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Jufm!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facb232fc-cb45-4abc-b60a-663459c983e5_1304x780.png 424w, https://substackcdn.com/image/fetch/$s_!Jufm!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facb232fc-cb45-4abc-b60a-663459c983e5_1304x780.png 848w, https://substackcdn.com/image/fetch/$s_!Jufm!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facb232fc-cb45-4abc-b60a-663459c983e5_1304x780.png 1272w, https://substackcdn.com/image/fetch/$s_!Jufm!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facb232fc-cb45-4abc-b60a-663459c983e5_1304x780.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Jufm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facb232fc-cb45-4abc-b60a-663459c983e5_1304x780.png" width="1304" height="780" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/acb232fc-cb45-4abc-b60a-663459c983e5_1304x780.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:780,&quot;width&quot;:1304,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Jufm!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facb232fc-cb45-4abc-b60a-663459c983e5_1304x780.png 424w, https://substackcdn.com/image/fetch/$s_!Jufm!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facb232fc-cb45-4abc-b60a-663459c983e5_1304x780.png 848w, https://substackcdn.com/image/fetch/$s_!Jufm!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facb232fc-cb45-4abc-b60a-663459c983e5_1304x780.png 1272w, https://substackcdn.com/image/fetch/$s_!Jufm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facb232fc-cb45-4abc-b60a-663459c983e5_1304x780.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>To clean up the viewport, untick the box on the collection object to re-exclude it from the viewport. In the bake network, confirm the input is set to the import collection. Adjust the Blender animation timeline to fit your imported sequence (start and end frames can be set at the top right of the keyframe panel, and FPS can be set in the output properties which can be found behind the printer icon in the vertical icon bar on the right between the viewports and the inspector). Click the play button to preview your animation and check everything&#8217;s in order.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!goNk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F768ce01b-549a-4157-9815-7e66b690f9f3_804x962.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!goNk!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F768ce01b-549a-4157-9815-7e66b690f9f3_804x962.png 424w, https://substackcdn.com/image/fetch/$s_!goNk!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F768ce01b-549a-4157-9815-7e66b690f9f3_804x962.png 848w, https://substackcdn.com/image/fetch/$s_!goNk!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F768ce01b-549a-4157-9815-7e66b690f9f3_804x962.png 1272w, https://substackcdn.com/image/fetch/$s_!goNk!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F768ce01b-549a-4157-9815-7e66b690f9f3_804x962.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!goNk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F768ce01b-549a-4157-9815-7e66b690f9f3_804x962.png" width="384" height="459.46268656716416" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/768ce01b-549a-4157-9815-7e66b690f9f3_804x962.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:962,&quot;width&quot;:804,&quot;resizeWidth&quot;:384,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!goNk!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F768ce01b-549a-4157-9815-7e66b690f9f3_804x962.png 424w, https://substackcdn.com/image/fetch/$s_!goNk!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F768ce01b-549a-4157-9815-7e66b690f9f3_804x962.png 848w, https://substackcdn.com/image/fetch/$s_!goNk!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F768ce01b-549a-4157-9815-7e66b690f9f3_804x962.png 1272w, https://substackcdn.com/image/fetch/$s_!goNk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F768ce01b-549a-4157-9815-7e66b690f9f3_804x962.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Now select (!) only the bake network in the object tree and go to File -&gt; Export -&gt; Alembic (.abc). Change the scene options if required, but make sure &#8220;Only: Selected Objects&#8221; is ticked!</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!1Prr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3712c5f-ea88-44f7-9366-c54b0ddc035f_1312x798.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!1Prr!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3712c5f-ea88-44f7-9366-c54b0ddc035f_1312x798.png 424w, https://substackcdn.com/image/fetch/$s_!1Prr!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3712c5f-ea88-44f7-9366-c54b0ddc035f_1312x798.png 848w, https://substackcdn.com/image/fetch/$s_!1Prr!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3712c5f-ea88-44f7-9366-c54b0ddc035f_1312x798.png 1272w, https://substackcdn.com/image/fetch/$s_!1Prr!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3712c5f-ea88-44f7-9366-c54b0ddc035f_1312x798.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!1Prr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3712c5f-ea88-44f7-9366-c54b0ddc035f_1312x798.png" width="1312" height="798" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a3712c5f-ea88-44f7-9366-c54b0ddc035f_1312x798.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:798,&quot;width&quot;:1312,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!1Prr!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3712c5f-ea88-44f7-9366-c54b0ddc035f_1312x798.png 424w, https://substackcdn.com/image/fetch/$s_!1Prr!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3712c5f-ea88-44f7-9366-c54b0ddc035f_1312x798.png 848w, https://substackcdn.com/image/fetch/$s_!1Prr!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3712c5f-ea88-44f7-9366-c54b0ddc035f_1312x798.png 1272w, https://substackcdn.com/image/fetch/$s_!1Prr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa3712c5f-ea88-44f7-9366-c54b0ddc035f_1312x798.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!WOaG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dd8d92e-60af-4d49-8356-035fc3f52b7b_1648x1238.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!WOaG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dd8d92e-60af-4d49-8356-035fc3f52b7b_1648x1238.png 424w, https://substackcdn.com/image/fetch/$s_!WOaG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dd8d92e-60af-4d49-8356-035fc3f52b7b_1648x1238.png 848w, https://substackcdn.com/image/fetch/$s_!WOaG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dd8d92e-60af-4d49-8356-035fc3f52b7b_1648x1238.png 1272w, https://substackcdn.com/image/fetch/$s_!WOaG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dd8d92e-60af-4d49-8356-035fc3f52b7b_1648x1238.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!WOaG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dd8d92e-60af-4d49-8356-035fc3f52b7b_1648x1238.png" width="1456" height="1094" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7dd8d92e-60af-4d49-8356-035fc3f52b7b_1648x1238.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1094,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!WOaG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dd8d92e-60af-4d49-8356-035fc3f52b7b_1648x1238.png 424w, https://substackcdn.com/image/fetch/$s_!WOaG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dd8d92e-60af-4d49-8356-035fc3f52b7b_1648x1238.png 848w, https://substackcdn.com/image/fetch/$s_!WOaG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dd8d92e-60af-4d49-8356-035fc3f52b7b_1648x1238.png 1272w, https://substackcdn.com/image/fetch/$s_!WOaG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7dd8d92e-60af-4d49-8356-035fc3f52b7b_1648x1238.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Now, the file can be imported into any package that supports Alembic:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!j501!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F508fe0d7-b7b3-4eb8-8e79-7336c4458dde_803x468.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!j501!,w_424,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F508fe0d7-b7b3-4eb8-8e79-7336c4458dde_803x468.gif 424w, https://substackcdn.com/image/fetch/$s_!j501!,w_848,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F508fe0d7-b7b3-4eb8-8e79-7336c4458dde_803x468.gif 848w, https://substackcdn.com/image/fetch/$s_!j501!,w_1272,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F508fe0d7-b7b3-4eb8-8e79-7336c4458dde_803x468.gif 1272w, https://substackcdn.com/image/fetch/$s_!j501!,w_1456,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F508fe0d7-b7b3-4eb8-8e79-7336c4458dde_803x468.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!j501!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F508fe0d7-b7b3-4eb8-8e79-7336c4458dde_803x468.gif" width="803" height="468" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/508fe0d7-b7b3-4eb8-8e79-7336c4458dde_803x468.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:468,&quot;width&quot;:803,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!j501!,w_424,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F508fe0d7-b7b3-4eb8-8e79-7336c4458dde_803x468.gif 424w, https://substackcdn.com/image/fetch/$s_!j501!,w_848,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F508fe0d7-b7b3-4eb8-8e79-7336c4458dde_803x468.gif 848w, https://substackcdn.com/image/fetch/$s_!j501!,w_1272,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F508fe0d7-b7b3-4eb8-8e79-7336c4458dde_803x468.gif 1272w, https://substackcdn.com/image/fetch/$s_!j501!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F508fe0d7-b7b3-4eb8-8e79-7336c4458dde_803x468.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>]]></content:encoded></item><item><title><![CDATA[Pre-baking dynamics into FBX animations]]></title><description><![CDATA[Rendering physics in 3D engines that don't support it!]]></description><link>https://pierretempel.com/p/pre-baking-dynamics-into-fbx-animations</link><guid isPermaLink="false">https://pierretempel.com/p/pre-baking-dynamics-into-fbx-animations</guid><dc:creator><![CDATA[Pierre]]></dc:creator><pubDate>Tue, 27 Dec 2022 19:00:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!eIPJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1818dd24-2bca-40fb-9ad7-8420622f15b9_1928x814.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>For a recent project, I needed to render rigid body physics for a few motion graphics elements. For basic 3D compositing I use <a href="https://www.motionvfx.com/store,mo2,p2345.html">MotionVFX&#8217;s mo2</a> (soon to be replaced by mo3). However, since mo2 has to work in realtime within Apple Motion, it has numerous limitations, one of which is no support for any dynamics. I found a workaround: baking dynamics into FBX F-curve animations.</p><p>This is a pretty straightforward solution, and how much hassle is involved depends on the 3D modeler you&#8217;re using. I re-used a model of a microSD card I recently created in my modeler of choice (Cheetah) and used Cheetah&#8217;s Bullet integration. Bullet is a dynamics engine that supports a range of features, including rigid body and soft body dynamics, and effectors (or forces). These are all standard features of any modern modeling package and you should be able to follow along in Blender, C4D or whatever else you&#8217;re using.</p><p>For this technique to work, each &#8220;particle&#8221; has to be a separate mesh within the scene. This will then show up as a separate mesh object in the FBX hierarchy and mo2. The main problem with this approach is performance. Separate objects perform much worse during the simulation than particle-systems that instantiate the same reference object.</p><p>Before we worry about that though, we need to set up a scene. For this demo I went with:</p><ul><li><p>a floor: a plane</p></li><li><p>a hit target: extruded text</p></li><li><p>an effector: a cylindrical turbulent force effector helps to &#8220;throw&#8221; the SD cards down with a satisfying twist</p></li></ul><p>If dynamic objects are allowed to interact with a complex but static shape, the added disturbance and physical complexity make the result much more enjoyable and communicate to a viewer that this isn&#8217;t hand-key-framed animation. Here&#8217;s my scene setup in Cheetah 8.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!eIPJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1818dd24-2bca-40fb-9ad7-8420622f15b9_1928x814.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!eIPJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1818dd24-2bca-40fb-9ad7-8420622f15b9_1928x814.png 424w, https://substackcdn.com/image/fetch/$s_!eIPJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1818dd24-2bca-40fb-9ad7-8420622f15b9_1928x814.png 848w, https://substackcdn.com/image/fetch/$s_!eIPJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1818dd24-2bca-40fb-9ad7-8420622f15b9_1928x814.png 1272w, https://substackcdn.com/image/fetch/$s_!eIPJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1818dd24-2bca-40fb-9ad7-8420622f15b9_1928x814.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!eIPJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1818dd24-2bca-40fb-9ad7-8420622f15b9_1928x814.png" width="1456" height="615" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1818dd24-2bca-40fb-9ad7-8420622f15b9_1928x814.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:615,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;scene setup in Cheetah 8&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="scene setup in Cheetah 8" title="scene setup in Cheetah 8" srcset="https://substackcdn.com/image/fetch/$s_!eIPJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1818dd24-2bca-40fb-9ad7-8420622f15b9_1928x814.png 424w, https://substackcdn.com/image/fetch/$s_!eIPJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1818dd24-2bca-40fb-9ad7-8420622f15b9_1928x814.png 848w, https://substackcdn.com/image/fetch/$s_!eIPJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1818dd24-2bca-40fb-9ad7-8420622f15b9_1928x814.png 1272w, https://substackcdn.com/image/fetch/$s_!eIPJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1818dd24-2bca-40fb-9ad7-8420622f15b9_1928x814.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The plane and text get a rigid body tag. A tag in Cheetah is an extended object property page, in this case it connects the object it tags to the dynamics engine and sets up the basic physical properties of the object.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3BlR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83224858-d8fa-43e9-b9ca-f1b62d334f1e_1202x448.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3BlR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83224858-d8fa-43e9-b9ca-f1b62d334f1e_1202x448.png 424w, https://substackcdn.com/image/fetch/$s_!3BlR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83224858-d8fa-43e9-b9ca-f1b62d334f1e_1202x448.png 848w, https://substackcdn.com/image/fetch/$s_!3BlR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83224858-d8fa-43e9-b9ca-f1b62d334f1e_1202x448.png 1272w, https://substackcdn.com/image/fetch/$s_!3BlR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83224858-d8fa-43e9-b9ca-f1b62d334f1e_1202x448.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3BlR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83224858-d8fa-43e9-b9ca-f1b62d334f1e_1202x448.png" width="1202" height="448" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/83224858-d8fa-43e9-b9ca-f1b62d334f1e_1202x448.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:448,&quot;width&quot;:1202,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!3BlR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83224858-d8fa-43e9-b9ca-f1b62d334f1e_1202x448.png 424w, https://substackcdn.com/image/fetch/$s_!3BlR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83224858-d8fa-43e9-b9ca-f1b62d334f1e_1202x448.png 848w, https://substackcdn.com/image/fetch/$s_!3BlR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83224858-d8fa-43e9-b9ca-f1b62d334f1e_1202x448.png 1272w, https://substackcdn.com/image/fetch/$s_!3BlR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83224858-d8fa-43e9-b9ca-f1b62d334f1e_1202x448.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The rigid body tag for the floor plane is &#8220;static&#8221;, i.e. it doesn&#8217;t move. The &#8220;concave&#8221; collision model means the faces of the mesh itself are used to calculate the collisions, rather than a bounding box or shaped hull. As the name implies, this is required for concave objects, since other objects may need to penetrate its bounding box, but also increases simulation precision at the cost of performance. Since we&#8217;re not trying to get good realtime performance in Cheetah, this isn&#8217;t relevant - crank everything up. Cheetah has several other precision levels that I needed to turn up to the max, and you&#8217;ll likely have to do the same. Look for parameters like &#8220;margin&#8221;, &#8220;precision&#8221;, &#8220;dynamics sampling&#8221;.</p><p>I later tuned the frequency of the cylindrical turbulence effector until I got a pleasing visual result from the &#8220;throw&#8221;.</p><p>The next step is to manually instantiate multiple copies of the object. I&#8217;d keep it below 100 for use in mo2. This can be scripted if your modeler supports that. Make sure the initial object has the required rigid body properties and material assignments.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!gYzw!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F37c44b98-142b-4bf4-a58d-eb169c199772_792x232.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!gYzw!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F37c44b98-142b-4bf4-a58d-eb169c199772_792x232.png 424w, https://substackcdn.com/image/fetch/$s_!gYzw!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F37c44b98-142b-4bf4-a58d-eb169c199772_792x232.png 848w, https://substackcdn.com/image/fetch/$s_!gYzw!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F37c44b98-142b-4bf4-a58d-eb169c199772_792x232.png 1272w, https://substackcdn.com/image/fetch/$s_!gYzw!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F37c44b98-142b-4bf4-a58d-eb169c199772_792x232.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!gYzw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F37c44b98-142b-4bf4-a58d-eb169c199772_792x232.png" width="792" height="232" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/37c44b98-142b-4bf4-a58d-eb169c199772_792x232.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:232,&quot;width&quot;:792,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!gYzw!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F37c44b98-142b-4bf4-a58d-eb169c199772_792x232.png 424w, https://substackcdn.com/image/fetch/$s_!gYzw!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F37c44b98-142b-4bf4-a58d-eb169c199772_792x232.png 848w, https://substackcdn.com/image/fetch/$s_!gYzw!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F37c44b98-142b-4bf4-a58d-eb169c199772_792x232.png 1272w, https://substackcdn.com/image/fetch/$s_!gYzw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F37c44b98-142b-4bf4-a58d-eb169c199772_792x232.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>A fringe benefit of this is the ability to further specify separate materials. All instances have the same material reference, and mo2 will import it once, and then create links. However, you could later assign different materials to different particles to make it visually more interesting.</p><p>Now it&#8217;s time to bake. How you do this will again depend on your software, but the process I&#8217;m referring to here works like this: In realtime (no bake cache) mode, every time you play the animation time-lime or your current take, the dynamics engine will recalculate everything in real time. Because of random seeds, you&#8217;ll get different results. This simulation only exists in this software though and won&#8217;t be exported automatically with an FBX file. Baking runs the simulation once, and caches the physics state (once per frame). Successive plays will now look identical, as the the dynamics are no longer simulated but rather replayed from the cache.</p><p>If you need to, set up your frame-rate before doing this!</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!YkEo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb047e97b-3c29-478b-ac72-a89e05eed689_1202x560.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!YkEo!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb047e97b-3c29-478b-ac72-a89e05eed689_1202x560.png 424w, https://substackcdn.com/image/fetch/$s_!YkEo!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb047e97b-3c29-478b-ac72-a89e05eed689_1202x560.png 848w, https://substackcdn.com/image/fetch/$s_!YkEo!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb047e97b-3c29-478b-ac72-a89e05eed689_1202x560.png 1272w, https://substackcdn.com/image/fetch/$s_!YkEo!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb047e97b-3c29-478b-ac72-a89e05eed689_1202x560.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!YkEo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb047e97b-3c29-478b-ac72-a89e05eed689_1202x560.png" width="1202" height="560" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b047e97b-3c29-478b-ac72-a89e05eed689_1202x560.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:560,&quot;width&quot;:1202,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!YkEo!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb047e97b-3c29-478b-ac72-a89e05eed689_1202x560.png 424w, https://substackcdn.com/image/fetch/$s_!YkEo!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb047e97b-3c29-478b-ac72-a89e05eed689_1202x560.png 848w, https://substackcdn.com/image/fetch/$s_!YkEo!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb047e97b-3c29-478b-ac72-a89e05eed689_1202x560.png 1272w, https://substackcdn.com/image/fetch/$s_!YkEo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb047e97b-3c29-478b-ac72-a89e05eed689_1202x560.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>One more thing to do: transfer the internal cached states to keyframes.</p><p>mo2 supports basic f-curve animation. Each mesh within an FBX hierarchy can be animated independently, but only the basic transformations are available: scale, rotate, translate. Other software may support vert-based movement, allowing you to bake soft-body physics, but mo2 doesn&#8217;t.</p><p>Again, this depends on your choice of software, but in Cheetah my general approach is:</p><ol><li><p>Group particles into folders (no more than 50 each)</p></li><li><p>Switch to animation view and select a folder group</p></li><li><p>Activate &#8220;key hierarchy&#8221; - this will ensure keyframe record commands propagate to all child objects</p></li><li><p>Key record each frame</p></li><li><p>ungroup all batches</p></li><li><p>(optionally remove physics tags)</p></li></ol><p>The keyframe timeline should then look like this:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!YAH5!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faa39ceae-b091-443e-b597-c9ebfe0b1fd3_2294x560.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!YAH5!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faa39ceae-b091-443e-b597-c9ebfe0b1fd3_2294x560.png 424w, https://substackcdn.com/image/fetch/$s_!YAH5!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faa39ceae-b091-443e-b597-c9ebfe0b1fd3_2294x560.png 848w, https://substackcdn.com/image/fetch/$s_!YAH5!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faa39ceae-b091-443e-b597-c9ebfe0b1fd3_2294x560.png 1272w, https://substackcdn.com/image/fetch/$s_!YAH5!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faa39ceae-b091-443e-b597-c9ebfe0b1fd3_2294x560.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!YAH5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faa39ceae-b091-443e-b597-c9ebfe0b1fd3_2294x560.png" width="1456" height="355" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/aa39ceae-b091-443e-b597-c9ebfe0b1fd3_2294x560.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:355,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!YAH5!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faa39ceae-b091-443e-b597-c9ebfe0b1fd3_2294x560.png 424w, https://substackcdn.com/image/fetch/$s_!YAH5!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faa39ceae-b091-443e-b597-c9ebfe0b1fd3_2294x560.png 848w, https://substackcdn.com/image/fetch/$s_!YAH5!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faa39ceae-b091-443e-b597-c9ebfe0b1fd3_2294x560.png 1272w, https://substackcdn.com/image/fetch/$s_!YAH5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faa39ceae-b091-443e-b597-c9ebfe0b1fd3_2294x560.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>And we&#8217;re done! Now export your FBX scene, paying attention again to set the correct frame-rate in the export tools.</p><p>And then import into mo2, by choosing Add -&gt; Model -&gt; Import. You can bake multiple different simulations as separate takes, and mo2 will pick those up (along with the take name!).</p><p>Here&#8217;s a quick render of the final result:</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;6dc50ceb-e5c8-4029-989f-52d902c7b94b&quot;,&quot;duration&quot;:null}"></div><p>And another earlier test, using cube meshes:</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;8f0a4ba7-ca5b-492c-80a5-2cbb19ef9587&quot;,&quot;duration&quot;:null}"></div><p></p>]]></content:encoded></item></channel></rss>