The LattePanda Sigma

Following up on my testing of the LattePanda and , this time I’m looking at the Sigma, which (as is becoming the norm with my recent pieces) I actually got before summer break.

I wanted to have a look at it because it is something else again in the LattePanda universe: consider a laptop-class Intel motherboard with the case, keyboard, and display removed, but with more accessible I/O than most mini-PCs, and you’re close enough:

LattePanda Sigma retail box
LattePanda Sigma retail box.

And it doesn’t stop at I/O: the Sigma I’ve been testing has a Core i5-1340P and 32 GB of soldered LPDDR5 (which is probably worth its price in gold right now), making it probably the most powerful “single board computer” I’ve ever used.

Disclaimer: DFRobot was, again, kind enough to send me a Sigma free of charge, and as usual, this article follows my .

Even though I generally prefer ARM and AMD for that, there is a lot of Intel hardware in industrial manufacturing, and I see i5 factory floor machines starting to crop up as replacements for older generations already.

But besides summer break, one of the reasons this post is a tad long is that I took a, well… long look at how to use the Xe iGPU for inference, because I realized llama.cpp has an absurdly large range of techniques baked into it that are a cut above what I have on go-pherence–and why not learn from that?

Hardware

At 146x102mm, the Sigma is quite a bit larger than the and the Lite Carrier and feels much more like an industrial motherboard than a hobbyist SBC.

The board is built around Intel’s Core i5-1340P, which was originally meant as a mobile CPU (hence its 28W rating), but LattePanda designed the cooler for a sustained 44W configuration, and that is definitely the first thing you’ll notice when you pick it up:

The single-piece aluminium base of the Sigma
The single-piece aluminium base of the Sigma.

I must say that having the whole thing bolted to an aluminium slab actually made it quite nice to test, since I didn’t have to worry about getting loose screws or other parts under it.

Specs and Features

Removing the aluminium base reveals the mainboard and its components quite clearly, in a very neat layout:

Underside of the Sigma board with M.2 slots, RTC battery and expansion headers
Underside of the Sigma board with M.2 slots, RTC battery and expansion headers.

And yet, there is a lot going on here that is not immediately obvious from the outside or underside:

  • Core i5-1340P, with 4 P-cores, 8 E-cores, 16 threads and 12 MB cache
  • 32 GB LPDDR5-6400, shared with the Iris Xe GPU–one of the things I really like about this board.
  • two M.2 M-key slots, one PCIe 4.0 x4 and one PCIe 3.0 x4
  • two 2.5GbE ports (based on Intel I226-V)
  • two Thunderbolt 4 ports, as well as HDMI 2.1 and eDP 1.4b
  • an M.2 E-key for Wi-Fi/Bluetooth and an additional B-key slot with SATA, PCIe, USB and SIM connectivity (you get some nice thermal pads in the box, too)
  • SATA, internal USB, front-panel, audio and serial headers
  • A barrel jack for 19V power input (the supplied PSU is rated for 90W, which is plenty of headroom, as we’ll see)
  • and, finally, an onboard ATmega32U4-compatible LattePanda Leonardo for GPIO, PWM, analogue input, UART, I2C and SPI (which I didn’t test beyond the hello world check below)

This is also a slightly beefier i5 than I’ve seen before–as an example, compared with the i5-1235U in the , the i5-1340P adds 2 extra P-cores and 4 threads, keeping the 80 execution units in the Xe graphics unit.

Initial Setup and Connectivity

For my tests, I dug out a 1 TB WD Blue drive and an Intel AX210 Wi-Fi 6E/Bluetooth module.

The latter worked so well that I ended up freeing the Ethernet ports and just letting the Sigma work wirelessly (at a link rate of 1.2Gbps), but that is why you can see the Bluetooth device in what otherwise is a pretty sparse USB inventory:

# lsusb
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 003 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 003 Device 002: ID 3343:803a Unknown LattePanda Leonardo
Bus 003 Device 003: ID 8087:0032 Intel Corp. AX210 Bluetooth
Bus 004 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub

And yes, there’s a Leonardo in the USB inventory.

This mix of standard Intel devices and a small onboard microcontroller is still one of LattePanda’s better ideas, and makes it appealing for industrial devices: the host can run an ordinary operating system while the microcontroller handles timing-sensitive tasks and GPIO independently.

As to the PCI bus, I would call it… unashamedly Intel?

# lspci
00:02.0 VGA compatible controller: Intel Corporation Raptor Lake-P Iris Xe Graphics
01:00.0 Ethernet controller: Intel Corporation Ethernet Controller I226-V
02:00.0 Ethernet controller: Intel Corporation Ethernet Controller I226-V
03:00.0 Network controller: Intel Corporation Wi-Fi 6E AX210/AX1675 2x2
58:00.0 Non-Volatile memory controller: Sandisk Corp WD Blue SN580 NVMe SSD

Storage

I didn’t use the SATA interface for testing, and the WD Blue SN580 I used through the PCIe 4.0 M-key slot behaved as expected, but for the sake of thoroughness, I still wanted to document its performance here using fio benchmarks:

Workload Result p99 completion latency
Sequential write, 1MiB, QD32 4.11GB/s 10.68ms
Sequential read, 1MiB, QD1 2.63GB/s 0.57ms
Sequential read, 1MiB, QD32 3.95GB/s 10.03ms
Random read, 4KiB, QD1 10.65k IOPS 0.15ms
Random read, 4KiB, QD32 248.95k IOPS 0.33ms
Random 70/30 read/write, 4KiB, QD32 173.84k/74.55k IOPS 0.62/0.16ms

Nothing unexpected here. I didn’t do a test across more than one slot (NVMes are too precious to wear down on purpose these days), so these figures are for the PCIe 4.0 slot only.

Networking

Both I226-V interfaces use igc and advertise 10/100/1000/2500Mbit/s modes, and I had zero issues saturating the links during my tests. By now, 2.5GbE is just rock solid for most local network tasks, the Intel implementation is reliable, and I wouldn’t hesitate to rely on it for demanding workloads. The only reason I switched to my own Wi-Fi 6E card was because it was a bit more convenient and I could get possible fan noise further away from my desk while I tested thermals.

Thermals

And speaking of that, as you may have seen from the box shot, the board is dominated by its large heatsink and cooling assembly, which covers the CPU and other critical components:

LattePanda Sigma board with large heatsink and cooling assembly
LattePanda Sigma board with large heatsink and cooling assembly.

The only criticism I have of the design is that the slight slope on the heatsink (seen above on the left side of the photo) might make it a bit tricky to design a case that fits snugly (or connects the exhaust to the outside, like I did for the ) and has the potential to turn into a dust trap over time, but otherwise the cooling solution appears very effective indeed.

Neither sensors nor /sys/class/thermal worked as I expected at first, but turbostat could read the processor MSRs and supplied package/core temperatures, effective frequency, package power and hardware throttle counters. But after yelling at rpm-ostree for a bit, I got some very usable temperature data anyway:

# sensors
wlwifi_1-virtual-0
Adapter: Virtual device
temp1:        +39.0°C

lattepanda_sigma_ec-isa-0000
Adapter: ISA adapter
CPU Fan:     1497 RPM
Board Temp:   +36.0°C
CPU Temp:     +43.0°C

acpitz-acpi-0
Adapter: ACPI interface
temp1:        +27.8°C

coretemp-isa-0000
Adapter: ISA adapter
Package id 0:  +41.0°C  (high = +100.0°C, crit = +100.0°C)
Core 0:        +34.0°C  (high = +100.0°C, crit = +100.0°C)
Core 4:        +36.0°C  (high = +100.0°C, crit = +100.0°C)
Core 8:        +37.0°C  (high = +100.0°C, crit = +100.0°C)
Core 12:       +35.0°C  (high = +100.0°C, crit = +100.0°C)
Core 16:       +39.0°C  (high = +100.0°C, crit = +100.0°C)
Core 17:       +39.0°C  (high = +100.0°C, crit = +100.0°C)
Core 18:       +39.0°C  (high = +100.0°C, crit = +100.0°C)
Core 19:       +39.0°C  (high = +100.0°C, crit = +100.0°C)
Core 20:       +39.0°C  (high = +100.0°C, crit = +100.0°C)
Core 21:       +39.0°C  (high = +100.0°C, crit = +100.0°C)
Core 22:       +39.0°C  (high = +100.0°C, crit = +100.0°C)
Core 23:       +39.0°C  (high = +100.0°C, crit = +100.0°C)

nvme-pci-5800
Adapter: PCI adapter
Composite:    +34.9°C  (low  = -40.1°C, high = +83.8°C)
                       (crit = +87.8°C)
Sensor 1:     +45.9°C  (low  = -273.1°C, high = +65261.8°C)
Sensor 2:     +34.9°C  (low  = -273.1°C, high = +65261.8°C)

This was something I wanted to check because the fan was not inaudible (worst case was a persistent whooshing noise, with zero whine), but quiet enough at times for me to wonder if it was on. There’s no stall sensor, apparently, so you can’t directly detect if the fan is stuck or not spinning, but you can still monitor its RPM–and the reason I care about it is that fan monitoring is useful in industrial environments to detect fan failures.

Sustained Load

While doing benchmarks I left intel_pstate, the powersave governor and firmware power limits unchanged, and collected turbostat samples every two seconds through a set of CPU, memory, storage and media runs. And since I did everything in sequence with minimal (but realistic) cooldown periods between runs, the resulting data was so nice that I decided to keep it all in one chart:

LattePanda Sigma package and core temperatures, busy CPU frequency and package power across the benchmark window
turbostat readings every two seconds throughout the benchmark run. Power figures cover the CPU package only.

Low-power operation is amazingly good–the system idles at 39.5C and 2.0W package power, jumping up to 54C and 19.1W (and back down) as needed. During sustained 7-Zip, package temperature averaged 81.3C and peaked at 89C, with power averaging 33.5W and peaking at 39.2W.

At the wall, I measured roughly 44.0W max consumption (including the rest of the system, of course, which the CPU telemetry doesn’t account for).

But (and another reason why I find newer Intel silicon so impressive, and am hoping to look at the Ultra series next), I was actually surprised there was no real throttling. On an i5, which typically would start throttling under sustained load at high temperatures.

Yes, there was a plateau (3.72GHz, give or take), but neither the core nor package hardware throttle counter changed during the entire sweep, and no kernel thermal warnings appeared.

Software

Since the Sigma fits a rather broad range of applications between a personal workstation and a beefy low-power server, I started with the workstation part (by installing , where I did most of my testing) and, later on, popped in a second SSD with to try it out both as a Debian box and as a server.

As you’d expect, both Fedora and Proxmox were completely uneventful to set up, but each is worth a few notes.

Fedora Silverblue

Back in June, I landed on Fedora Silverblue 44 with kernel 7.1.5, which had full support for everything: the Iris Xe uses i915, both I226-V Ethernet controllers use igc, and the CPU runs under intel_pstate. But, more importantly, I got a completely stable Vulkan stack for my compute experiments.

There were no vendor kernels, board-specific image builders or mystery boot partitions involved, so the contrast with the ARM boards I keep testing is almost comical.

And yes, Thunderbolt worked fine too, as did the Leonardo when I did a little hello world test–but I did not test them thoroughly yet.

Proxmox

This was by far the easiest installation, because, well, I cheated.

I had previously installed on the for testing before using its internal eMMC instead, so I just popped that SSD into the Sigma (with a 3D-printed jig to hold the half-length board in place) and, as you’d expect, everything worked perfectly.

With 32GB RAM, two extra P-cores and dual 2.5GbE, this is a really good home server, especially considering the power envelope, so it was a no-brainer to temporarily migrate over a few of the things I am running on the :

  • My instance and two of the CI/CD workers (the other two run on , on-demand)
  • The main piclaw development instance, which is constantly rebuilding itself
  • The test Jellyfin LXC I have been using to develop jellyfin-livetube, which, despite not having local access to the ZFS video storage, performed just as well over Wi-Fi 6 (it is, after all, using a nearly identical Intel Xe chipset, and the network overhead is minimal).
  • The test instance for wrdp, which also had zero issues serving Wayland desktops using H.264 acceleration.

Media Acceleration

This deserves a little more digging into, since VA-API can be fiddly to set up, and getting it to work correctly with Fedora’s media packages when I booted back into it required some tweaks.

In the initial test installation, Fedora’s default ffmpeg-free build disabled the native H.264 and HEVC decoders, so hardware acceleration was effectively unavailable–no H.264, HEVC or even AV1 in sight.

Debian (in the Jellyfin LXC container inside ) “just worked”, which was annoying, so I went back to the all-Fedora setup and got H.264 acceleration working using rpm-fusion.

All around, transcoding 4K content was smooth and didn’t tax the CPU significantly (depending on the particular codec combo, of course, since audio tends to be piped through the CPU). Multiple streams weren’t an issue, either.

In fact, I am currently using the Sigma as my reference machine to develop my wrdp and go-264 projects since it is already proving to be a very capable platform for media acceleration and transcoding tasks.

Raw Performance

One of the quirks of the Sigma is that the i5-1340P has four performance cores with Hyper-Threading and eight efficiency cores without it–that’s 12 physical cores, not 16 equivalent ones. The E-cores are useful, but doubling the thread count doesn’t mean doubling the available compute: first you’re adding SMT threads to the same P-cores, then you’re bringing in a different class of core altogether.

7-Zip makes good use of the extra threads, but the “linear” scaling you’d hope for isn’t quite there.

7-Zip compression, decompression and total rating in MIPS versus thread count
One P-core thread, four physical P-cores, four P-cores with SMT, and all P- and E-core threads, respectively.
Placement Threads Compression Decompression Total rating
One P-core thread 1 5,232 MIPS 4,334 MIPS 4,783 MIPS
Four physical P-cores 4 31,643 MIPS 17,219 MIPS 24,431 MIPS
Four P-cores with SMT 8 41,456 MIPS 24,791 MIPS 33,124 MIPS
All P- and E-core threads 16 62,266 MIPS 43,702 MIPS 52,984 MIPS

Going from four P-core threads to eight with SMT raises the total rating by only 36%, not 100%.

Adding the eight E-cores brings another 60%, so even though all 16 threads deliver the best result, they only manage about 2.2 times the throughput of four physical P-cores, despite having four times the thread count.

That’s useful extra performance, but it isn’t proportional scaling–and neither SMT threads nor E-cores truly replace physical P-cores. But for background workloads like what you’d have on a server or during heavy multitasking, the extra threads can still be beneficial.

As to RAM speed, the Sigma’s LPDDR5-6400 does not disappoint.

I pulled out a little memory bandwidth testing to see how the Sigma handles sustained data transfers–not just the peak theoretical numbers, but what it can actually deliver under load. After futzing around a bit, I hit upon a simple STREAM-style benchmark that uses three 256MiB arrays: Copy transfers data from one array to another, while Triad multiplies values from one array by a constant, adds values from a second, and writes the results to a third. The GB/s figures count the data read and written per second, showing the sustained memory bandwidth the CPU can actually achieve:

Copy and Triad memory bandwidth in GB/s versus thread count
Three 256MiB arrays, using one P-core thread, four physical P-cores, four P-cores with SMT, and all P- and E-core threads.
Placement Copy Triad
One P-core thread 22.31GB/s 25.64GB/s
Four physical P-cores 55.11GB/s 51.93GB/s
Four P-cores with SMT 55.81GB/s 52.88GB/s
All P- and E-core threads 52.74GB/s 57.70GB/s

Four P-cores already saturate the best copy result, so adding more threads doesn’t help much there. The E-cores add around 9% to the best Triad result (probably because they will lean heavily on cache), and it plateaus after that–but it’s nothing to sneeze at.

The upside is that, in practice, the CPU and Iris Xe share this 52-58GB/s pool–there is no separate GPU memory bottleneck, or a need to bucket data across a slow bus.

Local Inference

This is, as usual, where I went totally overboard.

Update: I have tidied up all the tests I did (many of which did not make it into this review, since they were exploratory) into this set of documentation in my private llama.cpp fork, so if you want to reproduce any of this or look into the post-review iGPU work, this is where you will find it.

Based on older experiments on the , I was expecting quite low performance (around 3 tokens/s by default), but I have been tweaking my own fork of llama.cpp, so I had piclaw rebuild it with Clang 22, -march=native, AVX2 and AVX-VNNI support to take advantage of the new CPU features.

Then I downloaded the cool models du jour (Gemma 4, Qwen 3.6 and Ornith–a biased Qwen derivative) and set up a little agentic test suite: Each candidate got the same repository-search agent task (sized to fit under 4K tokens), which prompted the model to do at least one tool call (which when successful was under 200 tokens):

Prefill and generation throughput for three models on a controlled repository-search agent task
Repository-search task results for models with both prefill and generation measurements.

This is effectively one-shotting a 4K context, and, overall, I can’t complain about the Sigma for this kind of workload:

Model Selected CPU profile Prefill Generation
Gemma 4 E4B MTP-3, F16 KV, Flash off 61.19 tokens/s 25.30 tokens/s
Ornith 1.0 35B MTP-2, F16 KV, Flash off 37.53 tokens/s 16.65 tokens/s
Qwen3.6 35B-A3B Q2_K_XL MTP-3, Q4_0 KV, Flash on 28.36 tokens/s 15.34 tokens/s

Gemma 4 E4B was the obvious winner (as it tends to be in low-resource scenarios), and its performance advantage was clear both in prefill and generation throughput–but, most importantly, it can do tool calls right, which is not something every model can do (even though the other two did fine, Gemma is a bit more predictable at that at the expense of actual coding skills).

Ornith ingested prompts faster than Qwen and was slightly faster for short generation, while Qwen caught up somewhat in longer generation scenarios, but they’re fairly evenly matched.

MTP and Other Knobs

Then I began trying multi-token prediction (MTP) to see if it could improve throughput without sacrificing accuracy. If you’re new to this, MTP uses a draft head (i.e., a smaller “model”) to propose several tokens and lets the target model verify them together. It is not a generic “turbo” switch–the useful depth depends on the model, and more drafting can make things slower, but it can significantly boost throughput when used appropriately.

After a couple of hours of trial and error (and with longer prompts), I arrived at the following results:

Model Target only Best isolated MTP result Selected depth
Gemma 4 E4B 12.69 tokens/s 22.01 tokens/s 3
Ornith 1.0 35B 12.80 tokens/s 15.41 tokens/s 2
Qwen3.6 35B-A3B Q2 11.04 tokens/s 14.54 tokens/s 3

Then I started poking at other parameters–including stuff that affects memory usage and computational efficiency, and that doesn’t make sense in CPU-only scenarios. Also, I quickly realized that on this i5, the best way to maximize throughput was to carefully balance the load across the P-core SMT pairs (yes, you can do CPU pinning) without overwhelming the CPU–given what I got from the memory bandwidth tests, that was unsurprising.

But from a user perspective this is still essentially one-shotting prompts, which is cute but not very realistic for agents that need to maintain a large working context over time, so I froze a prompt at about 124K tokens and ran it through Gemma and Ornith in one 131,072-token slot:

Prefill throughput, generation throughput and peak proportional memory use for Gemma, Ornith and Qwen near their tested context limits
Gemma and Ornith used comparable prompts of around 124K tokens. Qwen topped out at 99,104, so its timings are for a shorter prompt.

This was a lot more interesting, and a trifle hotter than the earlier one-shot tests (fortunately, again, the CPU did not actually throttle):

Model Prompt tokens Prefill Generation Peak PSS Peak temperature
Gemma 4 E4B 124,112 22.49 tokens/s 4.49 tokens/s 10.9GiB 90C
Ornith 1.0 35B 124,341 13.14 tokens/s 2.63 tokens/s 23.3GiB 91C
Qwen3.6 35B-A3B Q2 99,104 4.58 tokens/s 2.10 tokens/s 13.4GiB 90C

The thing is, even though Gemma and Ornith handled the large context without crashing, the generation speed dropped significantly as the context grew. Gemma took up less RAM (and sweeping through weights in RAM more efficiently is, ultimately, why it was able to maintain higher throughput), but for coding tasks that require frequent context updates, this is not really a usable speed–consider it the extreme end of what one might tolerate interactively, and yet perfectly good for batch processing or long-form generation where latency is less critical.

Iris Xe Offload

This did not go as expected. I’m used to seeing some improvement with GPU offload, especially for prompt ingestion, but the Iris Xe behaved differently, and I’ve yet to pin down exactly why (but I have some theories).

First of all, there are two main ways to do compute on the Iris Xe: using Vulkan for explicit GPU offload, or using SYCL for a more integrated approach with the CPU. And… they are both quirky, and (initially) worse than the CPU:

Separate comparisons of CPU versus Vulkan synthetic Qwen probes and CPU versus SYCL on the original Gemma and Ornith agent task
Each pair compares CPU and GPU results from the same test. The panels use different tests and scales.

So I just filed that away for future investigation and tried to squeeze more performance out of the CPU path instead.

Going Split Brain

When I came back to this in September, I decided to use a trick that works well with discrete GPUs: let Vulkan handle prefill, then stop the GPU worker and hand the cached state over to the CPU for generation with MTP. This was a particularly fun trick to pull on the Sigma, because:

  • Vulkan handles the heavy lifting for the initial prefill, which is usually the most computationally expensive part.
  • I can just copy across stuff to the CPU in this architecture, without a transfer across PCIe from discrete GPU memory.
  • The cached state goes across as a checkpointed snapshot (not zero-copy, sadly, because, well, Vulkan and conversions, etc., so the handoff still takes time).
  • Follow-up tool turns with a warm cache stay on the CPU, so they don’t pay for another handoff.

Getting that working with Gemma required fixing two fairly fundamental things in my llama.cpp fork, though: There was a stupid bug (probably mine) that clipped things to 16K entries, and (this was the bit where Astra was useful) I had to make Gemma’s compact sliding-window attention cache transferable, preserving its valid padding and checking the text/token state format so the CPU could resume without processing the whole prompt again.

Watching it re-use N-1 cached tokens in an N-token test was quite satisfying–no need to chew through the whole prompt again. But compared with keeping everything on the CPU, the gain was more noticeable in the complete workflow, not follow-up turns:

Gemma CPU-only versus Vulkan-prefill and CPU-generation timings for a complete coding workflow and its warm follow-up rounds
Eight counterbalanced runs in September: 28.8% less time overall and 3.4% more time on warm follow-up rounds, which are included in the total. Lower is better.

And yes, this is not quite a general-purpose speedup: the complete coding workflow took 28.8% less time, but the warm follow-up rounds were 3.4% slower. It helps when there’s a fresh prompt to ingest; once an agent is working from a warm cache, there isn’t an extra turn-by-turn gain here.

I am looking into this further to see if there are ways to generalize the approach or improve the handoff between Vulkan and the CPU, but this is the kind of rabbit hole I can’t really get into these days, so I pivoted to another thing: speech recognition.

Whispering Really Quickly

Why speech? Well, because that Vulkan/CPU hybrid approach for Gemma got me thinking: Whisper has a much smaller model size and lower computational requirements compared to Gemma, and it might benefit from a similar prefill-and-handoff strategy, potentially allowing for faster speech recognition on the CPU after an initial Vulkan prefill. At least that was the theory.

So far, I don’t have pretty charts because the conversion from Vulkan-prefill to CPU generation is still a pain to do on the fly (even segmenting the audio, etc.), but I got Whisper v3 Large working with that approach at eight times real-time speed on the Sigma already without really optimizing anything and just doing the bulk Vulkan to CPU data conversion repeatedly.

I am now porting that into go-pherence, so I’ll likely update this post once I have repeatable benchmarks.

But the short version is that I can run both Whisper and Gemma simultaneously on the Sigma–which in turn means that this might well be a killer home automation server if you want fast speech recognition and simple, efficient language model processing all in one place.

Of course, not being a Home Assistant person (I rely on Siri and , for better or for worse), I can’t really take the next logical steps here, but I do want a local, private speech transcription solution (I would like to to a degree), and the Sigma is working quite well for that purpose already.

Living With It

I’ve since swapped back to the Fedora SSD full time, and have been using the Sigma as a development server for wrdp, go-264, go-pherence, etc. I am thinking of reinstalling on the bigger drive and consolidating most of my development there–including migrating quite a few of my other projects from to it as well, since even though borg has ample storage and RAM, the Sigma is just quieter and more power-efficient.

Verdict

The expandability, built-in 32GB of RAM and the i5-1340P’s core count would already make me choose the Sigma over most Intel mini-PCs I’d otherwise consider. But quiet operation and power efficiency are what make me want to keep using it as a development and home server.

For consolidating services into a custom server build or a small 10-inch rack, I’d also much rather start with this board than stack a bunch of mini-PCs. The two M.2 storage slots and SATA connectivity leave room to expand–if you need more SATA ports, you can use the second M.2 slot with an adapter, and Thunderbolt gives you another option for fast storage. I haven’t tested every expansion option (and, like many times before, I wish I had an eGPU setup to try).

Taking a DIY approach, I appreciate the readily accessible I/O and don’t particularly miss the enclosure (though a proper case would certainly make things tidier).

For now, it’s back on Fedora doing development work while I find the time to design a 3D-printed case that suits me–there are some available, but none is quite what I’d like…

Notes for September 6-12

Even though my personal calendar is now filled with a smattering of fresh events, I am quite happy about the ones that don’t involve health, legal and tax annoyances. My back and other ailments seem to be stabilizing thanks to daily exercise and my having dedicated some time to cooking my own meals, which, besides being cheaper (a concern I’ve been having of late), also forces me to stand and move about, not to mention giving me an excuse for handling very sharp knives in very satisfactory ways.

And I’ve lost weight–either from exercise or minor hardship, that is both undebatable and welcome.

Thank you, OpenAI

My free Codex Open Source plan ended yesterday, which meant I was keeping a close eye on piclaw and a few other projects I wanted to make some significant progress on–as a last hurrah, over the last week I was getting Codex to implement the foundations for family features and budgeting, and to do the usual code audit and refactoring pass.

However, I am very happy to report that I nearly got more free credits, and the reason for the near-miss is pretty awesome: go-264, the SIMD/assembly-based H.264 decoding library that I started building many months ago to quickly extract video frames on low-end hardware without all of the overhead from ffmpeg, is now apparently being used at ChatGPT inside OpenAI.

They reached out, pushed some PRs with fixes and enhancements I had not gotten around to due to lack of suitable hardware, and… gave me an extension to my Codex plan, which expired yesterday.

Sadly, I got a “The promo code is invalid.” error when I finally clicked on the fresh Pro invite this morning, so I guess that’s that (I don’t think you can have multiple extensions back to back, and yes, I made sure I was back on the Free plan before clicking).

Well, that was that
Well, that was that

But hey, it was a good run while it lasted, and it was a nice gesture on OpenAI’s part.

My alternative plans to scout OpenRouter and such for suitable open models that would be minimally good enough are now (again) on, and thankfully I did quite a bit of pricing research. for “stopping” using AI were unrealistic, but timely enough for me to reassess my usage and plan for alternative approaches.

So I am focusing on other stuff a bit more, especially since I do indeed want to lower my screen time altogether.

Reading and TV

With that in mind, I have been fixing my Calibre MCP server and making sure I get in at least half an hour’s reading each night. My propensity for skimming over The Economist every weekend has crowded out regular books a bit and I’ve started building up an arXiv queue again, plus, of course, watching Ted Lasso.

Can’t beat that for a wholesome feeling that nearly makes up for everything else going on.

The iPhone Duo (and the Rest)

Besides the showmanship demonstrated by the keynote opening (nice touches there, Tim) and the visual effect when you open it, the iPhone Duo is… strangely familiar, and yet, somehow just strange as well:

  • I am not a fan of the weird signal/battery status bar indicator or the vertical dock, although the ergonomics of moving toolbars there are undeniable–the bottom-right buttons are going to be very hard to reach single-handedly, though
  • I am a fan of having Touch ID back, as well as the under-display camera (which I suspect will eventually find its way downrange over the next few years)
  • I kept wanting to see the typing experience in “laptop” mode and Apple Pencil support (Apple says support for the USB-C Pencil is coming later this year)

Most of the UX we saw so far was both predictable and familiar to anyone who’s played with a (Apple did not invent screen splits, app swaps or drag and drop), and so was the camera placement.

Oh, and the price, of course. Definitely not a surprise even if shocking.

It won’t stop a lot of people, even if it is about as much as a couple of (sorry, three or four) iPad minis, but I am definitely not getting one–nor any other iPhone this year. Nor AirPods either, given I have excellent open ear $50 earbuds.

But I am interested in the new Watch, both due to the improved heart rate tracking and battery life (the dark bronze look and redesign versus my Series 9 doesn’t hurt either).

I am, however, more than a bit put off by the Live Rewind and Recap features, simply because it doesn’t sit well with me to have anything listening constantly, even if with a short retention window (Apple says these are opt-in and raw audio is deleted after processing).

That said, as it happens I have been investigating how to duplicate the Index 01 smart ring’s (deliberate) press-and-hold recording and transcription for notes with my Watch, so I found the feature intellectually interesting.

But I find it incredibly frustrating that Apple consistently fails to get even the basics right.

For instance, currently my voice memos from the Watch are neither transcribed nor synced to my Mac (Apple says they should sync), and Apple’s unwillingness to do any sort of useful automation on the Watch has become a huge turn-off.

Remember before they acquired it and nerfed it into Shortcuts? Those were the days.

Update: I forgot to mention that, obviously, none of the AI features are going to be available in the EU. Which only makes the lack of flexibility and automation all the more annoying, because I can’t even script sending audio to my own endpoints to process.

Update 2: Apple has released a whitepaper on Audio Intelligence Privacy that outlines how it works and how audio is handled on-device with only condensed, generalized and encrypted transcripts making it to their private cloud compute (where it is actually summarized), and… Well, OK, fine, I guess. It does require opt-in.

Update 3: Yes, I am aware quite a few of the outdoor sequences for some of the new adverts were shot in Lisbon, thank you–I cannot claim any influence in their picking at least 2 spots within walking distance from my house, though…

The Kuycon P20

The desire to own an Apple Studio Display has probably been hanging over most Mac desktop users since time immemorial (well, since 1998 at least, but most people are more familiar with the “modern” 2022-era look) for two reasons:

  • It defines what a desktop Mac display should, ideally, look like–a black-edged, aluminium-backed slab with zero frills and impeccable image quality.
  • It has, traditionally, been hilariously expensive along every single dimension, including polishing cloths, until the historic date of August 27th this year.

And I, too, have always wanted one. But economics and insularity are part of the reason I never got one, although that hasn’t stopped me: I’ve always been curious about what it would be like to use a credible approximation of one, but with a twist: I like taller displays.

And now, I can confirm that this is probably the best single monitor set-up I’ve used for a long while:

Kuycon P20 with iPad and desk setup
The P20 on my living room desk.

Disclaimer: Kuycon supplied me with a P20 free of charge (well, actually two, but that’s part of the story) and this follows .

Getting here took several months. The first panel flickered, and pressing on a corner of the monitor improved things. I suspected a partially disconnected internal connector, but couldn’t confirm the cause, although I very strongly suspect shipment damage.

I returned the first unit, but the replacement arrived without a stand, so I couldn’t use it for many weeks. And then I went on vacation, and that further delayed things…

I finally set it up (again) on our living room desk (the space I now retire to whenever I’m not working, either due to needing clear separation from work or to my 3D printers making the office too noisy) and have now been using it properly for a few weeks.

And (spoiler) it’s pretty great.

Why the P20

First of all, I’ve always been a fan of the Microsoft Surface Studio–not the computer itself (which was far too feeble), but its screen size and 3:2 aspect ratio. Its 4500x3000 resolution was also exactly the same as the Kuycon P20.

My preference for taller displays goes back a long way–all the way back to the huge, monochrome 4:3 DECstations I used in college, and I still prefer those proportions to the modern 16:9 default that we seem to be saddled with in this age of video consumption.

Second, I’ve been using LG panels and a combo and setup at my office desk for quite some time now, largely because ultra-wides, despite their popularity for gaming and video editing, are actually a slow-burning hindrance when researching, writing or coding because of their comparatively limited screen height. I use my portrait display for coding, researching, final page layout stuff, and, increasingly, CAD and 3D modelling.

However, it’s a bit annoying to flip windows to and fro (even with ), so when I realised I had a shot at using a subtly taller, better balanced display with what I still think are the right proportions for me, I jumped at the chance.

Why do I care about this so much? Well, for three reasons, two of which are related to my physical health:

  • I have high myopia, which makes the vertical space on my displays particularly important since anything off-centre, being invariably distorted by my corrective lenses, requires actual neck movement to view properly–even if my widescreen were curved, I would still have trouble seeing it clearly without moving my head.
  • My back and neck have been giving me trouble, so being able to raise the screen to the centre of my visual field matters a lot to me.
  • I simply prefer taller displays for productivity, as they allow me to see more content vertically without constantly scrolling.

Viewing Area

In order to better convey my point of view on this, I had my “assistant” draw up this comparison:

Active display areas of the Kuycon P20, LG ultrawide, LG DualUp and a 14-inch MacBook Pro drawn to the same physical scale
Active display areas, shown at the same physical scale.

The rectangles above are drawn to the same physical scale (which took some doing), leaving out bezels and stands.

I’ve also added a couple of insets showing how 4K and 1080p images fit at native 1:1 pixels, rather than the physical sizes of other monitors. You can futz with desktop scaling to change how much content fits and how large the text is, but the panel proportions stay the same–and the comparison makes those much clearer than diagonal sizes alone.

The P20 has a 28.2-inch IPS panel running at 4500x3000 and 60Hz, with a 3:2 aspect ratio (also written as 4.5:3). That gives it about 22% more pixels than my 5120x2160 LG ultra-wide, arranged in a much taller, narrower workspace–and a bit sharper, too.

In The Box

Kuycon P20 display and stand shipping boxes
Separate boxes for the display and stand.

The monitor and stand come in separate boxes (even though the illustration on the monitor box shows both, the stand is in the box on the right).

Inside the main box, everything’s neatly packed:

Kuycon P20 protected by foam inside its box
The foam insets have all the accessories.

Let’s get this out of the way: Yes, this is very heavily based on what we’ve come to consider Apple design language, from the glossy display, thin black bezels and all-aluminium look to the back vents:

Kuycon P20 rear vents
The rear vents are part of the all-aluminium look.

And yes, that is part of the reason I was intrigued by the P20 in the first place, and a controversial aspect for some, but what matters is what the hardware can do for you, and how.

Mounting Options and Ergonomics

Besides the sturdy aluminium stand (which I like the look of, and which allows the P20 to be rotated 90°, if you want), the monitor itself ships with a stylish VESA mounting bracket:

Kuycon P20 VESA mounting bracket showing the top catch and retaining-screw hole
The top catch holds the mount in place; the screw locks it.

The bracket itself appears to be machined out of a couple of solid plates of aluminium and is well designed in terms of strain relief, but as it happens none of my monitor arms was strong enough to hold the monitor without noticeably straining and drooping (my LGs are, after all, encased in light plastic), so I had to wait for the replacement stand.

The solid aluminium stand holds it steady. It does require some assembly, but nothing overly complicated:

Kuycon P20 stand assembly
Assembling the solid aluminium stand for the P20 takes 5 minutes.

Once the stand arrived, its height adjustment let me put the screen at the centre of my visual field–the top stop is high enough to get it where I want it.

Compatibility

You get 2 HDMI ports, a DisplayPort, and a USB-C port as inputs, plus a USB-C hub with 2 additional ports for peripherals (I plugged a Logitech Brio 4K into one of them) and a headphone jack.

Kuycon P20 ports
The available ports, in line with the power connection at the centre of the monitor’s back panel.

Note that there are no speakers on this monitor (and likely no good place to put them inside the chassis), so you’ll need to rely on external audio solutions.

As you would expect, we plugged everything we could into the monitor to see how it would handle different connections:

  • All our Macs (including old Intel ones) worked without any issues.
  • Our Windows machines, both Intel and ARM, also had no trouble detecting and using the monitor at full resolution (and HDR where supported, which was the usual hit-and-miss in Windows).
  • Our Linux machines also detected and used the monitor without any major issues–I even wrote a Noctalia shell extension for my for managing monitor placement in Niri.
  • The iPad Pro worked seamlessly, as discussed in the next section.
  • And a Chromebook I tested also worked without any issues (except for resolution limitations due to its ARM hardware).

