Selected work / 05Independent
Entity tracking and render system
Two loops at their own speeds, and never a frame that’s half old and half new.
- When
- Dec 2024 to Jan 2025
- Language
- C++
- Libraries
- Windows API, GLM
- Build
- CMake
A simulation of the problem, not the program’s output: one thread rewrites a row of entities while another draws them.
01The problem
Read the state of every entity in a running process and draw it as an overlay at frame rate, while the process keeps running at its own speed.
Reading is slow and uneven. Drawing has to be steady. Neither loop can wait on the other.
02Why it was hard
01 · Tearing
Half-written frames
If the renderer reads while the reader is writing, a frame mixes two moments. Try the simulation above.
02 · Contention
Waiting is visible
Any time the renderer spends blocked on the reader shows up as a stutter.
03 · Projection
World to screen
Positions and skeletons have to land exactly where they are on screen, every frame, from the camera’s own data.
03What I built
- Reader threadBuilds each snapshot of the world in a buffer of its own, then swaps it into the shared slot under a spin lock and signals an event.
- Render threadSleeps on that Windows event and wakes only when a fresh snapshot lands, copies it out under the same lock, then projects and draws it.
- Start-up barrierA small signal type that lets the threads check in and start together.
- Cache threadRefreshes slow-changing state every few seconds, off both hot paths.
- ProjectionGLM rebuilds the view-projection matrix from the camera’s axes and field of view each frame, then projects positions and skeletons into screen space.
- InterfaceIts own menu system (tabs, sliders, list boxes, hotkeys) with saved configs, and support for three storefront builds of the same target.
04Decisions
| Decision | Instead of | Because |
|---|---|---|
| Build into a private buffer, then swap | Writing into the buffer the renderer reads | The renderer only ever sees whole snapshots. |
| A spin lock | A kernel mutex | Keeps the short swap-and-copy path in user space. |
| Wake on an event | Polling on a timer | No wasted frames, and no drawing the same snapshot twice. |
05The lock
The lock, trimmed to its core. It spins on a plain load and only tries the exchange when the lock looks free, so waiting threads don’t fight over the cache line.
struct SpinLock { std::atomic<bool> Lock; void lock() noexcept { while (true) { if (!Lock.exchange(true, std::memory_order_acquire)) return; while (Lock.load(std::memory_order_relaxed)) __builtin_ia32_pause(); // back off while someone holds it } } void unlock() noexcept { Lock.store(false, std::memory_order_release); } };
The reader holds it for one swap. The renderer holds it for one copy.
Next
Wingwise