Debugging a Flickering Issue Caused by Asynchronous Culling

After implementing frustum culling in the Untold Engine, performance improved, but right away I noticed flickering. It didn’t happen every frame, but it was noticeable whenever most of the models were in view.

So, I opened up Instruments to profile the issue. I noticed warnings that the engine was holding on to the drawable too long. I tried restructuring things to hold on to the drawable for as short a time as possible, but nothing helped.

According to Instruments, the engine was not CPU-bound or GPU-bound. There was no clear indication of the root cause of the flickering.


Digging Deeper

At that point, I decided to record a short video of the issue. I slowed it down and went frame by frame. What I saw wasn’t the usual kind of flickering—it was different.

  • Frame 1: a certain set of models was visible.
  • Frame 2: a completely different set was visible.
  • Frame 3: some disappeared, others suddenly appeared.

Models were popping in and out, almost as if something was out of sync.

This was a huge hint: it looked like a data race.


The Culprit

Looking at the code confirmed it.

In the frustum culling command buffer completion handler, I was updating the visibleEntityId array. This array held all the entities that passed the culling test.

The problem was that the GPU calls this completion handler asynchronously, while the CPU was already using that same array during the rendering passes (shadow and geometry).

 
 

In other words, the CPU was iterating over visibleEntityId at the same time the GPU might be modifying it.

Classic data race.


The Fix: Triple Buffering

The solution was to add a triple-buffered visible entity list.

During culling, the GPU writes results into buffer n+1.

 
 

During rendering, the CPU continues to read from buffer n.

 
 

When the frame finishes and the render command buffer’s completion handler triggers, I update the index so the CPU reads from the freshly written buffer n+1 on the next frame.

This guarantees that the CPU never reads data being modified by the GPU. The renderer always sees a stable snapshot of the visible entities.


The Result

With triple buffering in place, the flickering disappeared instantly. Models no longer popped in and out between frames.

This bug was a good reminder: sometimes what looks like a rendering artifact isn’t a math error at all, but a synchronization issue between CPU and GPU.


Lesson Learned

Whenever the GPU produces results asynchronously, the CPU should never iterate over those results directly. Always work with a snapshot. Triple buffering (or even double buffering) is a small architectural change that guarantees stability and avoids subtle bugs that can masquerade as rendering issues.

This experience reinforced for me how crucial synchronization and data ownership are when building GPU-driven systems—sometimes the hardest-looking bugs aren’t about shaders or math, but about who’s allowed to touch the data, and when.

Harold Serrano

Computer Graphics Enthusiast. Currently developing a 3D Game Engine.