The only input I was unable to test was DisplayPort, simply because we have zero machines in the house with it–most of the laptops and SBCs I used connected to the monitor’s USB-C input, including those with Thunderbolt ports.

My trusty old had no real trouble driving it at full resolution (albeit at 30Hz) via HDMI. I also tried a few USB-C to HDMI adapters, including one of my trusty travel cables, without any significant issues (other than 4K limitations in some of the converters, which was expected).

iPad Use

To put it bluntly, this thing is glorious to use with my . Completely overkill, but also completely right as far as screen proportions and resolution are concerned, even if Apple still provides essentially zero control over any monitor features other than HDR:

iPadOS display settings being its usual dense self
External display settings for the P20 on iPadOS.

I worked on this draft on the P20 itself using my iPad Pro ( on the right of the display, references on the left, and Notes on my iPad just below the monitor), and I would make that a habit if it were easier to manage and split windows on :

iPadOS split-view, also being its usual dense self
Working my way back in time through my notes and references on the P20 with my iPad Pro.

Colour Accuracy

One of my teens, who is just as obsessively attuned to colour as I am, spent an afternoon trying to adjust the P20 to a close match of what our MacBook screens can do and eventually gave up–with or without HDR, our unit had a salmon/pink/warm tint that we just couldn’t get to go away for a while.

I don’t have any colour calibration hardware and I tend to only edit photos on my iPad these days out of sheer convenience (and, let’s face it, due to having given up any pretence at doing “pro” photography over the years), but that tint was something I noticed immediately, even after trying the various DCI-P3 profiles macOS offered in its settings window.

The monitor’s own DCI-P3 and sRGB modes didn’t resolve it either, so we’ve settled on turning off HDR and using its “User” and “Cool” colour settings instead–that and some more judicious tweaking of other settings finally removed the tint, so expect spending some time fiddling with settings on both ends if you’re fussy.

It was impossible to do a true side-by-side with my LG monitors because of plain physics (I don’t have a big enough desk, and swapping one of my monitors for testing the P20 alongside would require me to rewire a bunch of things), but we still haven’t been able to get the colours quite right, in any setup.

HDR

HDR mode was… OK, I guess.

But with HDR enabled, I couldn’t adjust the monitor’s colour settings (and the iPad offered no help there, either):

HDR settings for the Kuycon P20
The OSD in HDR mode on the P20.

Note that I’ve mostly given up on it on computers both due to the wide variation in colour gamuts and the fact that it only really works for me on the Mac (and even then mostly on internal displays), so none of my machines are set to output HDR–but letting YouTube HDR videos play out at 4K on the P20 while I worked provided some uncanny “this feels like a window” moments, even if some of it was over-saturated until I settled on a colour space I liked.

In my tests, switching to HDR made the desktop look washed out, with muted colours on regular screen elements, and switching back made the difference particularly obvious on the P20. Even on the Mac, some YouTube footage was completely washed out in HDR mode–I couldn’t isolate whether that was down to the content, the software or the monitor.

Things I Missed, Occasionally

Feature-wise, the one thing I found myself missing in the P20 was the ability to display two inputs–although it would likely have been pretty awkward in this 4.5:3 aspect ratio, I do use that capability a lot on my LG ultra-wide (to have work and personal machines alongside, in an approximately 4:3 ratio) and on my LG Dual Up (to have my Mac and an SBC, or a TV dongle to watch the news in more troubled times).

The Remote

I was not a fan of the remote:

The remote for the Kuycon P20.
The remote for the Kuycon P20.

My objection is to having another remote at all: it’s the kind of loose end that I invariably end up losing or finding with a dead battery in between the three times I am likely to use it every year (which has been the fate of most non-TV remotes in the house). I already have three of these things for different kinds of portable monitors and gadgets, and those never work reliably.

I’d have much preferred an LG-like five-way switch to another remote to keep track of.

Speakers

There are no built-in speakers, so if you want audio beyond what your computer or tablet provides, you’ll need separate speakers or headphones.

I didn’t miss them myself–the MacBook, my iPad and the Surface Laptop I was working on all have great speakers, and the only mild annoyance was when any of the machines forgot to keep using its internal speakers.

Verdict

The Kuycon P20 on my living room desk.
A reminder that any monitor is no match for sunlight and reflections, especially if it’s glossy…

After all the delays, the P20 has actually been a very good fit for the way I work. I like the height, the resolution and being able to write or research without shuffling windows between two displays, and I particularly like using it with the iPad–so much that it is going to stay on my living room desk (where I retire to focus and write) for the foreseeable future.

And not just for myself, since with college-aged children around, having an extra display in the living room has proven to be extremely useful for everyone in the household–although fights haven’t broken out over it yet.

The colour tint is the only reservation I’d have before recommending it to anyone doing colour-critical work. I don’t have calibration measurements, but both of us could see it, and settling on a cooler preset isn’t the same as correcting it. But we did manage to offset it, so I would rate it as “finicky”.

But for my purposes–writing, coding and everyday use, plus occasional sessions–the proportions are still perfect to my eyes.

Notes for August 29–September 5

As many people have remarked, the moat between ideas and technical execution is narrower than ever, and that has a number of consequences–both when it comes to investment (in this case, personal, although …) and focus. And I think I need to be more selective about what gets my evenings, since I am back at work and already spending too much of my free time building things on a computer.

I Need To Stop Using AI

Why? Because I am right back where I was before my break, spending too little time doing anything but sitting at a computer, or using a computer to do things that only work on a computer.

I keep having ideas, and even though I finish my workdays achy and drained, I inevitably get caught up in the excitement of being able to build stuff quickly. And since my free Codex subscription is due to end this month, I’m trying to make the most of it…

I need to get back to electronics, hardware, and reading (maybe even music) as a hobby, and stop spending so much time glued to a screen–which is something I have been trying to balance against my writing backlog, and part of the reason the notes this week are relatively sparse.

But there are some hints of progress in finding a better balance between screen time and other activities:

More Time In The Big Blue Room

I finally hit my daily step goal for several days in a row, which has been a small but satisfying victory and has eased some of the strain from sitting at my desk for long periods (albeit not completely).

The trick? Doing the grocery shopping myself. You end up walking more than you would if you relied on delivery, and it adds a bit of variety to your daily routine. Not to mention milk carton lifting, which is a surprisingly effective upper-body workout.

Review Backlog

I spent some time (re)setting up test machines and going through my review backlog, trying to catch up on notes and tasks that had accumulated while I was focused on other projects–even though I have little to show for it in terms of output, I managed to at least organize most of my notes, some of which date back several months now…

A Legion Of My Own

As an offshoot of the work I did with , I’ve been poking at two related problems for a while: getting agents to preserve their state, and tackling the holy grail of freeing computing environments from being tied to a particular machine.

legion takes that idea and gives it a twist. Rather than packing everything into one portable process, it is a self-hosted runtime for durable agents backed by a Raft cluster, with WASM and Bun functions (making those available across the whole cluster is still work in progress):

  • every agent turn is event-sourced
  • nodes form a Raft cluster
  • code is deployed as content-addressed WASM modules or Bun bundles
  • it’s all organized as a 9P namespace

