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 thread
    Builds 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 thread
    Sleeps 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 barrier
    A small signal type that lets the threads check in and start together.
  • Cache thread
    Refreshes slow-changing state every few seconds, off both hot paths.
  • Projection
    GLM rebuilds the view-projection matrix from the camera’s axes and field of view each frame, then projects positions and skeletons into screen space.
  • Interface
    Its own menu system (tabs, sliders, list boxes, hotkeys) with saved configs, and support for three storefront builds of the same target.

04Decisions

DecisionInstead ofBecause
Build into a private buffer, then swapWriting into the buffer the renderer readsThe renderer only ever sees whole snapshots.
A spin lockA kernel mutexKeeps the short swap-and-copy path in user space.
Wake on an eventPolling on a timerNo 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.
Two threads, one shared buffer. Not to scale.

Next

Wingwise