00Work & projects

Projects that
escalated.

None of these started as a plan. Each one started as a small annoyance that turned out to be interesting, and then it was three in the morning and I was reading a forum thread from 2014 about refractive index.

Close-up of three glass cubes on a mirrored shelf — the edges are visibly rounded, black numerals sit inlaid in each face, and light refracts through the bodies onto their reflections.
Rounded edges are the SDF, not a bevelled mesh. Numerals are inlaid black enamel.
The play area — coloured glass cubes piled in a lit box, each casting a coloured shadow onto the shelf below.
Coloured shadows: light keeps the tint it picked up passing through.
01iOS · Shipping September

I gave a puzzle game
real ray tracing.

A drop-and-merge game where the pieces are physical glass. Not sprites of glass — actual refraction, computed per pixel, on the phone's ray tracing hardware. Light bends through each cube, picks up its colour on the way past (Beer–Lambert absorption), and lands on the shelf as a coloured shadow.

The cubes are not a mesh. Each one is a signed distance field the ray marches to find the true rounded surface, which means the edges are mathematically round rather than faceted, and the code knows the exact distance light travelled inside the glass.

At the edges a six-wavelength spectral fan splits the light into visible prism fire — but only at edges and corners, because that is the only place real glass does it.

Renderer
Metal, hardware RT
Target
120 Hz locked
Requires
A17 Pro or newer
Fallback
Full SceneKit path

The bug I chased twice before diagnosing it. Cubes looked like they had square corners. I twice guessed at the geometry and twice was wrong. So I built a debug view that paints each ray branch a different colour — and it showed a clean rounded arc. The geometry was never wrong. At a cube's silhouette the surface is edge-on, so Fresnel drives it to pure reflection — and my environment had nothing to the sides to reflect. The rims rendered near-black, and a dark outline reads exactly like a hard edge. The fix was adding studio light panels outside the camera's view. Diagnose, don't guess.

Metal / MSLHardware ray tracing SDF raymarchingMetalFX SceneKitSwiftUI
02macOS · 227 commits in 16 days

A photograph you can
look around.

Take a flat photo. Get back a gaussian splat — a 3D scene made of millions of soft coloured blobs — and display it on a Looking Glass holographic screen, where it has real depth you can lean side to side and look into.

The display needs 66 views of the scene at once, rendered into a single 4092×4092 "quilt" that its lenticular lens fans out across your eyes. I measured the whole path end to end: 270 fps, about 3.7 ms a frame, roughly four and a half times the budget — so the scene can be dragged around live rather than pre-baked.

Most of the real work was measurement. I built an offscreen renderer that runs the display's own lenticular shader and hands back the exact pixels the glass would show, so bugs could be seen without photographing the hardware.

Views/frame
66
Measured
270 fps
Quilt
4092 × 4092
Own code
~16,000 lines

The finding that saved the project. The SDK kept rejecting my render target with a message about pixel formats. It was not a format problem — I tested five and all five failed identically. The texture has to be allocated by the SDK itself, because it wraps it as a shared surface internally. Same format, different allocator, zero rejections. The error message was pointing at the wrong thing, and measuring 10 draws across 6 configurations is what proved it.

MetalGaussian splatting Looking GlassSwift Photogrammetry
03Raspberry Pi · Running on the wall

A wall panel that feels
like an appliance.

A permanently mounted touchscreen that controls the house's music. The goal was never "a dashboard" — it was something that feels like a $400 product you'd buy, not a hobby project running in a browser.

A 5.5" AMOLED panel rotated to landscape, driven by a Raspberry Pi 5 booting straight into the app with no desktop ever visible. Behind it, the album art is sampled for its dominant colours and those drive a WebGL background of seven drifting, breathing clouds that shift with whatever is playing.

It talks to the speakers locally over the network, not through anyone's cloud, so it keeps working if the internet doesn't. The next-track artwork and its palette are fetched before you press skip, so the transition never stalls.

Panel
5.5" AMOLED
Control
Local UPnP
Frontend
Svelte 5 + WebGL2
Boot
Straight to kiosk

Two days lost to a deploy that reported success. The code lives in iCloud Drive, which keeps files as placeholder stubs until something reads them. The sync tool read the stubs, cheerfully reported bytes transferred, and delivered every single file at zero bytes to the Pi. The fix is to force the files to materialise before packing them. A tool reporting success is not evidence that it worked.

Raspberry Pi 5TypeScript Svelte 5WebGL2 Sonos UPnPsystemd
04Swift + Python · Honest result

I built a model to beat
Vegas. It lost.

I play in a college football confidence pool — rank ten games, score that many points for each one you get right. I've won it twice. The obvious move was to replace my spreadsheet with a real model.

So I built one. A leakage-aware training set of 6,398 games across nine seasons, walk-forward backtested. Then I added the features that supposedly hold an edge: rest days, travel distance, time-zone changes, rankings.

The edge features contributed −0.02 points a week. And simply ranking games by the betting line beat both models outright — 44.07 points a week against 43.55. Feature importance was blunt about why: the closing line already prices in everything, because thousands of people with money at stake got there first.

I could have shipped it as "AI that beats Vegas." I'd built the thing that proved that claim false, and shipping it anyway would have been a lie with a nice icon on it. So the app was reframed around what's actually true: the market ranking is provably the best available, and the value is automation, honest calibration, and a genuinely good native app around it.

Training set
6,398 games
Market
44.07 pts/wk
Best model
43.57 pts/wk
Verdict
Market wins

Why this is the project I'm proudest of. A negative result you actually publish is worth more than a positive one you had to squint at. The calibration work was real — the win-probability curve was refit on all 6,398 games and the old one was measurably overconfident. That's a genuine improvement. It just wasn't the improvement I set out to find.

Swift / SwiftUIPython scikit-learnOn-device LLM WeatherKitSwift Charts
05Fabrication · Ongoing

It didn't stop
at a seat.

The sim isn't a racing game — it models tyre temperature, grip state and downforce in real time, and hands all of it back as live telemetry. So the car already knows your left front is about to let go. The project is getting that onto your body before you'd have read it.

Five haptic transducers, each with its own address: engine and gearshifts through the seat, lockup in the foot that's causing it, kerbs in the shoulder on that side. Two wind pods with Bell–Mehta nozzles. A harness that tightens across your chest when you brake. And because the cockpit has no extrusion to bolt to, every mount doing all that had to be designed and printed from scratch.

Wheelbase
Moza R12 V2
Display
Vision Pro / CloudXR
Haptics
5 channels
Structural
ASA

The datum I had wrong. Early brackets referenced the wheelbase's fin-tip plane, because that's the obvious surface. The real datum is a flat annular ring at Y = 23.44 — fin tips are neither coplanar nor continuous enough to locate a bracket with a vibrating fan hanging off it.

Read the full build

06Also on the bench

Smaller, still
overdone.

The smart home

Automations built for local control wherever possible, so nothing important depends on someone else's cloud staying online — or their company staying solvent. The test is whether it still works with the internet unplugged.

Printed things

Replacement parts for objects the manufacturer abandoned, brackets for things never meant to be mounted, and a number of objects that did not need to exist. Some end up in the shop.

The home lab

Self-hosted where it makes sense, networked properly, and documented enough that the version of me troubleshooting it at midnight in two years has something to go on.

Building something
similar?

I do the research part for other people too — a full parts list, in the right order, with the mistakes already removed.