The namespace provides a uniform way to manage the whole thing, and was, I confess, a whimsical choice. But my intention is that crashes, restarts and individual machines going away should not make an agent stop or lose track of ongoing work, and so far this seems like a moderately sane way to do it.

This is still at an early design stage and might end up going nowhere, but there are already enough runnable examples to exercise most of the architecture:

Legion Web Chat showing a durable conversation across reconnects
Legion’s web chat reconnecting to a durable conversation

Keeping Track of Meeting Agendas

I had an epiphany about how to keep track of meeting agendas without relying on my memory alone: give the computer the talking points and have it listen for what we’ve already covered, leaving me to pay attention to the conversation.

And yes, [Teams] does that, but I wanted something that works entirely on my Mac and was fully local, without relying on any cloud services, plus I was curious to see how far I could push Apple’s on-device models for better privacy and responsiveness.

Which is why swift-smart-prompter started as a little demo of how much useful work Apple’s on-device speech and language models can do, even on relatively low-end hardware.

Then , and it grew into a Mac app that listens to both sides of a call, tracks which topics have come up and suggests a short next response in a floating cue panel (which I can keep above the meeting window, instead of continually looking away to consult my notes):

Sometimes I am just too tired to keep track of meeting agendas
Sometimes I am just too tired to keep track of meeting agendas

And, of course, there’s a hack: instead of using diarization, I rely on the audio split: ScreenCaptureKit gives me the microphone and system audio separately, so the transcripts are labelled “You” and “Call”–it doesn’t try to distinguish individual people at the other end at all.

And although I’ve been hacking on on-device translation and semantic understanding, that’s still a bit slow, so topic matching has a keyword-based fallback, with Apple Intelligence adding contextual cues and coverage classification that so far seems to work regardless of the language being spoken. Mostly. Ok, for English and Portuguese, at least for now.

Plus, I can manually correct the checklist when it gets things wrong. Without Apple Intelligence, it still shows the next uncovered point.

It all stays on the Mac, with no cloud API or account, and neither audio nor transcripts are written to disk. It does need macOS 26 and the appropriate on-device speech models, and multilingual use is a bit of a chore, since you have to fish around to enable everything.

Poor Man’s Screen Sharing

On a more pragmatic note, I have a “me” problem with Azure Virtual Desktop: meetings happen inside it, but sometimes I need to share something that is on my local desktop, which the remote machine obviously cannot see.

My poor man’s fix is provisionally called ShareCam: select a region locally and pipe it into AVD as a camera. Crude, but it works, and once I clean it up I will put it up on GitHub someplace:

Sharecam selecting a region of the local desktop to share inside Azure Virtual Desktop
Selecting part of my local desktop to send into Azure Virtual Desktop

And yes, this shows up instead of my camera view, but that is perfectly acceptable for my use case. The only real challenge is that screen sharing typically has a dedicated pane in Teams and Zoom, whereas my solution just replaces the camera feed and people have to arrange it themselves accordingly.

This was also based on rcarmo/swift-app-template, which I’ve been tweaking to build all my new little Mac tools. It took me all of… 25 minutes to get it working end-to-end, including a second pass for individual window selection and capture that still needs some UX tweaks. gpt-6-astra did the core implementation in less than 15 minutes–having the template ready certainly helped.

And this, I think, is what I should be doing more of: investing my personal development time in satisfying life hacks and little tools I will actually use instead of trying to boil small oceans–those I can always keep doing at work…

Not a Joke

I have a long and somewhat embarrassing history with that goes back to the years when it was fading from academia and LISP machines were giving way to and DECstations (a transition that should be familiar to anyone who read the UNIX-HATERS Handbook), so I got properly hooked on when it came out.

But had the huge disadvantage of being tied to the virtual machine. That dependency was also one of its superpowers, but I always saw it as its biggest flaw.

Despite that, I used it in production for a few years and have been mourning the fact that you can’t have it without the ever since. On a or an ARM SBC, starting a just to run a REPL feels like bringing a shipping container to a picnic.

Joker was the escape hatch I’d been looking for–a standalone binary that understands most of syntax and needs no external runtime. I started using it for scripting and linting years ago, and when I began thinking about building gi (my own lightweight coding agent), embedding Joker as the extension language was the obvious choice.

There was just a tiny little problem: it was slow. Not “a bit slow”–it was orders of magnitude slower than on anything involving loops, arithmetic or recursion. Fine for linting, but useless for anything else.

I forked it and spent two very intense days making it fast over a bank holiday. I’ve written about go-joker before, including :

go-joker notebook with Mandelbrot rendering
The Go-Joker notebook rendering Mandelbrot through the WASM-backed imaging path.

But I recently realised that I never really put together all of my notes from last April, and it’s long overdue to write a proper post about it, so here it is.

The Approach

My deep, dark past poking at the (did you know that had one audited externally in Europe? Ask me how I know) and my limited time working on internals–plus a lot of reading about the tiered compilation–all told me the same thing: the path from “slow interpreter” to “fast interpreter” follows a fairly predictable arc. First you identify the hot paths, then you lower them to a simpler representation, then you specialise that representation for the common types. If you’re lucky, you can go further and compile to native code for the innermost loops.

This isn’t something I’ve done often (not for a few decades, really), but I used to discuss it with one or two compiler nerds I worked with ages ago–we had long, weird phone calls about gcc, of all things–so I had an idea of how to do it.

The trick was getting a coding agent to do most of the mechanical work while I steered the architecture. I had gpt-5.5 implement each layer while I provided the design constraints, which initially boiled down to:

  • flat bytecode
  • register-based execution
  • no heap allocation for primitives
  • a tree-walker fallback for anything weird

A few hours of thumbing through ancient books, interspersed with liberal swearing and infected by relative madness, eventually got me to a tiered execution engine:

The final thing, in a very rough sketch
Go-Joker’s tiered execution pipeline, including WASM, typed IR, boxed IR and tree-walker fallbacks.

Each tier handles what it can and drops to the next for anything more complex. Early on, execution would start in the tree-walker and be promoted as the interpreter recognised patterns it could optimise, and the implementation grew progressively more intricate from there.

And since I needed something to compile, I went out and grabbed The Computer Language Benchmarks Game, which has a range of computational scenarios that resist trivial optimisation (to a degree), along with ready-made comparisons.

Phase 1: IR Bytecode

The first step, heavily inspired by , was compiling hot loops and functions to flat bytecode–an intermediate representation with fixed-size opcodes, a value stack and no allocation for integer/float operations. This alone got mandelbrot from 450ms down to about 40ms. The key insight (which I stole from the JIT literature) was that most loops are either purely numeric or purely structural–they rarely mix–so you can have a typed path that handles Int/Double without boxing and a boxed path for everything else.

And my old JVM tricks also paid off: stripping Int and Double down to single-field structs (8 bytes, stack-allocable) cut allocations by half across the board. That is the kind of change an won’t suggest unless you ask very specifically, because it breaks the type hierarchy in ways that it “feels” are wrong until you measure.

Phase 2: WASM Compilation

The realisation that pure numeric loops could go further came when I noticed that wazero (a pure- runtime) could JIT-compile to native code–with zero cgo, another requirement of mine. If the tree-walker detected a loop that was purely integer/float arithmetic, we could emit WASM bytecode for it, hand it to wazero and get native-speed execution (well, almost) without leaving the process.

