This commit is contained in:
parent
0be50b6c24
commit
2c37e7fa62
39 changed files with 410 additions and 397 deletions
|
|
@ -49,3 +49,10 @@ jobs:
|
|||
apt update && apt install -y rsync
|
||||
mkdir -p /pages
|
||||
rsync -a --delete dist/ /pages/schmelczer-dev
|
||||
|
||||
- name: Copy build to staging pages mount
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
apt update && apt install -y rsync
|
||||
mkdir -p /pages
|
||||
rsync -a --delete dist/ /pages/schmelczer-dev-staging
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: A 50 FPS Game Engine on an ATtiny85
|
||||
description: Building a tiny embedded game engine around an ATtiny85V, OLED display, IR input, EEPROM persistence, and a custom PCB.
|
||||
title: A 50 FPS Game Engine on an 8-Bit Microcontroller
|
||||
description: A handheld game built from the PCB up — ATtiny85V, OLED, IR receiver, EEPROM, 8 MHz 8-bit ALU. 50 FPS floor.
|
||||
date: 2026-05-06
|
||||
projectPeriod: 'Spring 2020'
|
||||
thumbnail:
|
||||
|
|
@ -8,9 +8,9 @@ thumbnail:
|
|||
alt: The Ad Astra game running on a small OLED display.
|
||||
tags: ['embedded', 'games', 'systems']
|
||||
role: Hardware and firmware author
|
||||
stack: ['C', 'ATtiny85V', 'OLED', 'EEPROM', 'PCB design']
|
||||
scale: 8-bit microcontroller, 8 MHz clock, 15-20 ms maximum frame times during gameplay
|
||||
outcome: A working low-power handheld game engine and game built from the circuit board up
|
||||
stack: ['C', 'ATtiny85V', 'SPI OLED', 'IR receiver', 'EEPROM', 'KiCad']
|
||||
scale: 8 MHz, 8-bit ALU, ~31 mW at full brightness, ~1.5 mA standby, 15–20 ms frame budget
|
||||
outcome: A handheld built from schematic to firmware, with a 50 FPS game on it
|
||||
audience: technical
|
||||
links:
|
||||
- label: Source
|
||||
|
|
@ -22,42 +22,31 @@ media:
|
|||
mp4: /media/video/ad_astra.mp4
|
||||
captions: /media/video/ad_astra.vtt
|
||||
alt: Video demonstration of the embedded game running on a small OLED display.
|
||||
caption: The game engine ran on an ATtiny85V with an OLED display and IR input.
|
||||
transcript: No spoken dialogue. The demonstration shows the Ad Astra handheld board running its OLED game, with the player moving through the small display while the IR input controls gameplay.
|
||||
caption: The whole thing — board, firmware, sprites, game loop — runs on a single ATtiny85V at 8 MHz.
|
||||
transcript: No spoken dialogue. The handheld board runs its OLED game; the player moves through the small display while the IR input controls gameplay.
|
||||
---
|
||||
|
||||
Ad Astra came from wanting to combine graphics and microcontrollers without hiding behind a large development board. The result was a small embedded game engine and game built around an ATtiny85V, an OLED display, IR input, EEPROM persistence, and a custom PCB.
|
||||
**The short version:**
|
||||
|
||||
The fun part was that every layer mattered. The circuit, display driver, memory layout, object model, sprite tooling, and game loop all had to fit inside a tiny system.
|
||||
- A handheld game built from the PCB up: ATtiny85V, 0.96" SPI OLED, TSOP4838 IR receiver, 3.3V regulator, battery.
|
||||
- 8-bit ALU at 8 MHz. Every byte and every cycle has a price tag, and that's the whole point.
|
||||
- 15–20 ms peak frame time, so gameplay never drops below 50 FPS. Power draw ~31 mW peak, ~1.5 mA standby.
|
||||
|
||||
## The Problem
|
||||
## Why the PCB changed the project
|
||||
|
||||
The hardware setup was intentionally constrained: an ATtiny85V, a D096-12864-SPI7 OLED display, a TSOP4838 IR receiver, and a 3.3V regulator. The system was low power, with peak consumption around 31 mW at full brightness and a standby mode around 1.5 mA.
|
||||
I'd done microcontroller work on dev boards before and it always felt like I was renting the hardware. As soon as I had a real board with my soldering on it, bugs stopped feeling like software inconveniences and started feeling like consequences of choices I'd made in KiCad. That shift was most of the value of doing it this way.
|
||||
|
||||
Those numbers made the project feel physical. Performance was not an abstract target. Every frame and every byte had a cost.
|
||||
The constraint that mattered: an 8-bit ALU at 8 MHz, with no FPU, no SIMD instruction set, and 8 KB of flash. Anything I built had to fit inside that, or I'd be staring at a brick.
|
||||
|
||||
## Constraints
|
||||
## The bits worth showing
|
||||
|
||||
The engine ran at 8 MHz on an 8-bit ALU. That meant the display driver and game loop had to avoid expensive generality.
|
||||
- **SIMD-on-an-8-bit-ALU display driver.** The OLED is 128×64 monochrome, 1024 bytes per frame. The driver packs four pixels into a byte and processes them with bit-parallel tricks. That's how the frame budget stayed under 20 ms with room for game logic.
|
||||
- **Prototype-based inheritance, in C.** Entities share behaviour by pointing at a struct of function pointers. No vtable, no class, no allocator. Cheap dispatch and the whole object model fits on one screen.
|
||||
- **Atomic EEPROM commits.** Sprite data and save state both live in EEPROM. The commit path writes a new region, then swaps a tiny header pointer. Pull the battery mid-write and the previous version is intact.
|
||||
- **PNG-to-C sprite pipeline.** A Python script turns PNG artwork into static C arrays the firmware can include directly. Asset workflow without ever leaving the source tree.
|
||||
|
||||
Even the programming model needed restraint. I wrote the firmware in C, but used a balance of structured and object-oriented ideas to keep game object behaviour manageable without paying for a runtime that did not exist.
|
||||
## What I'd change
|
||||
|
||||
## Design
|
||||
|
||||
The display driver was the most performance-sensitive layer. I used SIMD-like techniques on the 8-bit ALU to process four pixels at once. That helped keep maximum frame times between 15 and 20 milliseconds during gameplay, so the lowest gameplay frame rate stayed above 50 FPS.
|
||||
|
||||
For game objects, I used prototype-based inheritance. It was a pragmatic way to reuse behaviour while keeping the implementation simple enough for the target.
|
||||
|
||||
Persistent state used the built-in EEPROM with an atomic commit approach. Sprite data also lived in EEPROM, and I wrote scripts to convert PNG sprites into C array definitions so assets could move into firmware cleanly.
|
||||
|
||||
## What Worked
|
||||
|
||||
The project worked because the abstraction level stayed close to the hardware. The engine had reusable pieces, but none of them pretended the platform was larger than it was.
|
||||
|
||||
The custom PCB also changed the project. Once the system had a real board, bugs felt less like software inconveniences and more like design consequences. That made the final result much more satisfying.
|
||||
|
||||
## What I Would Change
|
||||
|
||||
Today I would write a more explicit development log around the display driver and persistence layer. Those are the parts that still feel technically interesting, and they deserve diagrams and measurements.
|
||||
|
||||
I would also add a small emulator or host-side harness. Debugging firmware directly on constrained hardware is useful, but a fast feedback loop would have made the engine easier to evolve.
|
||||
- **A host-side emulator.** Debugging firmware directly on hardware was character-building and slow. A small SDL-based simulator linking the same C code would have shortened the iteration loop from "reflash and hope" to "rebuild and run."
|
||||
- **Power numbers I'd actually trust.** I have peak and standby draw. I don't have a curve over a real gameplay session, so I honestly can't say how long the battery lasts under load. I can only say it outlasted my patience.
|
||||
- **A development log for the driver.** The display driver and the EEPROM commit protocol are the parts I'd still defend. They deserved diagrams and measurements at the time, not the half page of comments I left them with.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: Avoid, an Early Web Game
|
||||
description: A tiny archived web game from my first experiments with browser-based interaction.
|
||||
title: Avoid
|
||||
description: My first browser game. Tiny, archived for honesty.
|
||||
date: 2026-04-29
|
||||
projectPeriod: 'January 2018'
|
||||
thumbnail:
|
||||
|
|
@ -9,8 +9,8 @@ thumbnail:
|
|||
tags: ['games', 'web']
|
||||
role: Game author
|
||||
stack: ['JavaScript', 'Canvas']
|
||||
outcome: A small playable web game kept as an archive of early browser work
|
||||
outcome: My first browser game; kept for the timeline
|
||||
audience: general
|
||||
---
|
||||
|
||||
I recently found my first web game. It is very simple, but I killed some time with it.
|
||||
The first browser game I wrote. It's not good, but it was the moment a `<canvas>` element stopped being mysterious. Keeping it here because pretending the older work didn't happen would be dishonest.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: A Unity City Simulation for a Cybersecurity Challenge
|
||||
description: A client-server Unity simulation where REST-controlled traffic lights made mistakes immediately visible through crashes.
|
||||
title: A Unity City Where Bad PLC Code Made Cars Crash
|
||||
description: A REST-controlled traffic-light sim for a cybersecurity event. Bad PLC code showed up as car crashes — the most honest feedback loop I've shipped.
|
||||
date: 2026-05-01
|
||||
projectPeriod: 'July-August 2018'
|
||||
thumbnail:
|
||||
|
|
@ -9,17 +9,17 @@ thumbnail:
|
|||
tags: ['simulation', 'systems']
|
||||
role: Simulation author
|
||||
stack: ['Unity', 'C#', 'REST API', 'Blender']
|
||||
outcome: A visual context for a PLC-focused cybersecurity challenge
|
||||
outcome: Visible consequences for an otherwise abstract PLC challenge
|
||||
audience: technical
|
||||
links: []
|
||||
---
|
||||
|
||||
I simulated a city where car crashes were more frequent than usual.
|
||||
A small city in Unity where the traffic lights were driven by a REST API. Contestants in a PLC cybersecurity event would write control logic; bad logic made cars crash, and they'd watch it happen.
|
||||
|
||||
The state of the traffic lights could be changed through a REST API. Drivers followed the instructions of those lights, so if a mistake was made, collisions appeared in the simulation. There was also support for displaying tweets on a HUD.
|
||||
Three things are worth saying about it:
|
||||
|
||||
The project was the context for a cybersecurity challenge about PLCs. Contestants could see the effect of their work immediately, as crashes.
|
||||
- **Visual feedback was the whole point.** Most security challenges punish wrong answers with a red "incorrect." This one punished them with car wrecks, and people learned faster.
|
||||
- **Server-client, all decisions on the server.** Every agent's behaviour was computed centrally and broadcast. The harder problem wasn't simulation — it was getting the broadcast to be fault-tolerant on the conference Wi-Fi without flooding it.
|
||||
- **Built it solo, including the models and animations in Blender.** Not a flex, just context for why everything's a little janky.
|
||||
|
||||
The architecture was server-client. Every decision of the agents was calculated server-side, and the real challenge was broadcasting those decisions in a fault-tolerant way on minimal bandwidth.
|
||||
|
||||
It was built in Unity with C# as the scripting language. I also made the models and animations in Blender.
|
||||
There was also a HUD overlay for tweets, which felt clever at the time and dated horribly. Skip that part.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: Shared Simulation Code in a Mobile Multiplayer Browser Game
|
||||
description: How decla.red used shared TypeScript game logic, WebSockets, client prediction, and spatial indexing for a team-based browser game.
|
||||
title: One Game Library, Imported by Both the Client and the Server
|
||||
description: A mobile multiplayer browser game where client and server linked the same TypeScript module. One source of truth, one fewer class of bug.
|
||||
date: 2026-05-07
|
||||
projectPeriod: 'Autumn-Winter 2020'
|
||||
thumbnail:
|
||||
|
|
@ -8,9 +8,9 @@ thumbnail:
|
|||
alt: The decla.red browser game interface showing a space scene.
|
||||
tags: ['games', 'web', 'systems']
|
||||
role: Game and backend systems author
|
||||
stack: ['TypeScript', 'Node.js', 'WebSockets', 'Firebase', 'WebGL']
|
||||
scale: Multiple servers, each communicating with 16-32 clients
|
||||
outcome: A mobile-capable online browser game built on top of SDF-2D
|
||||
stack: ['TypeScript', 'Node.js', 'WebSockets', 'Firebase', 'WebGL', 'SDF-2D']
|
||||
scale: Multiple game servers, each talking to 16–32 clients, browser and mobile
|
||||
outcome: A multiplayer browser game that proved SDF-2D survived a real game loop
|
||||
audience: technical
|
||||
links:
|
||||
- label: Source
|
||||
|
|
@ -22,41 +22,31 @@ media:
|
|||
- type: image
|
||||
src: ./_assets/decla-red.jpg
|
||||
alt: The decla.red browser game interface showing a space scene with team controls and planets.
|
||||
caption: decla.red used the SDF-2D renderer in a real-time multiplayer game.
|
||||
caption: A real game loop is a worse audience than a tech demo. That's the point.
|
||||
---
|
||||
|
||||
`decla.red` was a conquest-style online multiplayer browser game set in space. Two teams fought over small planets, gained points based on control, and could shoot at the other team while moving through a ray-traced 2D scene.
|
||||
**The short version:**
|
||||
|
||||
The rendering made the game look interesting, but the architecture was the more useful lesson. The game needed to run on phones, talk to multiple servers, keep clients responsive, and avoid duplicating game rules between frontend and backend.
|
||||
- Conquest-style space shooter. Two teams, small planets, points for control, ray-traced 2D rendering.
|
||||
- Built on top of [SDF-2D](/articles/sdf-2d-ray-tracing/), partly to prove the renderer worked outside a thesis demo.
|
||||
- The architecture decision worth remembering: one TypeScript module containing the game rules, linked by both the Node server and the browser client. Same code, both sides of the wire.
|
||||
|
||||
## The Problem
|
||||
## The split that usually goes wrong
|
||||
|
||||
Real-time multiplayer games have an awkward split. The server should be authoritative, but the client has to feel immediate. If every meaningful interaction waits for a round trip, the game feels broken. If the client is trusted too much, the game becomes inconsistent or easy to abuse.
|
||||
Real-time multiplayer has an awkward two-machine problem. The server has to be authoritative or the game is cheatable; the client has to feel immediate or the game is unplayable. If you write the rules twice — once on each side — they will drift. Eventually a player's screen will say one thing and the server will think another.
|
||||
|
||||
For this project, I wanted the same game rules to be used by the server and the client. The server would calculate the actual next state. The client could predict locally with the same code and later reconcile with the server.
|
||||
I wanted the server's "compute the next state" function and the client's "predict the next state locally" function to be literally the same function. So I put the rules in a shared TypeScript library, published nothing, and had both `package.json` files link to it.
|
||||
|
||||
## Constraints
|
||||
The win wasn't elegance, it was the bugs that didn't happen. Client prediction stopped being an approximation of the server — it _was_ the server, run optimistically and reconciled when the authoritative update came back.
|
||||
|
||||
The project used TypeScript on both sides: browser code for the client and Node.js for the server. WebSockets carried real-time updates. Firebase helped the servers reach consensus about the active server set.
|
||||
## Other choices worth a sentence
|
||||
|
||||
Each server communicated with 16-32 clients. That is not large by industry standards, but it was enough to make careless spatial operations and state updates visible.
|
||||
- **k-d trees for spatial queries.** Once the world held more than a few dozen objects, naive collision and proximity checks dominated the server tick. A k-d tree dropped them out of the profile.
|
||||
- **Message-passing object model.** Lifted from Smalltalk's `doesNotUnderstand:` idea. Entities respond to messages they care about and ignore the rest. Easier to extend than the inheritance tree I tried first, and less brittle.
|
||||
- **Firebase only for server discovery.** Not for game state — for "which servers are currently in the pool." Tiny consistent store, didn't need to write one.
|
||||
|
||||
## Design
|
||||
## What I'd change
|
||||
|
||||
The key decision was a shared library for game logic. Both the client and server linked to it, so the transition rules lived in one place.
|
||||
|
||||
That reduced a common source of bugs: the client and server disagreeing about the meaning of an action. It also made client-side prediction more realistic, because the client was not approximating a different system.
|
||||
|
||||
As the game logic became heavier, spatial operations needed attention. I implemented k-d trees to reduce the cost of queries over objects in the world. For the object model, I borrowed ideas from message passing, including a version of the Smalltalk-style `messageNotUnderstood` pattern, to keep behaviour extensible without pushing every entity into a brittle inheritance tree.
|
||||
|
||||
## What Worked
|
||||
|
||||
Sharing simulation code was the most important architecture choice. It let the project stay coherent as the client and server evolved.
|
||||
|
||||
The project also validated SDF-2D outside a toy environment. A rendering library is more convincing when it survives a game loop, input, network updates, and mobile constraints.
|
||||
|
||||
## What I Would Change
|
||||
|
||||
I would now spend more effort on observability for synchronisation and prediction errors. Multiplayer systems need good visibility into divergence. Without that, debugging becomes a sequence of guesses.
|
||||
|
||||
I would also separate the story of rendering and networking more clearly in the codebase. Both were interesting, but they put different kinds of pressure on the architecture.
|
||||
- **Observability for desync.** Multiplayer systems live or die by visibility into divergence. I had logs; I needed dashboards showing the rate, the shape, and the triggering interaction for every prediction miss. Without those, debugging was guessing.
|
||||
- **Don't tangle rendering and networking in the same tree.** Both were interesting, both put different kinds of pressure on the architecture, and the directories grew into each other. Separate top-level folders from day one next time.
|
||||
- **Skip multi-server until the math demands it.** I wired up multi-server early because it sounded right. With 16–32 clients per server I was nowhere near needing it; the complexity wasn't free.
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
---
|
||||
title: A WebGPU Drawing Garden Where Agents Rewrite Your Strokes
|
||||
description: How Fleeting Garden runs an agent simulation in WebGPU compute shaders, with a 3×3 reaction matrix as the personality of each vibe.
|
||||
description: A single-file WebGPU drawing toy. You stroke a colour, agents follow it, and a 3×3 matrix per vibe gives each preset its personality.
|
||||
date: 2026-05-22
|
||||
projectPeriod: '2026'
|
||||
thumbnail:
|
||||
src: ./_assets/fleeting-garden.jpg
|
||||
alt: A kaleidoscopic Fleeting Garden snapshot of cyan, violet, and yellow agent trails radiating from a central knot.
|
||||
tags: ['graphics', 'simulation', 'web']
|
||||
role: Author
|
||||
role: Graphics and shader author
|
||||
stack: ['TypeScript', 'WebGPU', 'WGSL', 'Compute shaders', 'Vite', 'Tweakpane']
|
||||
scale: A single-file WebGPU bundle, ~10 WGSL shaders, six vibe presets, runs entirely client-side
|
||||
outcome: A browser drawing toy where user input seeds an agent simulation that rewrites the canvas in real time
|
||||
scale: One HTML file, ~10 WGSL shaders, 6 vibe presets, 60 FPS target on consumer hardware
|
||||
outcome: A browser drawing toy where user strokes seed an agent simulation that overwrites them
|
||||
audience: technical
|
||||
links:
|
||||
- label: Demo
|
||||
|
|
@ -20,75 +20,55 @@ links:
|
|||
media:
|
||||
- type: image
|
||||
src: ./_assets/fleeting-garden.jpg
|
||||
alt: Close-up of intertwining cyan, violet, and yellow agent trails radiating into a kaleidoscopic central knot, with a fine grain over the whole image.
|
||||
caption: A snapshot of one Fleeting Garden session. The trail texture is what you see; the agents that drew it are no longer visible.
|
||||
alt: Close-up of intertwining cyan, violet, and yellow agent trails radiating into a kaleidoscopic central knot.
|
||||
caption: A snapshot from one session. What you see is the trail texture; the agents that drew it are already gone.
|
||||
---
|
||||
|
||||
Fleeting Garden began as a chance to spend a few weeks inside WebGPU compute. The first constraint I set for myself was that user input should steer the simulation, not just seed it. The second was that the same engine should produce visibly different behaviour under different presets, without growing a fork per preset.
|
||||
**The three-bullet pitch:**
|
||||
|
||||
The shape that emerged is a single-page drawing toy. You pick a palette, drag a colour onto the canvas, and a swarm of agents follows the stroke, branches off, and slowly rewrites the patch you laid down. The strokes themselves vanish immediately. What remains is a trail texture that the agents both read from and write to, blurred and faded a little every frame.
|
||||
- Draw a stroke; agents spawn along it, follow the trail, and slowly overwrite the patch you laid down. You shape the gesture, the garden owns the response.
|
||||
- Six "vibes" share the same simulation. The personality of each one lives in a 3×3 matrix of nine numbers in `{-1, 0, 1}`. That's the entire behaviour rule.
|
||||
- One static HTML file. `vite build`, `rsync`, done. Runs offline, can be emailed, no account, no save state.
|
||||
|
||||
## The Problem
|
||||
## Why physarum needed a knob
|
||||
|
||||
Physarum-style agent simulations are a well-trodden idea. Sense the surrounding trail, turn toward what you like, deposit a bit of your own colour, repeat. Drop a million of these on a texture and you get the familiar branching networks that look biological from a distance.
|
||||
Physarum-style agent sims are everywhere and most of them stop being interesting after thirty seconds, because they converge to the same family of branching shapes no matter what you feed them. Seeding the initial condition isn't enough — the input has to keep being a force inside the loop, otherwise you're just watching the attractor settle.
|
||||
|
||||
The interesting question is not how to make one run. It is how to make one feel like something specific. A generic physarum visual converges to the same family of structures regardless of input, which is why so many of them stop being interesting after the first thirty seconds. User input has to do more than seed the initial condition; it has to remain a force inside the system.
|
||||
My second self-imposed constraint was that one engine had to produce six visibly different presets without forking. The first prototype had a `switch (preset)` with one behaviour function per vibe and it was already painful at vibe two. I needed the personality to live in data, not code.
|
||||
|
||||
The second part of the problem is variety. The same engine had to produce visibly different behaviour under different presets, so that switching vibes felt like changing seasons rather than nudging one slider. That ruled out separate behaviour code per preset, which had been the obvious shape of the first prototype and had not survived contact with the second one.
|
||||
## The reaction matrix
|
||||
|
||||
## Constraints
|
||||
Each vibe is a 3×3 table of colour-to-colour affinities. When an agent of colour `i` looks at the trail in front of it, it weights the three channels of that sample by row `i` of the matrix, then uses the sign to pick left, right, or straight. That's it. The whole behaviour rule.
|
||||
|
||||
The toy had to be a single static file. No server, no account, no save state. Open the URL, draw, close the tab. That is the deal the metaphor makes with the user, and the deployment story falls out of it: `vite build` produces one HTML file, which a CI job rsyncs to a static host.
|
||||
Three examples of what nine numbers can do:
|
||||
|
||||
It had to be WebGPU only. Compute shaders are the right tool for this kind of simulation, and writing a Canvas2D or WebGL fallback would have meant either a second implementation or a watered-down primary one. The browserslist is literally `supports webgpu and last 2 years`, and anything older gets a clear message instead of a degraded experience.
|
||||
- **Aurora Mycelium** — cyclic, each colour chases the next. Agents wind into ribbons.
|
||||
- **Velvet Observatory** — every off-diagonal entry negative. Colours repel into separate islands.
|
||||
- **Paper Lantern Fog** — matrix filled with ones. Colours collapse into one cooperative blob.
|
||||
|
||||
It had to run on consumer hardware at sixty frames per second. The number of agents is the obvious lever, so it had to be adaptive. The number of WGSL pipelines is the less obvious one, so the architecture had to keep each frame's compute work split across a small number of focused shaders rather than one fat kernel.
|
||||
Adding a tenth number to the matrix would tax every existing vibe. Tuning the nine I have is a text edit. Six presets in, I haven't extended it.
|
||||
|
||||
## Design
|
||||
## The compute work, broken into small jobs
|
||||
|
||||
The simulation is split into six compute stages, written across ten WGSL files. Each stage has one job:
|
||||
Six stages, ten WGSL files, each one short enough that I can hold it in my head when something breaks:
|
||||
|
||||
1. **Agent step** advances every agent by one frame. It samples the trail texture at a sensor offset, picks a turn direction, moves, and deposits a small amount of colour into the next frame's trail texture.
|
||||
2. **Diffusion** blurs and decays the trail texture, so old marks soften and disappear.
|
||||
3. **Brush** writes user strokes into the trail texture and a separate "source" texture that the agent shader can read.
|
||||
4. **Eraser** has two variants. One clears a region of the trail texture, the other kills agents inside the eraser radius.
|
||||
5. **Agent generation** handles spawning new agents along a stroke, resizing the agent buffer when the cap changes, and compacting the buffer after erasure so dead slots do not waste GPU time.
|
||||
6. **Render** reads the final trail texture and produces the canvas image, with the palette and grain applied at the last moment.
|
||||
1. **Agent step** — sample the trail at a sensor offset, pick a turn, move, deposit colour. ~300 lines, the longest one.
|
||||
2. **Diffusion** — blur and decay so old marks soften.
|
||||
3. **Brush** — write user strokes into both the trail texture and a separate "source" texture the agents can read.
|
||||
4. **Eraser** — two variants: one clears a region of the trail, the other kills agents in a radius.
|
||||
5. **Agent generation** — spawn along strokes, resize the buffer when the cap changes, compact after erasure so dead slots don't waste GPU time.
|
||||
6. **Render** — read the trail, apply palette and grain.
|
||||
|
||||
Each of these is around a few dozen lines of WGSL, and the longest one (agent step) is under 300. Keeping them small is what made the simulation tunable; once they grew tangled, the tuning loop slowed to a crawl.
|
||||
The bind-group setup overhead from running more pipelines was lost in the noise next to the simulation cost. The win was that when the eraser shader started killing the wrong agents, I opened one file and reasoned about it without touching anything else.
|
||||
|
||||
### The Reaction Matrix
|
||||
## Smaller calls
|
||||
|
||||
The piece of the design I would defend hardest is the reaction matrix. Each vibe carries a 3×3 table of colour-to-colour affinities. When an agent of colour `i` senses the trail in front of it, the three channels of that sample are weighted by row `i` of the matrix to decide whether to turn left, turn right, or hold course. That is the entire behaviour rule.
|
||||
- **Adaptive cap, circular buffer.** If FPS drops, the cap shrinks; if there's headroom, it grows. When the cap is hit, new agents overwrite older ones. The decay you see — a stroke vanishing thirty seconds after you drew it — isn't an explicit eraser, it's the buffer wrapping around.
|
||||
- **URL is the share format.** The chosen vibe is in the query string. The "send your friend this preset" link is just a URL with `?vibe=tidepool-lantern` on it. The parser is tolerant about accents and casing because people retype these.
|
||||
- **One HTML file.** All CSS and JS inline. The piano samples sit beside it. Self-contained enough to email or drop on a USB stick.
|
||||
|
||||
The matrix is nine numbers in `{-1, 0, 1}`, and it captures most of what makes the six vibes feel different. _Aurora Mycelium_ has a cyclic preference where each colour chases the next, so its agents wind into ribbons. _Velvet Observatory_ has every off-diagonal entry negative, so the colours repel each other and settle into separate islands. _Paper Lantern Fog_ has the matrix filled with ones, which collapses the three colours into one cooperative blob.
|
||||
## What I'd change
|
||||
|
||||
Putting the personality of a vibe in a small, legible matrix was deliberate. The earlier prototype had a behaviour function per preset, and that route did not survive the second vibe — every new mood became a new branch in a switch statement. A 3×3 matrix is small enough that I can read it and predict the rough shape of the result, which made tuning new vibes a matter of editing a table rather than writing code.
|
||||
|
||||
### Input and Mirroring
|
||||
|
||||
The drawing pipeline is intentionally simple. A pointer event becomes a series of stroke segments, each segment spawns agents along its length, and the agents' initial angle points along the stroke with a small amount of jitter. The mirror slider folds each stroke into N copies rotated around the centre, which is the cheapest way I could think of to give the user a sense of composition without a layers panel.
|
||||
|
||||
Spawning competes with an adaptive cap. If the framerate drops below the target, the cap shrinks; if there is headroom, it grows. When the cap is hit, new agents overwrite older ones in a circular buffer. That overwrite is what gives the garden its decay: a stroke you drew thirty seconds ago is gone not because anything erased it, but because its agents have been replaced.
|
||||
|
||||
### Vibes as URLs
|
||||
|
||||
Switching vibes is the only stateful action in the app, and the chosen vibe is encoded in the URL query string. That makes the link itself the share format. A snapshot is a PNG you download; a "send your friend this preset" is a URL with `?vibe=tidepool-lantern` on the end. The URL parser is tolerant about accents, casing, and whitespace, because the names are the kind of thing people retype rather than copy.
|
||||
|
||||
## What Worked
|
||||
|
||||
The reaction matrix earned its place. Six presets later, I have not had to extend it. Every new vibe so far has been a recolouring plus a different table, sometimes with tweaks to the diffusion or sensor parameters, and the underlying simulation has not changed. At this scale, configuration is cheaper to evolve than code. Adding a tenth number to the matrix would be a tax on every existing vibe; tuning the nine I have is a few minutes of editing a file.
|
||||
|
||||
Splitting the compute work across small WGSL stages held up for the same reason in a different form. When the agent-erase shader started killing the wrong agents, I could open one short file and reason about it without touching anything else. The cost of running more pipelines is the bind-group setup, and that was lost in the noise compared to the simulation work itself.
|
||||
|
||||
The single-file build is the part I underestimated. The whole app, including all CSS and JavaScript, is one HTML file; the piano samples sit beside it and are preloaded at startup. That makes deployment trivial — `rsync` and done — but the part that actually matters is that the file is self-contained enough to hand around. I can attach it to an email or drop it on a USB stick and it runs offline, which is the closest a web app gets to feeling like an object.
|
||||
|
||||
## What I Would Change
|
||||
|
||||
The intro animation cost more than it should have. Agents fly in from off-screen to spell out the title, then transition to steady-state behaviour. The choreography is tied to a single `progress: 0 → 1` value that bleeds into timing, easing, and target positions across three different shaders, and that coupling is what makes the intro the part of the code I would least want to refactor today. If I rebuilt this, I would model the intro as its own dispatch with its own agent buffer and hand off to the steady-state pipeline at the boundary.
|
||||
|
||||
Property tests would help more than I expected. The simulation has invariants that hand-written unit tests are bad at finding — agent count stays under the cap, every drawn stroke produces a positive-coloured deposit on the next frame, the eraser does not leak agents past its radius — and these are exactly the shape of claim a generator-based test would falsify quickly.
|
||||
|
||||
The mobile experience is good enough rather than good. Pointer events behave, but small screens make the toolbar fight the canvas for space, and the agent cap has to shrink hard to keep the framerate up. A real fix means rethinking the toolbar layout and probably making the cap-versus-resolution tradeoff a user-visible choice.
|
||||
|
||||
The part I would keep is the asymmetry. You shape the gesture; the garden owns the response. The trail decay and the refusal of save state both look like missing features in isolation, and both stop looking that way the moment the garden is allowed to be fleeting. Most of the rest of the design is what fell out of taking that idea seriously.
|
||||
- The intro animation (agents fly in to spell the title, then transition to steady state) couples three shaders through a single `progress: 0 → 1` value. It's the bit I'd least want to refactor today. Next time I'd model the intro as its own dispatch with its own buffer and hand off cleanly.
|
||||
- Mobile works, but the toolbar fights the canvas for screen and the agent cap has to shrink hard to keep frame time down. A proper fix means rethinking the toolbar and exposing the cap-vs-resolution tradeoff to the user.
|
||||
- The simulation has invariants — agent count under the cap, every stroke produces a positive-coloured deposit on the next frame, the eraser doesn't leak agents past its radius — that proptest would falsify in minutes. Snapshot tests aren't the right tool here.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: A Frequency-Domain Foreign Exchange Prediction Experiment
|
||||
description: An older EUR/USD prediction experiment built from smoothing, short-time Fourier transforms, extrapolation, and a Python prediction server.
|
||||
title: Predicting EUR/USD With Hanning Windows
|
||||
description: A weekend frequency-domain experiment that did a passable job on EUR/USD. I would not have trusted it with my money, and I didn't.
|
||||
date: 2026-05-03
|
||||
projectPeriod: 'Autumn 2019'
|
||||
thumbnail:
|
||||
|
|
@ -9,15 +9,21 @@ thumbnail:
|
|||
tags: ['systems', 'tools']
|
||||
role: Experiment author
|
||||
stack: ['Python', 'NumPy', 'SciPy', 'Flask', 'MQL4']
|
||||
outcome: A working prediction server connected to an MQL4 client for trading experiments
|
||||
outcome: A prediction server, an MQL4 trading client, and a clearer view of how far my edge wasn't
|
||||
audience: technical
|
||||
links: []
|
||||
---
|
||||
|
||||
This was an experiment in predicting EUR/USD rates. The animation from the old portfolio showed the implementation doing a passable job: the prediction was the blue graph and the actual values were the green one. I would not have trusted it with my money.
|
||||
A linear regression in the frequency domain, dressed up. Old portfolio screenshots showed the prediction (blue) tracking the actual rate (green) closely enough to be flattering. I did not trade real money with it.
|
||||
|
||||
The algorithm was a fancy linear regression in the frequency domain. The steps were: smoothing the input values, differentiating, applying a short-time Fourier transformation with overlapped and Hanning-windowed windows, extrapolating, and then applying the inverse of these transformations to the resulting values.
|
||||
The pipeline:
|
||||
|
||||
The prediction server was written in Python using NumPy, SciPy, and Flask. It communicated with an MQL4 client that was responsible for handling financial transactions based on the generated data.
|
||||
- Smooth the input series.
|
||||
- Differentiate.
|
||||
- Short-time Fourier transform with overlapped, Hanning-windowed frames.
|
||||
- Extrapolate the frequency-domain coefficients.
|
||||
- Invert everything back to a predicted price series.
|
||||
|
||||
There was still plenty of room for improvement, but even with this simple algorithm, a sometimes profitable strategy was viable. The project was mostly a look into trading algorithms, their complexity, and the competition around them.
|
||||
A Python server (NumPy, SciPy, Flask) ran the model. An MQL4 client on a broker terminal called the server and would have placed trades if I'd dared.
|
||||
|
||||
What I actually learned: even a naive model can show a sometimes-profitable backtest, and that's the trap. The real game is built by people with co-located servers, microsecond ticks, and millions in infrastructure. This project taught me how far my edge wasn't.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: A JavaFX Graph Editor for Simulation Input
|
||||
description: A small JavaFX editor for creating and uploading graph input for the cooling system simulator.
|
||||
title: A JavaFX Editor for the Cooling Simulator
|
||||
description: The companion editor for the cooling-system sim. Drag-and-drop graph layout, JSON export, upload-to-backend. Small tool, mattered more than I expected.
|
||||
date: 2026-04-25
|
||||
projectPeriod: 'October-November 2018'
|
||||
thumbnail:
|
||||
|
|
@ -9,13 +9,13 @@ thumbnail:
|
|||
tags: ['simulation', 'tools']
|
||||
role: Editor author
|
||||
stack: ['JavaFX', 'JSON', 'REST API']
|
||||
outcome: An editor for building input graphs and sending them to the simulation backend
|
||||
outcome: A drag-and-drop graph editor that let non-developers feed the simulator
|
||||
audience: technical
|
||||
links: []
|
||||
---
|
||||
|
||||
This was a small editor for building input graphs for the cooling system simulator.
|
||||
The input-side companion to the [cooling system sim](/articles/nuclear-cooling-simulation/). A JavaFX desktop tool where you'd lay out the plant as a graph, edit each element's parameters in a side panel, export JSON, or upload directly to the simulator backend.
|
||||
|
||||
Nodes could be moved with drag-and-drop gestures. Element parameters were edited on the right panel.
|
||||
It was small but it punched above its weight at the actual event. The simulator's value depended entirely on the organisers being able to change the plant without me sitting next to them. The editor was what made that possible.
|
||||
|
||||
The UI was built with JavaFX. The output could be exported as JSON or uploaded directly to the simulation backend.
|
||||
If I built it again I'd skip JavaFX and put the editor in the browser — same place the monitoring clients lived. One install fewer for everyone.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: Designing an ML Deployment API Around Best Practices
|
||||
description: How GreatAI tried to make stronger ML deployment habits accessible through a small Python API.
|
||||
title: A Python Framework Where Doing the Right Thing Is the Default
|
||||
description: My MSc thesis. 33 catalogued ML deployment habits, a decorator-shaped Python API, and a survey of working engineers on which actually got adopted.
|
||||
date: 2026-05-09
|
||||
projectPeriod: '2022'
|
||||
thumbnail:
|
||||
|
|
@ -9,9 +9,9 @@ thumbnail:
|
|||
tags: ['ai', 'systems', 'tools']
|
||||
featuredOrder: 1
|
||||
role: Researcher and framework author
|
||||
stack: ['Python', 'ML deployment', 'API design']
|
||||
scale: 33 deployment best practices, six proposed additions, evaluated with professional data scientists and software engineers
|
||||
outcome: A Python framework, thesis, and research-backed API design for production-oriented AI deployments
|
||||
stack: ['Python', 'decorators', 'FastAPI', 'survey design']
|
||||
scale: 33 deployment habits surveyed, 6 proposed additions, framework evaluated by working data scientists and engineers
|
||||
outcome: A pip-installable framework, an MSc thesis, and one strong opinion about API surface area
|
||||
audience: recruiter-relevant
|
||||
links:
|
||||
- label: PyPI
|
||||
|
|
@ -25,47 +25,33 @@ media:
|
|||
- type: image
|
||||
src: ./_assets/great-ai.png
|
||||
alt: Example Python code using GreatAI decorators and prediction helpers.
|
||||
caption: GreatAI's public surface was designed to keep deployment best practices close to the application code.
|
||||
caption: A working GreatAI service is about ten lines on top of a plain prediction function.
|
||||
---
|
||||
|
||||
GreatAI started from a practical frustration: applying machine learning was becoming easier, but deploying it well was still easy to get wrong. Many failures were not about model architecture. They were about missing metadata, weak versioning, poor reproducibility, untracked inputs, or interfaces that made the right behaviour too cumbersome to use.
|
||||
**The short version:**
|
||||
|
||||
My thesis work looked at that gap from two sides. First, I collected and organised AI/ML deployment best practices, including 33 practices and six additions proposed through the research. Then I designed a Python framework that tried to make those practices feel like the natural path rather than an enterprise checklist.
|
||||
- I surveyed working data scientists and engineers on a catalogue of 33 ML deployment habits. The honest finding: people skip habits not because they disagree, but because adopting one means writing five lines of glue, and they have a deadline.
|
||||
- That's a design problem, not a discipline problem. The thesis built a Python framework around one bet: put the deployment behaviour next to the prediction function with a single decorator, and the right thing becomes the path of least resistance.
|
||||
- Pip-installable as `great-ai`. Worth a look mainly for the survey methodology behind it.
|
||||
|
||||
The result was GreatAI: a deployment-oriented framework with a deliberately small API. The design goal was not to wrap every part of an ML stack. It was to make common deployment concerns visible, automatic where possible, and hard to forget.
|
||||
## The thing nobody wants to admit
|
||||
|
||||
## The Problem
|
||||
The literature has a long list of habits you should adopt when shipping an ML service: track inputs, version models, expose health, log decisions, keep predictions reproducible. Everyone agrees with the list. Almost nobody implements all of it.
|
||||
|
||||
Deployment quality is often treated as something that happens after model development. That separation creates a bad default. A model can be useful in a notebook, but a deployed AI service also needs traceability, stable interfaces, input/output logging, model metadata, and operational behaviour that can be inspected later.
|
||||
I spent the bulk of the thesis catalogueing 33 such habits, proposing 6 more, and surveying engineers on which actually got applied in their day jobs. The data was pretty clear about the failure mode: it wasn't ignorance, it wasn't laziness, it wasn't budget. It was that the cost of doing the right thing — five lines of glue per habit, multiplied across a stack — was higher than the visible cost of skipping it. So skipping it became the default.
|
||||
|
||||
The hard part is not listing those needs. The hard part is getting busy engineers and data scientists to adopt them without making their work feel slower.
|
||||
So the real research question wasn't "what should engineers do." It was "what API shape makes doing the right thing cheaper than not."
|
||||
|
||||
So the core question became: can a framework implement meaningful deployment practices while keeping the API small enough that people would actually use it?
|
||||
## The framework's bet
|
||||
|
||||
## Constraints
|
||||
- **A decorator on a plain function.** `@GreatAI.create` turns a regular Python function into a deployed service with metadata, request tracing, and a versioned interface. No inheritance, no project layout, no enforced directory structure. The mental cost is one import.
|
||||
- **Implicit behaviour only for cross-cutting concerns.** Logging, versioning, metadata are implicit. Anything touching business logic stays explicit. The rule: if it would surprise me when I'm debugging, it shouldn't be implicit.
|
||||
- **Own the contract, leave the storage alone.** Where you persist logs, models, or metrics is your choice; GreatAI defines the shape and provides defaults. The model registry stays somebody else's library.
|
||||
|
||||
GreatAI had to satisfy two constraints that usually pull in opposite directions.
|
||||
The survey backed up the central premise: ease of use and functionality both matter for adoption, and they're independent axes. A framework that ticks every box and is awkward will lose to a smaller one that doesn't.
|
||||
|
||||
It needed to encode deployment practices such as metadata handling, model loading, request tracing, and reproducible prediction interfaces. It also had to be approachable enough that the basic use case still looked like ordinary Python.
|
||||
## What I'd change
|
||||
|
||||
That shaped the API. The framework could not demand a new mental model for every project. The deployment behaviour had to sit close to the prediction function, because that is where the developer already has context.
|
||||
|
||||
## Design
|
||||
|
||||
The design leaned on decorators and lightweight conventions. The application author should be able to declare the prediction boundary, attach the relevant model and metadata behaviour, and let the framework handle repeated operational concerns.
|
||||
|
||||
That is a careful tradeoff. Too much implicit behaviour makes systems difficult to debug. Too much explicit setup makes best practices optional in practice, because the path of least resistance is to skip them. GreatAI tried to keep the implicit parts focused on cross-cutting deployment concerns rather than business logic.
|
||||
|
||||
Feedback from professional data scientists and software engineers supported the main premise: ease of use and functionality both matter when people decide whether to adopt deployment tooling. A framework that is technically complete but awkward to use will still fail.
|
||||
|
||||
## What Worked
|
||||
|
||||
The strongest part of the project was treating API design as part of deployment quality. Best practices are not only documentation. They need interface support, defaults, and feedback loops.
|
||||
|
||||
The research also forced the framework to be specific. "Production-ready" is too broad to be useful. A concrete list of deployment practices made it possible to ask which practices can be automated, which ones need explicit developer decisions, and which ones belong outside the framework.
|
||||
|
||||
## What I Would Change
|
||||
|
||||
If I returned to the project now, I would focus more on integration boundaries: how GreatAI should fit into modern observability, model registry, and evaluation workflows without trying to own them. Deployment frameworks age quickly when they become too broad.
|
||||
|
||||
The part I would keep is the central idea: make the right deployment behaviour easy enough that it becomes the default.
|
||||
- I'd narrow further. Anything GreatAI did that overlapped with MLflow, BentoML, or modern observability stacks would go. The durable bit was always the decorator and the catalogue behind it.
|
||||
- I'd publish the survey instrument separately. The 33-habit catalogue and the adoption-vs-impact methodology outlive the framework. People still ask about that part.
|
||||
- I'd stop calling them "best practices." I used that phrase in the thesis and it aged into corporate-speak. The honest name is "things that hurt later if you skip them."
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: Syncing State with Immutable Tries
|
||||
description: How a multi-device life tracking project used trie structure to diff, reconcile, and synchronise goal state.
|
||||
title: Syncing State with an Immutable Trie
|
||||
description: A visual goal tracker whose lasting idea was the sync model — an immutable trie so structural diffs are trivial and only deltas cross the wire.
|
||||
date: 2026-05-05
|
||||
projectPeriod: 'August-September 2019'
|
||||
thumbnail:
|
||||
|
|
@ -9,9 +9,9 @@ thumbnail:
|
|||
tags: ['systems', 'web', 'tools']
|
||||
featuredOrder: 4
|
||||
role: Full-stack author
|
||||
stack: ['Python', 'Angular', 'State synchronisation']
|
||||
stack: ['Python', 'Angular', 'TypeScript', 'Custom sync protocol']
|
||||
scale: Multi-device goal and task state shared between clients and a server
|
||||
outcome: A working synchronisation model built around immutable trie properties
|
||||
outcome: A working sync protocol where structural sharing made the delta tiny
|
||||
audience: recruiter-relevant
|
||||
links:
|
||||
- label: Source
|
||||
|
|
@ -20,35 +20,43 @@ media:
|
|||
- type: image
|
||||
src: ./_assets/towers.jpg
|
||||
alt: Screenshot of a life tracking web interface represented with tower-like visual structures.
|
||||
caption: The visual idea was simple; the useful lesson was the synchronisation model behind it.
|
||||
caption: The interface was a 2019 weekend experiment. The trie underneath aged better.
|
||||
---
|
||||
|
||||
Life Towers was a multi-device goal and task tracker with an intentionally visual interface. The surface idea was an aesthetic representation of previous and current goals. The more interesting part was synchronising state across clients without sending more data than necessary.
|
||||
**The short version:**
|
||||
|
||||
This was not a large distributed system, but it had a real version of a common problem: clients and server drift apart, and the system needs a compact way to compare, reconcile, and update.
|
||||
- Built as a personal task tracker with a tower metaphor. The UI was the fun bit; the sync model is the part I'd still write the same way today.
|
||||
- Clients and server hold the same data as an immutable trie. Comparing two versions is a walk over shared nodes. Sending the delta is sending only the nodes that aren't shared.
|
||||
- No CRDT, no OT, no clever conflict resolution. The structure does the work, the protocol stays boring.
|
||||
|
||||
## The Problem
|
||||
## The problem in one paragraph
|
||||
|
||||
If a task model is stored as an ordinary mutable object graph, synchronising it often becomes a choice between sending too much data or writing complicated ad hoc diff logic.
|
||||
Pick any non-trivial mutable object graph, sync it across devices, and you end up either sending the whole thing on every change (wasteful) or writing ad-hoc diff logic per shape (brittle). I wanted a representation where the _shape_ of the data made the diff fall out for free.
|
||||
|
||||
I wanted a structure where the shape of the data made synchronisation easier. The client should be able to compare its state with the server's state, find a difference, reconcile it, and send only the delta.
|
||||
## The trie, concretely
|
||||
|
||||
## Design
|
||||
A goal in Life Towers is a path of strings. `Health / Running / 5k`. Tasks under a goal hang off the leaf. A user's whole state is a tree, and a trie is exactly the data structure that makes that tree's _identity_ manipulable.
|
||||
|
||||
I used a trie. A trie made the hierarchical shape explicit, and its properties made it easier to reason about differences between stored versions.
|
||||
Two properties did the heavy lifting:
|
||||
|
||||
The immutable nature of the structure simplified much of the logic. Instead of mutating arbitrary branches in place, updates could produce new structure with shared unchanged parts. That made reconciliation easier to reason about and reduced the amount of data that needed to move across the network.
|
||||
- **Structural sharing.** When you tick off a task under `Health / Running / 5k`, the new root reuses every untouched subtree by reference. The `Career` branch and the `Reading` branch are the same objects they were before. Comparing the old and new roots is mostly pointer equality; only the path that actually changed gets walked.
|
||||
- **Immutability.** Updates produce new structure instead of mutating. "Where I was" and "where I am" become two pointers, not two snapshots. The diff between them is whatever's not shared — and that walk is O(changes), not O(state).
|
||||
|
||||
The project also gave me a reason to deepen my Python and Angular knowledge, but the synchronisation structure was the main lesson.
|
||||
The sync loop falls out:
|
||||
|
||||
## What Worked
|
||||
1. Client holds the last root the server acknowledged plus its own current root.
|
||||
2. To send: walk only the unshared paths, emit one op per changed leaf. In practice that's a handful of bytes for a typical edit, no matter how large the rest of the tree is.
|
||||
3. Server applies, returns its new root.
|
||||
4. Client rebases any in-flight edits by replaying them on top.
|
||||
|
||||
The biggest win was choosing a data structure that matched the problem. Once the state was represented in a way that made comparison natural, the network protocol became simpler.
|
||||
There's no conflict resolution layer because the operations commute on the structure — two clients adding tasks under different branches produce non-overlapping deltas that compose trivially. The hard cases (two clients editing the same leaf) are tiny and obvious, because they're the _only_ place the deltas touch the same path.
|
||||
|
||||
The other useful lesson was that visual products still need a strong internal model. A pleasant interface is fragile if the underlying state is hard to trust.
|
||||
## What I'd change
|
||||
|
||||
## What I Would Change
|
||||
- **Property tests around the rebase.** The reconcile path is exactly where a generator finds bugs that hand-written tests never think to write. I had hand-written cases; I'd start with `proptest` now.
|
||||
- **A standalone spec for the wire format.** The part worth lifting out was the protocol, not the goal tracker. A short spec would let me (or anyone) reimplement it in a different stack without re-deriving everything from the Python source.
|
||||
- **Strip the visual experiment.** The tower visualisation was fun but it bound the storage to a UI metaphor. The sync model should be a library; the towers should be a separate toy.
|
||||
|
||||
Today I would document the sync protocol more formally and add property-based tests around reconciliation. Synchronisation code is exactly the kind of code that benefits from generated edge cases.
|
||||
## If you take one idea from this
|
||||
|
||||
I would also separate the visual experiment from the state synchronisation story more explicitly. The latter is the part that aged better.
|
||||
Most sync problems are diff problems pretending to be transport problems. Pick the data structure that makes the diff free, and the protocol almost writes itself. The corollary: if you're writing a lot of "if this changed, send that" code, you're using the wrong structure.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: Lights Synchronized to Music
|
||||
description: A Raspberry Pi music player that analysed audio output and drove RGB LED strips.
|
||||
title: My First Real Project — LEDs Driven by an FFT
|
||||
description: A Raspberry Pi music player that drove RGB strips through MOSFETs. The first thing I started and actually finished.
|
||||
date: 2026-04-26
|
||||
projectPeriod: 'Spring 2016'
|
||||
thumbnail:
|
||||
|
|
@ -8,14 +8,18 @@ thumbnail:
|
|||
alt: RGB LED strips lit by a music synchronisation project.
|
||||
tags: ['systems', 'tools']
|
||||
role: Hardware and software author
|
||||
stack: ['Python', 'NumPy', 'FFT', 'Raspberry Pi', 'Vanilla web']
|
||||
outcome: My first finished non-trivial project, combining a web UI, audio processing, and hardware output
|
||||
stack: ['Python', 'NumPy', 'FFT', 'Raspberry Pi', 'MOSFETs', 'vanilla web']
|
||||
outcome: The first non-trivial project I started and finished
|
||||
audience: technical
|
||||
links: []
|
||||
---
|
||||
|
||||
A Raspberry Pi ran a small music player, and the audio it produced drove the colour of a couple of RGB LED strips through some MOSFETs.
|
||||
Spring 2016. I had a Raspberry Pi, a couple of 12V RGB LED strips someone had given me, a handful of MOSFETs from an electronics kit, and zero idea what I was doing. The plan was something like: play music, look at it, make the lights match.
|
||||
|
||||
It was the first non-trivial project I actually finished. Far from perfect, but I am still proud that I built it on my own.
|
||||
I got bands wrong first. I tried mapping raw audio amplitude to brightness, which made the lights pulse with anything — clipping, voice, fan noise — and produced a strobing mess that hurt to look at. Reading about Fourier transforms long enough to type `numpy.fft.fft(audio_chunk)` into a REPL was the moment the project started actually behaving like the thing I'd imagined. Bass-heavy frequency bins went to red; mids to green; highs to blue. Smoothing the output over a few frames stopped the seizure-inducing flicker.
|
||||
|
||||
The backend was Python, with NumPy doing the FFT. The frontend was a vanilla web page for picking tracks and tweaking settings.
|
||||
The MOSFETs took longer than they should have. I wired one backwards and it got hot enough to leave a small mark on the breadboard. I learned to read a datasheet, slowly, by needing one.
|
||||
|
||||
The frontend was a vanilla web page on the same Pi: pick a track, tweak the band thresholds, see what changed. No framework. Just a `<select>`, a few sliders, and an `XMLHttpRequest`. It worked.
|
||||
|
||||
It's not impressive in 2026. The thing I actually keep from it isn't the FFT or the MOSFETs — it's the discovery that I'd rather have a finished janky thing than an elegant unfinished one. Most of the projects on this site are downstream of that discovery. I'd still recommend the same path to anyone learning: pick something physical, plug things together until they work, accept that the first version will be ugly.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: My Notes, an Android Markdown App
|
||||
description: A small Android notes app for creating, editing, and filtering markdown notes with hashtags.
|
||||
title: My Notes — A Markdown App for Android
|
||||
description: A small Android note app built on Markwon. The idea wasn't new; the point was learning a platform that wasn't the web.
|
||||
date: 2026-05-02
|
||||
projectPeriod: 'November 2019'
|
||||
thumbnail:
|
||||
|
|
@ -9,15 +9,15 @@ thumbnail:
|
|||
tags: ['tools']
|
||||
role: Android app author
|
||||
stack: ['Android', 'Markdown', 'Markwon']
|
||||
outcome: A functional markdown note organiser and a first exposure to Android development
|
||||
outcome: A working notes app and my first time outside the web stack
|
||||
audience: technical
|
||||
links:
|
||||
- label: Source
|
||||
url: https://github.com/schmelczer/my-notes
|
||||
---
|
||||
|
||||
My Notes was a small Android note organiser and editor built on top of Markwon.
|
||||
A small Android app for writing Markdown notes and filtering them by hashtag. Built on top of Markwon for the rendering.
|
||||
|
||||
It let me create Markdown notes and filter them by hashtag. It was also my first exposure to Android development.
|
||||
The idea wasn't original — every developer writes their own notes app eventually — and the bar for shipping one wasn't high. What I actually wanted from the project was a few weeks somewhere outside the web stack, in a platform with different conventions about lifecycle, storage, and resource constraints. Android delivered that.
|
||||
|
||||
The idea was not new, but the app worked, and the platform was different enough from the full-stack web work I had been doing that the project was worth finishing.
|
||||
I don't use the app anymore (it lost a long battle with Obsidian, which is also why I later wrote [reconcile-text](/articles/reconcile-text-3-way-merge/)). I'd still recommend "write a small thing on a new platform" as a way to recalibrate what you take for granted.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: Graph Models for a Real-Time Cooling Simulation
|
||||
description: Simulating a nuclear facility cooling system with graph traversal, matrix solving, Flask, NumPy, and real-time monitoring clients.
|
||||
title: Two Graphs Are Simpler Than One — A Cooling System Simulator
|
||||
description: A live cooling-system sim for a PLC cybersecurity event. Splitting flow and heat into two graph passes kept the calculation cheap and the behaviour believable.
|
||||
date: 2026-05-04
|
||||
projectPeriod: 'October-November 2018'
|
||||
thumbnail:
|
||||
|
|
@ -10,47 +10,53 @@ tags: ['simulation', 'systems', 'tools']
|
|||
featuredOrder: 5
|
||||
role: Simulation and UI author
|
||||
stack: ['Python', 'Flask', 'NumPy', 'HTML canvas', 'JavaFX']
|
||||
scale: Remote simulation server with multiple monitoring clients and a separate graph editor
|
||||
outcome: A believable, extensible cooling-system simulation for a cybersecurity challenge context
|
||||
scale: One remote sim server, many monitoring clients, separate JavaFX graph editor
|
||||
outcome: A believable PLC simulation usable by non-specialists during a live cybersecurity challenge
|
||||
audience: recruiter-relevant
|
||||
links: []
|
||||
media:
|
||||
- type: image
|
||||
src: ./_assets/process-simulator.jpg
|
||||
alt: Screenshot of the cooling system simulator with pipes, pumps, coolers, and temperature values.
|
||||
caption: The simulator calculated flow and temperature over graph-based process models.
|
||||
caption: Flow ran first as a graph traversal, then heat solved as a matrix equation.
|
||||
- type: image
|
||||
src: ./_assets/process-simulator-input.jpg
|
||||
alt: Screenshot of the JavaFX graph editor used to define simulator input.
|
||||
caption: A separate JavaFX editor produced JSON inputs for the simulation backend.
|
||||
caption: The JavaFX editor produced JSON that the simulator ate as input.
|
||||
---
|
||||
|
||||
This project simulated the cooling system of a nuclear facility. It was built for a cybersecurity challenge about PLCs, where participants needed to see the consequences of changing a system state.
|
||||
**The short version:**
|
||||
|
||||
The simulation did not try to be physically complete. It aimed to be cheaply calculated, believable to a non-specialist, scalable enough for the event context, and understandable through a clean GUI.
|
||||
- A cooling-system simulator with reactors, coolers, pumps, heat exchangers, drains, and pipes. Built for a cybersecurity event where contestants poked at PLCs and watched the consequences in real time.
|
||||
- Not physically accurate. Aimed at "cheap to compute, plausible to a non-specialist, runs all weekend on one server."
|
||||
- The useful design move was modelling flow and heat as **two separate graph passes**, not one combined PDE.
|
||||
|
||||
## The Problem
|
||||
## What the event needed
|
||||
|
||||
The simulated system needed reactors, coolers, pumps, heat exchangers, drains, sources, and pipes. Those elements had to be configurable, and multiple monitoring clients needed to update in real time from a remote server.
|
||||
The challenge was about PLCs. Contestants would change setpoints, valves, or pump speeds, and we needed them to see whether their action made the plant stable, wasted coolant, or melted something. That meant:
|
||||
|
||||
The key challenge was representing flow and temperature in a way that was simple enough to calculate repeatedly but structured enough to produce plausible behaviour.
|
||||
- Multiple monitoring clients had to update from one simulation server in near real time.
|
||||
- The system had to be configurable enough that the event organisers could ship me a new plant on Friday night and have it running Saturday morning.
|
||||
- It had to be obvious. A simulator nobody understands isn't a teaching tool, it's noise.
|
||||
|
||||
## Design
|
||||
## The split that made it cheap
|
||||
|
||||
The system used two graph models. First, water was distributed by traversing the graph of pipes according to pressures generated by pumps. Then, an adjacency matrix was populated from the relations between nodes based on water flow.
|
||||
Trying to solve flow and heat as a coupled system would have been a real CFD problem and I had two weeks. Instead:
|
||||
|
||||
After accounting for base temperatures, heaters, and heat exchangers, the matrix was solved to calculate current node temperatures. Repeating that process advanced the simulation.
|
||||
1. **Flow first, as graph traversal.** Walk the pipe graph from the pumps, accumulate pressure, distribute water to nodes.
|
||||
2. **Heat second, as a linear system.** Build the adjacency matrix from the flow result, add boundary conditions (heaters, exchangers, base temperatures), solve for node temperatures with NumPy.
|
||||
3. Repeat both passes per tick.
|
||||
|
||||
Python handled the backend logic with Flask and NumPy. The monitoring frontend used an HTML5 canvas. A separate JavaFX graph editor let users move nodes, edit element parameters, export JSON, and upload inputs to the backend.
|
||||
This is wrong as physics. It's right as a model. Flow doesn't react to instantaneous heat in any way contestants could perceive, and the cost of solving them separately was a tiny fraction of solving them together. The clean phase boundary also meant when "the heat is weird," I knew exactly which pass to look at.
|
||||
|
||||
## What Worked
|
||||
## Why the editor mattered
|
||||
|
||||
The graph/matrix split was a useful modelling boundary. Flow and heat exchange are related, but treating them as separate calculation phases kept the implementation easier to reason about.
|
||||
The simulator's most-used UI was the _input_ editor — a separate JavaFX tool where you laid out the plant, set parameters per element, and exported JSON the sim ate. I wrote up the editor's [own story here](/articles/graph-editor-javafx-simulation-input/), because in hindsight it deserved to be its own project.
|
||||
|
||||
The editor also mattered. A simulation is much more useful when its input is inspectable and editable by people who are not editing source files.
|
||||
The lesson: a simulation is only as useful as its input pipeline. If editing the plant requires editing source, organisers won't use it.
|
||||
|
||||
## What I Would Change
|
||||
## What I'd change
|
||||
|
||||
Today I would formalise the model limitations more clearly. A convincing simulation can be useful, but it should say exactly what it does and does not claim.
|
||||
|
||||
I would also add recorded scenarios and regression tests. Simulation projects are vulnerable to accidental behaviour changes that still look plausible on screen.
|
||||
- **State what the model claims.** A convincing sim needs an honest README about what it does and doesn't model. Mine didn't. Anyone who took the numbers seriously could have walked away believing more than the model deserved.
|
||||
- **Recorded scenarios as regression tests.** Sim projects drift in ways that look plausible on screen. Storing "this input over 60 seconds produces these outputs" would have caught me when I broke the temperature solver on Saturday morning at the event.
|
||||
- **Skip JavaFX.** Cross-platform packaging was painful and the desktop dependency made the editor harder to hand off than it should have been. A web-based editor in the same browser the monitors used would have meant one fewer install for the organisers.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: A Proof-of-Concept Photo Colour Grader
|
||||
description: A web UI experiment for selecting colours and transforming nearby ranges based on colour distance.
|
||||
title: A Colour Grader Where Distance Was the Whole Idea
|
||||
description: Pick a colour, transform every nearby colour as a function of distance. A proof-of-concept grader I built to try one interaction idea.
|
||||
date: 2026-04-30
|
||||
projectPeriod: 'June 2018'
|
||||
thumbnail:
|
||||
|
|
@ -9,13 +9,13 @@ thumbnail:
|
|||
tags: ['graphics', 'web', 'tools']
|
||||
role: Interface and image processing author
|
||||
stack: ['JavaScript', 'Canvas', 'Image processing']
|
||||
outcome: A proof-of-concept colour grading interaction model
|
||||
outcome: A working proof-of-concept grader and an interaction model I'd still defend
|
||||
audience: technical
|
||||
links: []
|
||||
---
|
||||
|
||||
This was a colour grader web application I built as a proof-of-concept to try out a few interaction ideas.
|
||||
A web-based photo grader I built to try one specific interaction idea: you pick colours from the image, and any transformation you apply spreads to other colours weighted by how far they are from your selection in colour space.
|
||||
|
||||
The main feature was the colour selector UI. The core idea was that you could select some colours and then apply transformations to other colours as a function of their distance to the selected colour.
|
||||
The UI was a colour wheel where you'd click to drop a marker, drag to move it, click anywhere to add another. Each marker had its own settings. The transformations smoothly fell off with distance, so editing "this orange" subtly nudged the nearby reds and yellows without me ever having to think in masks.
|
||||
|
||||
Clicking a coloured circle let you change its settings. New circles could be created by clicking inside the large circle, and they could be moved with drag and drop.
|
||||
I never built it into a real tool. The idea — that distance in colour space is the natural unit for prose-style editing of an image — still feels right to me. If I returned to it, I'd reach for WebGL instead of canvas to make the interaction live-preview-able on a real photo.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: A Static Photo Site Generator
|
||||
description: A simple photography site generated from a directory of images with automatic resizing to multiple quality settings.
|
||||
title: A Photo Site That Generated Itself From a Folder
|
||||
description: A Webpack script that turns a folder of photos into a static site with responsive image variants. Mostly here as an excuse to talk about walks.
|
||||
date: 2026-04-27
|
||||
projectPeriod: 'Summer 2016'
|
||||
thumbnail:
|
||||
|
|
@ -9,13 +9,13 @@ thumbnail:
|
|||
tags: ['web', 'tools']
|
||||
role: Site generator author
|
||||
stack: ['Webpack', 'Image processing', 'Static site generation']
|
||||
outcome: A generated static photo site for publishing photography with responsive image output
|
||||
outcome: A photography site that updated itself when I dropped new images into a folder
|
||||
audience: general
|
||||
links: []
|
||||
---
|
||||
|
||||
Photos was a small webpage where you could view my photos.
|
||||
A Webpack script that took a directory of full-size photos and produced a static site with multiple resized variants per image. Drop a new photo into the source folder, run the build, get a deployable site.
|
||||
|
||||
Taking time to appreciate the world around us fills me with joy, which is why I like to go on walks with a camera. I might not end up with great photos, but I usually come back with some inspiration for the current or next project.
|
||||
The reason this exists at all: I take walks with a camera. Most of what I shoot isn't good, but the act of walking slowly with a frame to think about is the most reliable way I know to come back with an idea for whatever I'm working on. The site was an excuse to make that habit visible.
|
||||
|
||||
The site itself was generated by a Webpack script from a directory of images. Automatic resizing to multiple quality settings was part of the pipeline.
|
||||
If I rebuilt it today I'd use Astro, which is what this site runs on.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: A 3D Platform Game in C and SDL 1.2
|
||||
description: 'My first proper project: a 3D game with random maps, destructible voxels, enemies, powerups, and time slowdown.'
|
||||
title: A 3D Voxel Game in C, Built While Learning Pointers
|
||||
description: My Basics of Programming course project. 3D platformer in C with SDL 1.2, destructible terrain, time-slowdown powerups, and a great many segmentation faults.
|
||||
date: 2026-04-28
|
||||
projectPeriod: 'Autumn 2017'
|
||||
thumbnail:
|
||||
|
|
@ -9,15 +9,14 @@ thumbnail:
|
|||
tags: ['games', 'systems']
|
||||
role: Game author
|
||||
stack: ['C', 'SDL 1.2', 'Voxel terrain']
|
||||
outcome: A playable 3D course project that made programming feel like the right long-term direction
|
||||
outcome: A playable course project, and the moment programming clicked
|
||||
audience: technical
|
||||
links: []
|
||||
---
|
||||
|
||||
This was my first proper project: a 3D game written in pure C on top of SDL 1.2.
|
||||
The course project that convinced me to keep going.
|
||||
|
||||
The maps were randomly generated and destructible voxel by voxel. That let the player build structures to hide from flying enemies, which chased the player and could destroy the terrain after merging together and growing larger.
|
||||
Pure C, SDL 1.2, no engine, no scripting layer. The maps were randomly generated and destructible voxel by voxel, which meant the player could dig their way out of trouble, or build walls against flying enemies that would merge into larger ones as they got closer. Powerups let you shoot, or slow down time at the cost of points.
|
||||
|
||||
After collecting enough powerups, the player could shoot and even slow down time, in exchange for losing some points.
|
||||
What I actually learned was pointers, painfully, through an adequate number of segfaults. The course was meant to teach the basics of programming; for me it was the moment programming stopped feeling like a list of facts and started feeling like a thing I could build with.
|
||||
|
||||
I built it as the final project for my Basics of Programming course. I learned a lot about pointers after an adequate number of segmentation faults, and it was the project that convinced me programming was the right long-term direction.
|
||||
I'd write almost none of it the same way today, and I'd defend every choice in it anyway. First-project privilege.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: A 3-Way Text Merger That Never Shows Conflict Markers
|
||||
description: How reconcile-text borrows the idea of operational transformation and applies it to consolidated diffs to auto-resolve conflicting edits.
|
||||
description: reconcile-text merges Markdown notes from three editors I don't control, with no operation history. Here's why git, CRDTs, and diff-match-patch each failed me.
|
||||
date: 2026-05-21
|
||||
projectPeriod: '2025'
|
||||
thumbnail:
|
||||
|
|
@ -8,10 +8,10 @@ thumbnail:
|
|||
alt: The reconcile-text logo and tagline "Conflict-free 3-way text merging".
|
||||
tags: ['systems', 'tools', 'web']
|
||||
featuredOrder: 2
|
||||
role: Author
|
||||
role: Library author
|
||||
stack: ['Rust', 'WebAssembly', 'Python', 'pyo3', 'wasm-bindgen']
|
||||
scale: One Rust core, three published packages (crates.io, npm, PyPI), driving an Obsidian sync plugin
|
||||
outcome: A small, well-tested library that fills a gap between git, CRDTs, and patch-based merging
|
||||
outcome: A small Rust library that auto-resolves prose conflicts, with WASM and Python bindings
|
||||
audience: recruiter-relevant
|
||||
links:
|
||||
- label: Demo
|
||||
|
|
@ -28,55 +28,45 @@ media:
|
|||
- type: image
|
||||
src: ./_assets/reconcile.png
|
||||
alt: The reconcile-text logo, a stylised merge arrow, with the tagline "Conflict-free 3-way text merging".
|
||||
caption: reconcile-text resolves conflicting edits to prose by weaving them together instead of asking a human to choose.
|
||||
caption: reconcile-text weaves conflicting edits together instead of asking a human to choose.
|
||||
---
|
||||
|
||||
`reconcile-text` started from a concrete need. I wanted to synchronise Markdown notes across devices where the editor was not under my control, and where the only thing I could observe was the final text on each side. Vim on one machine, VS Code on another, Obsidian on a third. No keystroke stream, no operation log, just the documents and a shared common ancestor from the last successful sync.
|
||||
**The two-bullet version:**
|
||||
|
||||
That setting is awkward for almost every existing tool. Git is the closest fit, but `git merge-file` answers conflicts with markers, which is exactly what a sync tool cannot ship to a user's note. CRDTs and operational transformation assume you control the editing infrastructure all the way down to the keystroke. `diff-match-patch` produces patches without a common ancestor, and on adjacent edits it silently corrupts the output. None of these matched the shape of the problem I had.
|
||||
- Given a parent text and two edited versions, return one merged string. No conflict markers, no dropped edits, no operation log required.
|
||||
- Single Rust core, shipped as a crate, an npm package (via wasm-bindgen), and a PyPI package (via pyo3). The Obsidian sync plugin I wrote alongside it is the first consumer.
|
||||
|
||||
So I wrote a library that does one specific thing: given a parent and two edited versions, return a single merged text that contains both sets of changes, without conflict markers and without dropping edits on the floor.
|
||||
## Why I wrote it
|
||||
|
||||
## The Problem
|
||||
I keep Markdown notes in three editors I don't control the internals of: Vim on my laptop, VS Code on my work machine, Obsidian on my phone. When two of them edit the same note between syncs, I have three files: the last-synced parent and two divergent children. That's the input. I want one merged file out, and I want to hand it back to the editors without conflict markers, because `<<<<<<< HEAD` is not something a notes app should ever show me.
|
||||
|
||||
The hard part is not detecting a conflict. The hard part is resolving it well enough that a human is happy to read the result without thinking about merge mechanics.
|
||||
Every existing tool got close and missed:
|
||||
|
||||
Source code has hard correctness requirements, so refusing to choose and emitting markers is the right default. Human prose is more forgiving. A merged paragraph that is slightly clumsy is almost always preferable to one that interrupts the reader with `<<<<<<< HEAD`. That observation is the entire reason this library exists in the form it does.
|
||||
- `git merge-file` does exactly the right thing structurally, then writes markers into the output. That's correct for source code and wrong for prose.
|
||||
- CRDTs and OT both assume you own the editing pipeline down to the keystroke. I don't. I'm looking at three files.
|
||||
- `diff-match-patch` doesn't take a common ancestor. On adjacent edits it quietly produces wrong output. I have a runnable example in the repo.
|
||||
|
||||
The challenge was to commit to that asymmetry honestly. The library should always produce a result. It should never silently lose an edit. It should preserve cursors so a collaborative editor can rely on it. And it should do all of this from end states alone, with no operation history available.
|
||||
So the library does exactly one thing: pure function from three strings to one. No async, no networking, no concurrency, no plugins. Anything outside that boundary is somebody else's library.
|
||||
|
||||
## Constraints
|
||||
## The decisions worth naming
|
||||
|
||||
The library had to live in three places: a Rust crate, a JavaScript package built through WebAssembly, and a Python package built through `pyo3`. The cross-language story was a constraint, not a stretch goal. The Obsidian plugin I was writing alongside it consumed the npm build, but I also wanted a clean Rust crate for sync engines and a Python package for scripting.
|
||||
**Myers diff per side, then weave the diffs.** Each child is diffed against the parent, the two edit scripts are optimised so adjacent changes group cleanly, then a single weaving pass interleaves them into one ordered op sequence that produces the merged text. The weave borrows the shape of operational transformation, but the inputs are batched complete diffs, not live keystrokes, so it only runs once per merge.
|
||||
|
||||
That ruled out anything that depended on language-specific runtime tricks. Generics, closures, and trait objects could live freely inside the Rust core, but the public surface had to be flat enough to cross both `wasm-bindgen` and `pyo3` without per-binding glue.
|
||||
**Tokeniser is the user knob.** This is the choice I'd defend hardest. Most of what people want when they say "merge differently" isn't a new algorithm — it's a different unit. Word-level tokenisation turns most "conflicts" in prose into two adjacent edits that coexist. Line-level makes it behave like `git merge-file`. Markdown-level merges on headings and list items. Same engine, four different products depending on what you call a token.
|
||||
|
||||
It also had to be predictable. There is no async story, no networking, no concurrency. A merge is a pure function from three strings to one string with some metadata. Everything that is not the merge itself was deliberately kept out.
|
||||
**Cursors are first-class merge inputs.** Each cursor has a stable ID and rides through the merge so a collaborative editor can ask "where did this cursor go?" without reconstructing it from the output text. This is the bit that made it useful to anything that wasn't just my sync script.
|
||||
|
||||
## Design
|
||||
**The Rust core is generic; the FFI surface is not.** Inside Rust, the tokeniser is a `dyn Fn(&str) -> Vec<Token<T>>`. That dies the moment you try to pass it through wasm-bindgen or pyo3. The fix was a closed enum of built-in tokenisers for non-Rust callers, with the generic version reserved for Rust users. Not elegant, but the alternative was per-binding glue forever.
|
||||
|
||||
The pipeline is short. The library tokenises the parent and the two edited versions, runs Myers' diff to compare each edited version against the parent, optimises the resulting edit sequences so that adjacent changes group together cleanly, and then weaves the two diffs into a single ordered sequence of operations that produces the merged text.
|
||||
**WASM size mattered enough to tune for it.** The release profile is aggressive about size, and the JS package ships a small leak detector that warns if you forget to free wasm-bindgen objects. I lost an afternoon to that the first time and didn't want anyone else to.
|
||||
|
||||
The weaving step borrows the concept of operational transformation, but applies it to a different problem. Classic OT transforms individual keystrokes against each other in real time. Here, OT is applied to the consolidated diff output of two complete edits. The structure is similar, but the inputs are batched and the algorithm only needs to run once per merge point. It became the simplest way I could find to describe how two sets of changes should be interleaved.
|
||||
## What's held up, what I'd change
|
||||
|
||||
The tokeniser turned out to be more important than I initially expected. It is what decides whether a conflict exists in the first place. Word-level tokenisation, the default for prose, often turns a "conflict" into two adjacent independent edits that can coexist. Line-level tokenisation makes the library behave more like `git merge-file`. Markdown-level tokenisation merges on headings and list items rather than characters. Exposing this as a user-facing knob meant the library could be shaped to the document, not the other way around.
|
||||
- **Kept:** the never-emits-markers, never-drops-edits guarantee. It's the only reason a sync engine can call this library without an escape hatch.
|
||||
- **Kept:** the comparison example against `diff-match-patch`. It's a runnable program in the repo showing exact inputs where the alternative is wrong. Way more convincing than a benchmark table.
|
||||
- **Cut:** the snapshot tests do well on regressions and badly on unknown edge cases. Three-way merging is exactly what proptest was made for, and I should have written generators on day one.
|
||||
- **Next:** I want to be more explicit about the boundary. reconcile-text is a merge primitive, not a live collab engine. If you have a keystroke stream and a real-time channel, use Yjs or Automerge. This library is for when you don't.
|
||||
|
||||
Cursors and selections were added as first-class merge inputs rather than something users reconstruct after the fact. Each cursor carries a stable ID and rides through the merge, ending up at a sensible position even when both sides edited the surrounding text. This is what made the library useful to anything resembling a collaborative editor.
|
||||
## If you take one idea from this
|
||||
|
||||
The cross-language surface needed extra care. The tokeniser inside Rust is a `dyn Fn(&str) -> Vec<Token<T>>`, which is convenient in Rust and impossible to pass through `wasm-bindgen` or `pyo3`. The fix was to expose a closed enum of built-in tokenisers to non-Rust callers and reserve the generic version for Rust users. WebAssembly users also paid a real binary-size cost, so the release profile is tuned aggressively, and the JS package ships a small leak detector to remind callers that wasm-bindgen objects must be freed explicitly.
|
||||
|
||||
## What Worked
|
||||
|
||||
The strongest part of the project is that the result never has conflict markers and never silently drops an edit. That sounds modest, but it is exactly the property that makes the library usable inside a sync engine without an escape hatch.
|
||||
|
||||
Choosing the tokeniser as the main user-facing knob also held up well. Most of the "tuning" people want when merging prose is not a different algorithm, it is a different idea of what counts as a unit. Letting users choose between character, word, line, and Markdown granularity covered the realistic cases without inventing new merge strategies.
|
||||
|
||||
The comparison example against `diff-match-patch` was probably the most useful piece of writing in the repository. It is a runnable program, not a benchmark table, showing concrete cases where a popular alternative quietly produces wrong output. Having that as a falsifiable claim in the source tree made the value proposition much clearer than any prose description would have.
|
||||
|
||||
## What I Would Change
|
||||
|
||||
If I revisited this now, I would invest more in formal property tests around the merge. Three-way merging is exactly the kind of problem where generated inputs find behaviours that hand-written tests do not, and the snapshot tests I have are good at catching regressions but not at finding unknown edge cases.
|
||||
|
||||
I would also be more explicit about the boundary the library does not cross. It is a merge point primitive, not a live collaboration engine. CRDTs and OT remain the right tools when you actually have a keystroke stream and a real-time channel. `reconcile-text` is for the part of the problem space where you do not.
|
||||
|
||||
The part I would keep is the asymmetry the project rests on. Human text deserves a merger that prefers a slightly imperfect sentence over a conflict marker, and that decision is what shaped every other choice in the design.
|
||||
Prose deserves a merger that prefers a slightly clumsy sentence over a marker. Code doesn't. That one asymmetry is the whole reason the library exists in the shape it does; everything else fell out of taking it seriously.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: Tile-Based Optimization for 2D SDF Ray Tracing
|
||||
description: How SDF-2D used signed distance fields, dynamic shaders, and tile-based rendering ideas to make 2D ray tracing run well in the browser.
|
||||
title: A 2D Ray Tracer for the Browser, Tuned for the Phone in Your Pocket
|
||||
description: My BSc thesis library. The mobile GPU constraint did the architectural work — tile-based passes, deferred shading, shaders generated per scene and device.
|
||||
date: 2026-05-08
|
||||
projectPeriod: 'Autumn-Winter 2020'
|
||||
thumbnail:
|
||||
|
|
@ -9,9 +9,10 @@ thumbnail:
|
|||
tags: ['graphics', 'web', 'systems']
|
||||
featuredOrder: 3
|
||||
role: Library author
|
||||
stack: ['TypeScript', 'WebGL', 'WebGL2', 'Signed distance fields']
|
||||
scale: Browser library with mobile-oriented real-time rendering and reusable demos
|
||||
outcome: Reusable NPM package and thesis project for efficient 2D SDF rendering
|
||||
stack:
|
||||
['TypeScript', 'WebGL', 'WebGL2', 'Signed distance fields', 'Dynamic shader generation']
|
||||
scale: Browser library, mobile-targeted, real-time on consumer GPUs, both WebGL1 and WebGL2 paths
|
||||
outcome: An NPM package and BSc thesis; the renderer behind the decla.red multiplayer game
|
||||
audience: recruiter-relevant
|
||||
links:
|
||||
- label: NPM package
|
||||
|
|
@ -25,43 +26,35 @@ media:
|
|||
- type: image
|
||||
src: ./_assets/sdf2d.jpg
|
||||
alt: Browser demo page showing SDF-2D scenes rendered with soft lighting effects.
|
||||
caption: SDF-2D was built as a reusable TypeScript library rather than a single demo.
|
||||
caption: SDF-2D shipped as a TypeScript library, not a one-shot demo. That distinction shaped most of the design.
|
||||
---
|
||||
|
||||
SDF-2D was my attempt to make a small, reusable browser library for 2D scenes rendered with ray-tracing techniques. The rendering is based on signed distance fields, where geometry can be represented as functions that return the distance to the nearest surface.
|
||||
**The short version:**
|
||||
|
||||
The interesting part was not the basic idea. Signed distance fields are a known technique. The interesting part was making the approach fast and reusable enough for browser demos, including on mobile devices.
|
||||
- 2D ray tracing in the browser via signed distance fields. Soft shadows, smooth reflections, no triangle mesh.
|
||||
- The hard constraint was running on a mid-range Android phone, not a desktop GPU. That single requirement picked tile-based passes, deferred shading, and shaders generated per scene and per device.
|
||||
- Half of my BSc thesis. The other half was [decla.red](/articles/declared-shared-simulation-code/), the multiplayer game built on top of it, which proved the library survived a real game loop.
|
||||
|
||||
The project became one half of my BSc thesis, together with the multiplayer game `decla.red`, which used the rendering library in a real interactive setting.
|
||||
## What "mobile GPU" actually meant
|
||||
|
||||
## The Problem
|
||||
A 2D SDF ray tracer is conceptually simple: for each pixel, march along a ray, sample the distance field, accumulate light. The implementation that works on a desktop NVIDIA card spends so much per pixel that a mobile GPU melts. So the design problem was never "can SDFs do soft shadows" (yes, easily), it was "what work can I avoid per pixel without giving up the look."
|
||||
|
||||
Ray tracing and distance-field rendering can produce appealing 2D lighting and reflections, but a straightforward implementation spends too much work per pixel. A browser library also has to deal with device variation: WebGL capabilities, shader limits, mobile GPUs, and the overhead of generating scenes dynamically.
|
||||
Three constraints did most of the design work:
|
||||
|
||||
The goal was not to render one hand-tuned scene. The goal was a library with a simple API, reusable scene definitions, and real-time behavior.
|
||||
- **WebGL1 and WebGL2 both supported.** No "modern browser only" cheat. That ruled out anything that needed compute shaders or storage buffers.
|
||||
- **No per-scene hand-tuned shader.** This is a library; users plug in their own scene descriptions. The renderer has to compile something appropriate at runtime.
|
||||
- **Acceptable on a phone.** Not "good when the user owns the right hardware" — acceptable on the laptop my advisor used to grade the thesis.
|
||||
|
||||
## Constraints
|
||||
## How it actually runs
|
||||
|
||||
The library had to support both WebGL and WebGL2. It had to run acceptably on phones. It had to avoid shipping scene-specific shader code by hand. And it had to expose an API that felt like a rendering library rather than a shader experiment.
|
||||
- **Tile-based rendering.** Group pixels and reason about them together. Most regions of a frame share the same nearby geometry, so you can early-out enormous swathes of pixel work if you know the tile's bounds. This was the single biggest perf win.
|
||||
- **Deferred shading.** Separate "find the surface" from "shade the surface." Shadow casting and reflections need the same geometry queries; doing them once per pixel and reusing the result was worth the extra texture bandwidth.
|
||||
- **Generated shaders per scene and device.** If a scene has no reflective surfaces, the generated shader doesn't carry the reflection path. If the device only supports WebGL1, the shader doesn't reach for WebGL2 features. Static feature flags do this badly; runtime generation does it well.
|
||||
- **TypeScript scene descriptions, no DSL.** I prototyped a small DSL for SDF authoring and threw it away. Users describe scenes in plain TypeScript and the library compiles them down. A DSL would have meant one more language to teach and one more compiler to debug.
|
||||
|
||||
Those constraints pushed the implementation toward generated shaders and capability-aware rendering paths.
|
||||
## Held up, didn't hold up
|
||||
|
||||
## Design
|
||||
|
||||
The main optimization was inspired by tiled renderers. Instead of treating the entire screen uniformly, the renderer could reason about groups of pixels and avoid unnecessary work where possible.
|
||||
|
||||
That was paired with deferred shading and dynamic shader generation. Dynamic generation mattered because scenes and devices differ. If a feature or operation was not needed for a given scene or device, the generated shader could avoid carrying that cost.
|
||||
|
||||
The API was deliberately kept in TypeScript. That made the library easier to package, document, and reuse in projects that were already browser-first.
|
||||
|
||||
## What Worked
|
||||
|
||||
The project worked best when the library boundary was respected. A good demo can hide a messy implementation. A reusable package cannot. The API had to explain the rendering model without making every user think like a shader compiler.
|
||||
|
||||
The mobile constraint also improved the design. It forced performance work to be structural rather than cosmetic. When a technique works only on a powerful desktop GPU, it is easy to mistake headroom for good architecture.
|
||||
|
||||
## What I Would Change
|
||||
|
||||
Today I would write more instrumentation around shader variants and device behavior. The project had many optimizations, but stronger profiling output would have made tradeoffs easier to explain and compare.
|
||||
|
||||
I would also document the rendering pipeline with diagrams. The ideas are visual, and the explanation should be too.
|
||||
- **Held up:** the mobile constraint forced structural perf work instead of cosmetic perf work. When something only runs on a desktop GPU you mistake headroom for good architecture, and the rude awakening comes from a user.
|
||||
- **Held up:** keeping the library boundary clean. A demo can hide a messy implementation; a published package can't.
|
||||
- **Didn't:** I had no instrumentation around shader variants. Today I'd ship a small `?debug=1` overlay that prints exactly which shader got compiled for that session and why.
|
||||
- **Didn't:** the docs are words about ray marching. The ideas are visual; the explanation should have been too. Diagrams next time.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: Ad Astra
|
||||
description: A tiny embedded game engine and custom PCB built around an ATtiny85V.
|
||||
description: A handheld game built from a custom PCB up — ATtiny85V, OLED, IR, EEPROM. 8-bit ALU at 8 MHz, 50 FPS floor.
|
||||
thumbnail:
|
||||
src: ./_assets/ad-astra.jpg
|
||||
alt: The Ad Astra handheld game running on its OLED display.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: Avoid
|
||||
description: A small early web game, kept as an archive of first experiments on the web.
|
||||
description: My first browser game, kept around so the timeline is honest.
|
||||
thumbnail:
|
||||
src: ./_assets/avoid.jpg
|
||||
alt: Screenshot of the Avoid canvas game.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: City Simulation
|
||||
description: A Unity traffic simulation where REST-controlled traffic lights could produce visible consequences for a cybersecurity challenge.
|
||||
description: A Unity city where REST-controlled traffic lights made bad PLC code visible as car crashes.
|
||||
thumbnail:
|
||||
src: ./_assets/city-simulation.jpg
|
||||
alt: Screenshot of a Unity city traffic simulation.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: Photo Colour Grader
|
||||
description: A proof-of-concept colour grading UI based on selecting colours and transforming nearby colour ranges.
|
||||
description: Pick a colour, edit every nearby colour as a function of distance. A grader built around one interaction idea.
|
||||
thumbnail:
|
||||
src: ./_assets/photo-colour-grader.jpg
|
||||
alt: Screenshot of a colour grading interface applied to a photograph.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: decla.red
|
||||
description: A team-based mobile multiplayer browser game with shared client/server game logic.
|
||||
description: Mobile multiplayer browser game where the client and server linked the same TypeScript game-rules module.
|
||||
thumbnail:
|
||||
src: ./_assets/declared.jpg
|
||||
alt: The decla.red browser game interface showing a space scene.
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
---
|
||||
title: Fleeting Garden
|
||||
description: A WebGPU drawing toy where coloured strokes spawn agents that follow them, branch off, and slowly rewrite the patch you laid down.
|
||||
description: A single-file WebGPU drawing toy. Your strokes seed a swarm; nine numbers per vibe give each preset its personality.
|
||||
thumbnail:
|
||||
src: ./_assets/fleeting-garden.jpg
|
||||
alt: A kaleidoscopic Fleeting Garden snapshot of cyan, violet, and yellow agent trails radiating from a central knot.
|
||||
period: '2026'
|
||||
sortDate: 2026-05-01
|
||||
technologies: ['TypeScript', 'WebGPU', 'WGSL', 'Compute shaders', 'Vite', 'Physarum']
|
||||
technologies: ['TypeScript', 'WebGPU', 'WGSL', 'Compute shaders', 'Vite', 'Tweakpane']
|
||||
selected: true
|
||||
essay: fleeting-garden-webgpu-drawing
|
||||
links:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: Foreign Exchange Prediction Experiment
|
||||
description: A frequency-domain prediction experiment using smoothing, differentiation, STFT, extrapolation, and inverse transforms.
|
||||
description: A Hanning-windowed STFT experiment on EUR/USD. Passable backtest, sober conclusions, no real money risked.
|
||||
thumbnail:
|
||||
src: ./_assets/forex.jpg
|
||||
alt: Chart from a foreign exchange prediction experiment.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: GreatAI
|
||||
description: A Python framework and research project for making AI deployment best practices easier to adopt.
|
||||
description: One decorator on a Python function turned it into a deployed ML service. MSc thesis with a survey to back the API choices.
|
||||
thumbnail:
|
||||
src: ./_assets/great-ai.png
|
||||
alt: Example Python code using the GreatAI API.
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
---
|
||||
title: Lights Synchronized to Music
|
||||
description: A Raspberry Pi music player that drove RGB LED strips from audio analysis.
|
||||
description: Raspberry Pi music player, NumPy FFT, MOSFETs, RGB strips. The first thing I built that I actually finished.
|
||||
thumbnail:
|
||||
src: ./_assets/leds.jpg
|
||||
alt: RGB LED strips glowing from a music synchronization project.
|
||||
period: 'Spring 2016'
|
||||
sortDate: 2016-04-01
|
||||
technologies: ['Python', 'NumPy', 'FFT', 'Raspberry Pi', 'Vanilla web']
|
||||
technologies: ['Python', 'NumPy', 'FFT', 'Raspberry Pi', 'MOSFETs', 'vanilla web']
|
||||
selected: false
|
||||
essay: lights-synchronized-to-music
|
||||
links: []
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
---
|
||||
title: My Notes
|
||||
description: A minimalist Android markdown note organizer and editor powered by Markwon.
|
||||
description: A small Android Markdown note app. The point was a few weeks outside the web stack.
|
||||
thumbnail:
|
||||
src: ./_assets/my-notes.png
|
||||
alt: Screenshot of the My Notes Android markdown app.
|
||||
period: 'November 2019'
|
||||
sortDate: 2019-11-01
|
||||
technologies: ['Android', 'Kotlin/Java', 'Markdown', 'Markwon']
|
||||
technologies: ['Android', 'Markdown', 'Markwon']
|
||||
selected: false
|
||||
essay: my-notes-android-markdown-app
|
||||
links:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: Graph Editor
|
||||
description: A JavaFX editor for creating and editing input graphs for the cooling system simulator.
|
||||
description: A drag-and-drop JavaFX editor that let event organisers reconfigure the cooling sim without me sitting next to them.
|
||||
thumbnail:
|
||||
src: ./_assets/process-simulator-input.jpg
|
||||
alt: JavaFX editor interface for the cooling system simulator input graph.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: Cooling System Simulation
|
||||
description: A graph-based process simulation with a monitoring client and JavaFX input editor.
|
||||
description: A live cooling-plant simulator for a PLC cybersecurity event. Flow as graph traversal, heat as a matrix solve — two passes instead of one PDE.
|
||||
thumbnail:
|
||||
src: ./_assets/nuclear-simulation.jpg
|
||||
alt: Cooling system simulator interface with pipes, pumps, and temperature values.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: Photo Site Generator
|
||||
description: A static photo site generated from a directory of images, with automatic resizing to multiple quality settings.
|
||||
description: Point a Webpack script at a folder of photos, get a static site with responsive image variants. An excuse to walk with a camera.
|
||||
thumbnail:
|
||||
src: ./_assets/photos.jpg
|
||||
alt: Screenshot of a generated photography site.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
title: Platform Game
|
||||
description: An early 3D game in C with SDL 1.2, random maps, destructible voxels, enemies, powerups, and time slowdown.
|
||||
description: My Basics of Programming project. 3D voxel game in C and SDL 1.2. Pointers, learned painfully.
|
||||
thumbnail:
|
||||
src: ./_assets/platform-game.jpg
|
||||
alt: Screenshot from an early 3D platform game.
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
---
|
||||
title: reconcile-text
|
||||
description: A Rust library that auto-resolves conflicting text edits without conflict markers, with WebAssembly and Python bindings.
|
||||
description: One Rust core, three packages. Merges Markdown notes from three editors I don't control, with no operation history. Never emits markers.
|
||||
thumbnail:
|
||||
src: ./_assets/reconcile.png
|
||||
alt: The reconcile-text logo and tagline "Conflict-free 3-way text merging".
|
||||
period: '2025'
|
||||
sortDate: 2025-05-01
|
||||
technologies: ['Rust', 'WebAssembly', 'Python', 'pyo3', 'Operational Transformation']
|
||||
technologies: ['Rust', 'WebAssembly', 'Python', 'pyo3', 'wasm-bindgen', 'Myers diff']
|
||||
selected: true
|
||||
essay: reconcile-text-3-way-merge
|
||||
links:
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
---
|
||||
title: SDF-2D
|
||||
description: A browser rendering library for optimized 2D ray tracing with signed distance fields.
|
||||
description: A browser 2D ray-tracer tuned for the phone in your pocket. Tile-based passes, deferred shading, shaders generated per scene and device.
|
||||
thumbnail:
|
||||
src: ./_assets/sdf2d.jpg
|
||||
alt: SDF-2D browser demo with soft lighting effects.
|
||||
period: 'Autumn-Winter 2020'
|
||||
sortDate: 2020-12-01
|
||||
technologies: ['TypeScript', 'WebGL', 'WebGL2', 'SDF rendering']
|
||||
technologies: ['TypeScript', 'WebGL', 'WebGL2', 'Signed distance fields']
|
||||
selected: true
|
||||
essay: sdf-2d-ray-tracing
|
||||
links:
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
---
|
||||
title: Life Towers
|
||||
description: A multi-device goal tracker where the lasting idea was syncing state with immutable tries.
|
||||
description: A multi-device goal tracker. The trie underneath made the sync diff free; the towers were just the UI.
|
||||
thumbnail:
|
||||
src: ./_assets/towers.jpg
|
||||
alt: Life Towers goal tracking interface with tower-like visual structures.
|
||||
period: 'August-September 2019'
|
||||
sortDate: 2019-09-01
|
||||
technologies: ['Python', 'Angular', 'State synchronization']
|
||||
technologies: ['Python', 'Angular', 'TypeScript', 'Immutable trie']
|
||||
selected: true
|
||||
essay: life-towers-immutable-tries
|
||||
links:
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export const site = {
|
|||
name: 'Andras Schmelczer',
|
||||
title: 'Andras Schmelczer — Software engineer',
|
||||
description:
|
||||
'Andras Schmelczer writes about software systems, AI deployment, graphics, simulations, and tools.',
|
||||
'Notebook of someone who keeps reaching for the same two moves: let the hard constraint pick the data structure, then keep the API small enough to defend.',
|
||||
url: 'https://schmelczer.dev',
|
||||
email: 'andras@schmelczer.dev',
|
||||
github: 'https://github.com/schmelczer',
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
} from '../lib/site';
|
||||
import defaultOg from '../assets/og-default.jpg';
|
||||
|
||||
const STARTING_POINTS = 4;
|
||||
const STARTING_POINTS = 5;
|
||||
|
||||
const posts = await getPublishedPosts();
|
||||
const startingPoints = posts
|
||||
|
|
@ -18,13 +18,27 @@ const startingPoints = posts
|
|||
.sort((a, b) => (a.data.featuredOrder ?? 99) - (b.data.featuredOrder ?? 99))
|
||||
.slice(0, STARTING_POINTS);
|
||||
|
||||
const STARTING_POINT_NOTES: Record<string, string> = {
|
||||
'greatai-ai-deployment-api': 'Small API as policy.',
|
||||
'reconcile-text-3-way-merge':
|
||||
'Constraints (no history, three runtimes) pick the design.',
|
||||
'sdf-2d-ray-tracing': 'Mobile GPU as the architecture.',
|
||||
'life-towers-immutable-tries': 'Data structure as the protocol.',
|
||||
'nuclear-cooling-simulation': 'Two graphs are simpler than one big one.',
|
||||
};
|
||||
|
||||
const startingPointsAnnotated = startingPoints.map((post) => ({
|
||||
post,
|
||||
note: STARTING_POINT_NOTES[post.id.replace(/\.mdx?$/, '')],
|
||||
}));
|
||||
|
||||
const personImage = await optimizeOgImage(defaultOg);
|
||||
|
||||
// Canonical Person JSON-LD. Other pages reference this entity by @id.
|
||||
const personJsonLd = buildPersonJsonLd({
|
||||
jobTitle: 'Software Engineer',
|
||||
description:
|
||||
'Software engineer with an MSc in Computer Science working on AI/ML systems, web platforms, graphics, simulations, and tools.',
|
||||
'Software engineer who keeps reaching for the same two moves: let the hard constraint pick the data structure, then keep the API small enough to defend.',
|
||||
knowsAbout: [
|
||||
'Software architecture',
|
||||
'AI/ML systems',
|
||||
|
|
@ -40,24 +54,23 @@ const personJsonLd = buildPersonJsonLd({
|
|||
|
||||
<Page
|
||||
title="About"
|
||||
description="A direct summary of my background, technical interests, and best starting points."
|
||||
description="A few sentences about the two moves I keep reaching for, and the posts that show them in different shapes."
|
||||
jsonLd={personJsonLd}
|
||||
ogType="profile"
|
||||
>
|
||||
<div class="prose">
|
||||
<p>
|
||||
I am Andras Schmelczer, a software engineer with an MSc in Computer Science and more
|
||||
than six years of professional engineering experience. My work spans AI/ML systems,
|
||||
web platforms, graphics, simulations, and tools, and I like projects where
|
||||
architecture, constraints, and product usefulness all matter.
|
||||
I'm Andras. I write software for a living, and have done so for about six years. MSc
|
||||
in CS. The first non-trivial thing I finished was a Raspberry Pi music visualiser
|
||||
driving LED strips through MOSFETs in 2016, and I've been chasing that same feeling
|
||||
— pick something I can't yet do, finish it — ever since.
|
||||
</p>
|
||||
<p>
|
||||
I am especially interested in architecting and building large-scale systems,
|
||||
particularly around AI/ML. In my own time I also return to shaders, data
|
||||
visualization, simulations, and occasionally microcontrollers. The
|
||||
<a href="/articles/">articles</a> and <a href="/projects/">projects</a> indexes are the
|
||||
best way to understand that range; the CV and contact links are here when a direct summary
|
||||
is more useful.
|
||||
Two patterns show up in almost everything here. First, the hard constraint usually
|
||||
picks the data structure: an 8-bit ALU, a mobile GPU, a single static HTML file, a
|
||||
cross-language ABI, no edit history. Second, once the data structure is right, the
|
||||
API shrinks to something I can defend in one paragraph. When I get those two right I
|
||||
tend to like the result years later. When I don't, I say so in the writeup.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -65,17 +78,22 @@ const personJsonLd = buildPersonJsonLd({
|
|||
<h2 id="quick-facts">Quick Facts</h2>
|
||||
<dl>
|
||||
<div>
|
||||
<dt>Focus</dt>
|
||||
<dt>Lives in</dt>
|
||||
<dd>Code. Also Europe.</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Studied</dt>
|
||||
<dd>
|
||||
Software systems, AI deployment, architecture, graphics, data visualization
|
||||
MSc Computer Science. BSc thesis on SDF-2D, MSc thesis on GreatAI — both have
|
||||
writeups below.
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Education</dt>
|
||||
<dd>MSc in Computer Science</dd>
|
||||
<dt>Languages I'm fastest in</dt>
|
||||
<dd>TypeScript, Python, Rust. C or Rust when bytes matter.</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Contact</dt>
|
||||
<dt>Email</dt>
|
||||
<dd>
|
||||
<address>
|
||||
<a href={`mailto:${site.email}`}>{site.email}</a>
|
||||
|
|
@ -83,7 +101,7 @@ const personJsonLd = buildPersonJsonLd({
|
|||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Links</dt>
|
||||
<dt>Elsewhere</dt>
|
||||
<dd class="about-links">
|
||||
<a href={site.cv} rel="noopener">CV</a>
|
||||
<a href={site.github} rel="noopener me">GitHub</a>
|
||||
|
|
@ -95,26 +113,54 @@ const personJsonLd = buildPersonJsonLd({
|
|||
|
||||
<section class="about-section">
|
||||
<div class="section-heading">
|
||||
<h2 id="best-starting-points">Best Starting Points</h2>
|
||||
<a href="/articles/">Browse all articles <span aria-hidden="true">→</span></a>
|
||||
<h2 id="best-starting-points">Five posts that show the two moves</h2>
|
||||
<a href="/articles/">All articles <span aria-hidden="true">→</span></a>
|
||||
</div>
|
||||
<ArticleList posts={startingPoints} />
|
||||
<div class="prose starting-point-notes">
|
||||
<p><strong>Why these five:</strong></p>
|
||||
<ul>
|
||||
{
|
||||
startingPointsAnnotated.map(({ post, note }) =>
|
||||
note ? (
|
||||
<li>
|
||||
<strong>{post.data.title}</strong> — {note}
|
||||
</li>
|
||||
) : null
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="about-section facts">
|
||||
<h2 id="working-style">How I Work</h2>
|
||||
<h2 id="working-style">A few things I believe</h2>
|
||||
<div class="prose">
|
||||
<p>
|
||||
I am strongest when I can reason through a system end to end: the data model, the
|
||||
API shape, the performance constraints, the operational risks, and the human path
|
||||
through the tool. The projects on this site are older and newer examples of that
|
||||
habit.
|
||||
</p>
|
||||
<p>
|
||||
I care about simple interfaces over accidental complexity, and I prefer technical
|
||||
depth that can be explained clearly. That is why this site is structured around
|
||||
articles rather than screenshots and slogans.
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Most "interesting algorithm" problems are actually data-structure problems
|
||||
wearing a costume. Pick the structure that makes the comparison, the query, or
|
||||
the merge trivial, and the algorithm fits in a screen.
|
||||
</li>
|
||||
<li>
|
||||
A library you can explain in one paragraph beats a framework you have to teach.
|
||||
I'll take five lines of WGSL I can read over one beautiful 300-line kernel.
|
||||
</li>
|
||||
<li>
|
||||
The cost of a project is whether I'll still trust it in three years. The ones I
|
||||
trust are the ones with a tiny surface and a boring centre.
|
||||
</li>
|
||||
<li>
|
||||
I like working at the seams: firmware meeting a render loop, a Rust core
|
||||
crossing wasm-bindgen and pyo3, a client and server agreeing on what "next
|
||||
state" means. That's where the design work actually is.
|
||||
</li>
|
||||
<li>
|
||||
Caveat: the writeups here are biased toward things that worked. The dead
|
||||
prototypes don't have URLs and I should probably write up one of them honestly
|
||||
sometime.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</Page>
|
||||
|
|
|
|||
|
|
@ -24,17 +24,16 @@ const personJsonLd = buildPersonJsonLd();
|
|||
|
||||
<Base jsonLd={personJsonLd}>
|
||||
<section class="home-intro">
|
||||
<p class="eyebrow">
|
||||
Software systems, AI deployment, graphics, simulations, and tools
|
||||
</p>
|
||||
<p class="eyebrow">A notebook, written after the fact</p>
|
||||
<h1>
|
||||
Andras Schmelczer writes about building software that has to work under real
|
||||
constraints.
|
||||
Andras Schmelczer — writing up the projects, the trades I made inside them, and the
|
||||
ones I'd make differently now.
|
||||
</h1>
|
||||
<p>
|
||||
I am a software engineer with an MSc in Computer Science. This site is mostly a
|
||||
notebook of technical articles and project writeups; the hiring details live on the
|
||||
<a href="/about/">About</a> page.
|
||||
Most of these started because I couldn't yet do the thing. An 8-bit ALU, a mobile
|
||||
GPU, a single static HTML file, a cross-language ABI, three editors I didn't
|
||||
control. The <a href="/about/">About page</a> is where I describe what I keep reaching
|
||||
for; the posts below are the evidence.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
|
|
|||
|
|
@ -855,6 +855,16 @@
|
|||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
.starting-point-notes {
|
||||
margin-top: var(--space-6);
|
||||
color: var(--color-muted);
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.starting-point-notes p {
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.about-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue