This week I worked on adding Geometry Streaming to the engine and fixed a flickering issue that had been quietly annoying me for a while.
Both tasks ended up being more related than I initially expected.
BTW, here is version 0.10.0 of the engine with the Geometry Streaming Support.
Geometry Streaming Wasn’t the Hard Part — Integration Was
Getting Geometry Streaming working on its own wasn’t too bad. The goal was simple enough: render large scenes without having to load the entire scene into VRAM during initialization. Instead, meshes should be loaded and unloaded on demand, without stalling rendering.
The part that caused friction was not streaming itself, but getting it to behave correctly alongside two existing systems:
- the LOD system
- the static batching system
Each of these systems already worked well in isolation. The instability showed up once they had to coexist.
I initially overcomplicated the problem, mostly because I was treating these systems as if they were peers operating at the same level. They’re not.
The Assumption That Broke Everything
The thing that finally made it click was realizing that these systems don’t negotiate with each other — they react to upstream state.
Once I stopped thinking of them as equals and instead thought of them as layers in a pipeline, the engine immediately became more predictable.
A stable frame ended up looking like this:
- Geometry streaming updates asset residency
- LOD selection picks the best available representation
- Static batching groups the selected meshes
- The renderer submits batches to the GPU
Once I enforced this flow in the update loop, a surprising number of bugs simply disappeared.
The key insight here was that ordering matters more than clever logic.
These systems don’t need to know about each other — they just need to run in the right sequence and respond to state changes upstream.
The Kind of Bugs That Only Show Up Once Things Are “Mostly Working”
Getting the ordering right was half the battle. The other half was dealing with the kind of bugs that only appear once the architecture is almost correct.
For example:
- I wasn’t clearing the octree properly, which caused the engine to look for entities that no longer existed.
- One particularly frustrating bug refused to render a specific LOD whenever two or more entities were visible at the same time.
That second one took an entire day to track down.
It turned out the space uniform was getting overwritten during the unload/load phase of the streaming system. Nothing fancy — just a subtle overwrite happening at exactly the wrong time.
That kind of bug is annoying, but it’s also a signal that the system boundaries are finally being exercised in realistic ways.
The Flickering Issue That Didn’t Behave Like a Flicker
The flickering issue was a different kind of problem.
It only showed up in Edit mode, not reliably in Game mode. And it wasn’t the usual continuous flicker you expect when something is wrong. Instead, it would flicker once, stabilize, then flicker again a few seconds later — or sometimes not at all during a debug session.
That made it especially hard to reason about.
At first, I assumed it was a synchronization issue between render passes. I tried adding fences, forcing stricter ordering — none of that helped.
The clue ended up being that the flicker correlated with moments when nothing should have been changing visually.
The Real Cause: State Falling Out of Sync
Eventually, I traced the issue back to the culling system.
In some frames, the culling pass was returning zero visible entities — not because nothing was visible, but because the visibleEntityIds buffer was getting overwritten.
The fix wasn’t to add more synchronization, but to acknowledge reality: the culling system was already using triple buffering, and visibleEntityIds needed to follow the same pattern.
Once I made visibleEntityIds triple-buffered as well, the flickering disappeared completely.
The takeaway here wasn’t “use triple buffering,” but:
Any system that consumes frame-dependent data must respect the buffering strategy of the system producing it.
Final Thoughts
None of the issues this week were caused by exotic bugs or broken math. They all came from small assumptions about ordering, ownership, and state lifetime.
Once those assumptions were corrected, the engine became noticeably more stable — not just faster, but easier to reason about.
That’s usually a good sign that the architecture is moving in the right direction.
Thanks for reading.