This was huge fun: the arithmetic benchmarks went from 12ms (IR) to 0.24ms (WASM), giving what used to be a interpreter pretty much / speed. It’s limited–it only handles cases where every value is a known numeric type and there are no collection operations–but when it applies, it’s great.

Phase 3: Polishing

The rest was just grinding out the hotspots. Per-instance function compilation caches (irGetFnProg), capture-slot optimisation for closures (captureSlotSet), a StringCursor native type for zero-allocation string iteration (because, well, it was getting embarrassing to append stuff to strings…), transient vectors for non-escaping loop mutations and tail-call rewriting at parse time–I had to ask piclaw to check the ordering, but this was all done by systematically going through the benchmarks.

Given my fondness for profiling, I wanted this thing to be self-diagnosing, so I asked gpt-5.5 to add a runtime introspection namespace (joker.runtime) so scripts can inspect their own IR, output, escape analysis and allocation profiles.

By the end, I had some pretty nice results:

benchmark comparison
Current CLBG and micro-benchmark results in milliseconds per operation; lower is better.

Why This Matters (for me, at least)

The original goal wasn’t really to build a fast (well, not this fast, at least), but as usual I wandered off big time. Eventually I had to get back to what I wanted in the first place: an extension language for gi that:

  • compiles into the binary (no external runtime)
  • starts instantly (no , no Node.js)
  • is fast enough for real work (not just config parsing)
  • has a REPL for interactive debugging
  • can introspect its own execution

I now have all five. Scripts and extensions for gi can be written in , stored in the database alongside everything else, and executed at speeds that range from “competitive with ” to “competitive with JIT-compiled ”, depending on the workload.

I’m not doing anything with gi right now, but the above is close enough to the dream that I still use go-joker quite frequently.

The AI Angle

Two days–that’s how long this took, from “Joker is too slow” to “Joker beats on Mandelbrot.”

I could not have done this in two days without AI–the mechanical work of implementing 30+ IR opcodes, writing typed dispatch paths, plumbing emission, and generating benchmark harnesses would have taken weeks by hand.

But (and this is the interesting bit for me) I also could not have done it with AI alone–the architectural decisions (tiered execution, typed vs. boxed split, for numeric leaves, the fallback chain) came from knowing how the and work internally, remembering that I had a copy of Smith & Nair and the wazero source (kudos), and spending years thinking about what makes interpreters fast and (let’s face it) taking a few shortcuts.

All in all, I think this ratio of thinking to execution (and, by the way, go-joker comes with a massive battery of tests I would never have thought of writing) is what I want to get out of most of my projects. It’s never going to be as popular as the in , but it was a lot of fun.

My E-ink Summer Report

I’m now back to work and already embroiled in far too much, so I’m going through my infinite set of mental checklists and trying to relax by offloading some of the stuff I never got around to writing down. While I was on vacation, one of the things I did was tune out (as much as possible), which meant relying more on my e-ink devices.

They look great together
They look great together

Nomadism, Redux

I’ve written a bit about , but that was almost a year ago, and in the meantime there have been quite a few software updates–plus additional tweaks of my own.

But the interesting thing is that none of that changed the way I use the device. It’s definitely replaced my for nearly everything (and more). It’s been by my bedside for a year and a half now, and I still use it to take notes and catch up on the news. Catching up on the news has become somewhat more frequent, as I don’t need (or want) the additional features (or distractions) afforded by my iPad.

I haven’t used it more even though I do most of my reading on it, but I also haven’t used it less, despite a fairly long hiatus in my regular reading habits. It is very handy, virtually weightless when detached from its cover and frictionless for noting down stuff–even during work calls, when I mostly take notes to reinforce my memory of the event rather than for accuracy.

So it was a no-brainer to take it along for a week in Spain, but the new environment reminded me of three shortcomings:

  • The lack of a front light makes it hard to use for late-night reading marathons (and it’s still the thing I miss the most, even if I love how light and crisp the screen is).
  • It can be slow (and crashy) when running Android apps (the Kindle app crashed a few times).
  • I still can’t read or annotate PDFs on it comfortably (going “back” to it instead of an iPad makes the screen size and speed differences pretty obvious).

I still wish it had a fingerprint reader, too. And, these days, I would probably appreciate a microphone for privacy-respecting dictation (I haven’t played with Bluetooth on it enough to tell if that would be a viable option).

But a bit of hacking on picoflux made it trivial to read the daily news comfortably over hotel Wi-Fi, and there are a lot of small hacks that I can pull off, even if the base system is already well polished. And even though I don’t use it often as a regular tablet, , , Termux and let me do the few things I need to keep tabs on my home lab and “regular” notes.

The Mightiest Tiny Book

But based on screen time alone, the e-ink device I used the most on vacation wasn’t the Nomad–it was the .

I installed a nightly build of CrossPoint Reader on a whim to see if it fixed my Wi-Fi problems (spoiler: 1.5.0-rc didn’t, 1.6.0-rc apparently does now that I am back). The X4 worked perfectly, and I carried it almost everywhere we went.

At this point, I would consider it the perfect travel and beach gadget. It is easy to pick up/put down/put away, has absolutely zero distractions, and gave me everything I needed for entertainment and learning. My 3D-printed hard cover protected it, and I added a little rubber USB-C plug to keep sand out.

Toning Down

Vacations and trips are self-contained periods when it’s pretty easy to cut down on noise and distractions by shifting to different devices for quieter, more focused downtime. It doesn’t surprise me that this went so well.

And although it is impossible to gauge how much time I’ve spent on e-ink devices over the year, I want to spend more time with them as I try to stay focused and productive. Doing more on low-end devices is going to be harder, of course, but the quality of the downtime also matters, and at least I can build my own solutions.

For instance, I’ve been making a few notes about improvements to my OPDS and syncing server, and I’ve been meaning to investigate Bluetooth audio and put together a dictation plugin for the Nomad–the SDK seems straightforward enough…

Notes for August 24–28

I came back to work this week, started catching up on everything, and decided to go out for groceries without an umbrella (it is still August, right?) and got drenched.

Read More...

Notes for August 17–23

Tomorrow I’m going back to work after a rather lopsided summer break (not feeling up to it physically or mentally), and the social network pendulum is swinging back into a bit of normalcy as I progressively tune nearly all of them out.

Read More...

Apple Papercuts Summer Edition

This is a short follow-up to my piece, wherein I bunched together a few more annoyances that I’ve come across while using my extensively on vacation.

Read More...

Building Piclaw on Top of an Opinionated Coding Agent

I’ve spent the better part of six months building piclaw–my personal AI assistant, workspace and, occasionally, agent swarm–on top of Mario Zechner’s engine, and I think it’s time to write about not just my motivation but also how I feel about having invested that much time into the whole thing.

Read More...

The Age of Infinite Software

I am most definitely not the first person to mention this, but the past year has seen a Cambrian explosion of two things: AI deniers who base their judgement on very limited exposure (or effort to use it) and thousands of variations on software of all kinds, from the perennial to-do list to AI-infused toothpicks.

Read More...

Notes for August 9–16

This was a different week, partly because we decided to watch the eclipse from a Spanish beachfront and partly because I actually read three books. There is an entire sub-thread around my back and neck aching worse than ever and my sleep patterns looking like a game of Splatoon that I will spare my readership, though.

Read More...

Notes for August 3–8

I have a problem. Or, rather, I have two problems:

Read More...

Archives3D Site Map