<?xml version="1.0" encoding="UTF-8"?>
<!--Generated by Site-Server v@build.version@ (http://www.squarespace.com) on Thu, 16 Apr 2026 05:30:33 GMT
--><rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://www.rssboard.org/media-rss" version="2.0"><channel><title>Blog - Harold Serrano | Game Engine Developer</title><link>https://www.haroldserrano.com/blog/</link><lastBuildDate>Sat, 14 Feb 2026 16:03:32 +0000</lastBuildDate><language>en-US</language><generator>Site-Server v@build.version@ (http://www.squarespace.com)</generator><description><![CDATA[<p>I'm developing a 3D Game Engine using Metal and C++, and I share all my knowledge of these topics on this blog.</p>]]></description><item><title>Lessons Learned: When the Drawable Leaks Into Your Render Pipeline</title><category>Game Engine Development</category><dc:creator>Harold Serrano</dc:creator><pubDate>Sat, 14 Feb 2026 22:35:17 +0000</pubDate><link>https://www.haroldserrano.com/blog/lessons-learned-when-the-drawable-leaks-into-your-render-pipeline</link><guid isPermaLink="false">54851541e4b0fb60932ad015:54851687e4b016eb8428ff4b:69909cd4150579291c4e2155</guid><description><![CDATA[<p>This week, while rendering scenes in Vision Pro using the Untold Engine, I realized that scenes were being rendered with the incorrect color space. Well, initially, I thought it was a color space issue — but something was telling me that this was more than just a color space problem. </p>












































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/ca3bf4cc-e2d9-4458-b248-ca3d0612dc0e/Screenshot+2026-02-14+at+3.21.24%E2%80%AFPM.png" data-image-dimensions="2744x1746" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/ca3bf4cc-e2d9-4458-b248-ca3d0612dc0e/Screenshot+2026-02-14+at+3.21.24%E2%80%AFPM.png?format=1000w" width="2744" height="1746" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/ca3bf4cc-e2d9-4458-b248-ca3d0612dc0e/Screenshot+2026-02-14+at+3.21.24%E2%80%AFPM.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/ca3bf4cc-e2d9-4458-b248-ca3d0612dc0e/Screenshot+2026-02-14+at+3.21.24%E2%80%AFPM.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/ca3bf4cc-e2d9-4458-b248-ca3d0612dc0e/Screenshot+2026-02-14+at+3.21.24%E2%80%AFPM.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/ca3bf4cc-e2d9-4458-b248-ca3d0612dc0e/Screenshot+2026-02-14+at+3.21.24%E2%80%AFPM.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/ca3bf4cc-e2d9-4458-b248-ca3d0612dc0e/Screenshot+2026-02-14+at+3.21.24%E2%80%AFPM.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/ca3bf4cc-e2d9-4458-b248-ca3d0612dc0e/Screenshot+2026-02-14+at+3.21.24%E2%80%AFPM.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/ca3bf4cc-e2d9-4458-b248-ca3d0612dc0e/Screenshot+2026-02-14+at+3.21.24%E2%80%AFPM.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<p>After analyzing my render graph and verifying the color targets I was using in the lighting pass and tone mapping pass, I realized that I had made a crucial mistake in the engine.</p>
<p>See, my lighting pass was doing all calculations in linear space, which is correct. However, the internal render targets were being created using the drawable's pixel format. Doing so meant that every platform could change the precision, dynamic range, and even encoding behavior of my internal buffers.</p>
<p>In other words, my lighting results were being stored in formats dictated by the drawable’s target format. That is wrong. The renderer should own its internal formats — not the presentation layer.</p>
<p>Because the drawable format differs per platform (for example, <code>.bgra8Unorm_srgb</code> on Vision Pro), my internal render targets were sometimes:</p>
<ul>
<li>8-bit</li>
<li>sRGB-encoded</li>
<li>Not HDR-capable</li>
</ul>
<p>Even though my lighting calculations were done in linear space, the storage format altered how those results were preserved and interpreted.</p>
<p>So yes — the math was linear, but the buffers holding the results were not consistent across platforms.</p>
<p>That is where the mismatch came from.</p>
<p>To fix this, I explicitly set the color target used in the lighting pass to <code>rgba16Float</code>. By doing this, I ensured:</p>
<ul>
<li>Stable precision</li>
<li>HDR-capable storage</li>
<li>Linear behavior</li>
<li>Platform-independent results</li>
</ul>
<p>Now, my lighting calculations are identical regardless of the platform, because the internal render targets are explicitly defined by the engine — not by the drawable.</p>
<hr>
<h2>The Second Issue: Tone Mapping Is Not Output Encoding</h2>
<p>The other issue was more subtle and made me realize that I still have a lot more to learn about tone mapping.</p>
<p>My pipeline originally followed this path:</p>
<ul>
<li>Lighting Pass</li>
<li>Post Processing</li>
<li>Tone Mapping</li>
<li>Write to Drawable</li>
</ul>
<p>The problem with this flow was that I assumed that after tone mapping, the image was ready for the screen.</p>
<p>But that is not true.</p>
<p>Different platforms expect different things:</p>
<ul>
<li>Different pixel formats (RGBA vs BGRA)</li>
<li>Different encoding (linear vs sRGB)</li>
<li>Different gamuts (sRGB vs Display-P3)</li>
<li>Different dynamic range behavior (SDR vs EDR)</li>
</ul>
<p>My pipeline above implicitly assumed that the tone-mapped result already matched whatever the drawable expected.</p>
<p>But tone mapping does <strong>not</strong> mean “ready for any screen.”</p>
<p>Tone mapping only compresses HDR → display-referred brightness range. It does <strong>not</strong>:</p>
<ul>
<li>Encode to sRGB automatically</li>
<li>Convert color gamut</li>
<li>Match the drawable’s storage format</li>
<li>Handle EDR behavior</li>
</ul>
<p>So when I wrote directly to the drawable after tone mapping, I was essentially letting the platform decide how the final color should be interpreted.</p>
<p>And since platforms differ, my final image differed.</p>
<hr>
<h2>What Was I Missing?</h2>
<p>I needed to separate responsibilities more clearly.</p>
<p>I needed a pass that owned the creative look — fully defined and controlled by the engine:</p>
<ul>
<li>Exposure</li>
<li>White balance</li>
<li>Contrast</li>
<li>Tone mapping curve</li>
</ul>
<p>This defines how the image should look artistically.</p>
<p>And I needed a separate pass that is platform-aware — an Output Transform pass — that defines how the display expects pixels to be formatted:</p>
<ul>
<li>Encode to sRGB or not</li>
<li>Convert to P3 or not</li>
<li>Clamp or preserve HDR</li>
<li>BGRA vs RGBA channel order</li>
<li>EDR behavior</li>
</ul>
<p>In my original pipeline, I had collapsed Look + Output Transform into one step. I wasn’t explicitly controlling the final encoding, so the platform’s defaults influenced the final image.</p>
<p>With the extra passes and modifications I made, <strong>the Look pass now defines the artistic look of the image. The Output Transform defines how that look is encoded for a specific display.</strong></p>
<p>Previously, I was conflating the two — which allowed the platform’s drawable format to influence the final result.</p>
<p>Here is the image after the fix.</p>












































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/44922889-2938-4586-89bc-8415d079a9c3/Screenshot+2026-02-14+at+3.19.19%E2%80%AFPM.png" data-image-dimensions="2744x1746" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/44922889-2938-4586-89bc-8415d079a9c3/Screenshot+2026-02-14+at+3.19.19%E2%80%AFPM.png?format=1000w" width="2744" height="1746" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/44922889-2938-4586-89bc-8415d079a9c3/Screenshot+2026-02-14+at+3.19.19%E2%80%AFPM.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/44922889-2938-4586-89bc-8415d079a9c3/Screenshot+2026-02-14+at+3.19.19%E2%80%AFPM.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/44922889-2938-4586-89bc-8415d079a9c3/Screenshot+2026-02-14+at+3.19.19%E2%80%AFPM.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/44922889-2938-4586-89bc-8415d079a9c3/Screenshot+2026-02-14+at+3.19.19%E2%80%AFPM.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/44922889-2938-4586-89bc-8415d079a9c3/Screenshot+2026-02-14+at+3.19.19%E2%80%AFPM.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/44922889-2938-4586-89bc-8415d079a9c3/Screenshot+2026-02-14+at+3.19.19%E2%80%AFPM.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/44922889-2938-4586-89bc-8415d079a9c3/Screenshot+2026-02-14+at+3.19.19%E2%80%AFPM.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
          
          <figcaption class="image-caption-wrapper">
            <p data-rte-preserve-empty="true">After fix image</p>
          </figcaption>
        
      
        </figure>
      

    
  


  


<p>Now, the renderer owns the working color space and internal formats, and the drawable only affects the final presentation step.</p><p>Thanks for reading.</p>]]></description><media:content type="image/png" url="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1771108088273-06X4EAY2JQX5JF934VPI/Screenshot+2026-02-14+at+3.19.19%E2%80%AFPM.png?format=1500w" medium="image" isDefault="true" width="1500" height="954"><media:title type="plain">Lessons Learned: When the Drawable Leaks Into Your Render Pipeline</media:title></media:content></item><item><title>Lessons Learned: Vision Pro, Large Scenes, and Threading</title><category>Game Engine Development</category><dc:creator>Harold Serrano</dc:creator><pubDate>Sat, 14 Feb 2026 22:35:11 +0000</pubDate><link>https://www.haroldserrano.com/blog/lessons-learned-vision-pro-large-scenes-and-threading</link><guid isPermaLink="false">54851541e4b0fb60932ad015:54851687e4b016eb8428ff4b:699081f8930a911051778a95</guid><description><![CDATA[<p>This week I came across an unexpected issue. Loading a large scene on the Vision Pro would result in a run-time error. But loading the same scene on macOS did not. Both macOS and visionOS use essentially the same loading and rendering path. So what could be causing the issue?</p>
<p>To make things worse, the run-time errors were cryptic and pointed to different functions each time the application crashed. Sometimes it looked like a type issue. Other times it looked like a Foundation error. Nothing clearly indicated what the real problem was.</p>
<p>However, during debugging, I started to see a pattern. Most of the time, the error pointed to scene or component data access. That’s when I began wondering:</p>
<blockquote>
<p>What if, during loading, the system is trying to access data that is not yet stable?</p>
</blockquote>
<p>In other words, what if one part of the engine is writing to components while another part is reading from them?</p>
<p>Then another thought came to mind. What if this issue is also present on macOS, but because macOS does not use a dedicated render thread in the same way as Vision Pro, the race condition is simply not exposed?</p>
<p>After a few more debugging sessions, I realized I may have been onto something.</p>
<hr>
<h2>The Real Issue</h2>
<p>On Vision Pro, rendering runs on a dedicated render thread. When we load a large scene, the Untold Engine performs loading work on a separate thread so that we don’t block execution.</p>
<p>That means we had two things happening at the same time:</p>
<ul>
<li>The <strong>render thread</strong> traversing the scene graph, iterating component storage, performing culling, and building draw calls.</li>
<li>The <strong>loading thread</strong> creating entities, attaching components, recursively tagging entities for static batching, rebuilding batch data, and updating spatial structures such as the octree.</li>
</ul>
<p>In other words, the render thread was reading from scene/component data while the loading thread was writing to that same data.</p>
<p>This read/write overlap caused race conditions and eventually corrupted state.</p>
<hr>
<h2>Why This Did Not Happen on macOS</h2>
<p>The reason this did not happen on macOS is mostly due to timing and threading differences.</p>
<p>On macOS:</p>
<ul>
<li>The renderer and update loop are more tightly coupled.</li>
<li>The mutation window during loading is smaller.</li>
<li>The render traversal is less likely to intersect with scene mutation at the exact wrong moment.</li>
</ul>
<p>On Vision Pro:</p>
<ul>
<li>Rendering runs independently on a dedicated thread.</li>
<li>Frame submission follows its own cadence.</li>
<li>The renderer can traverse the scene while it is still being mutated.</li>
</ul>
<p>Large scenes amplify this issue because static batching and recursive hierarchy processing take longer, increasing the window where the world is in a partially mutated state.</p>
<hr>
<h2>The Solution</h2>
<p>The solution was to add a gating mechanism to prevent any read/write collision while loading was taking place.</p>
<p>The idea is simple:</p>
<ul>
<li>When a major scene mutation phase begins (for example, during large scene loading or static batch generation), increment a shared counter.</li>
<li>When the mutation phase finishes, decrement it.</li>
<li>The render thread checks this counter before traversing the scene.</li>
<li>If a mutation is in progress, the render thread continues to submit frames but avoids traversing scene or component data that may still be unstable.</li>
</ul>
<p>It’s important to note that I do <strong>not</strong> block the render thread on visionOS. I let it continue running, but I prevent it from accessing critical scene data while the loading phase is still mutating that data.</p>
<p>After this fix was in place, loading large scenes with the Untold Engine on Vision Pro no longer caused run-time crashes.</p>
<hr>
<h2>Final Thoughts</h2>
<p>In the end, the issue was about concurrency. </p>
<ul>
<li>Rendering reads from the world.  </li>
<li>Loading mutates the world.</li>
</ul>
<p>Without proper synchronization, those two operations cannot safely overlap.</p>
<p>Vision Pro didn’t introduce a new bug into the Untold Engine. It exposed a hidden assumption in my threading model.</p>
<p>And that’s a good thing.
Thanks for reading.</p>]]></description><media:content type="image/png" url="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1771108487612-STE97QUXX9VPJXG3V3E8/Screenshot+2026-02-14+at+3.34.20%E2%80%AFPM.png?format=1500w" medium="image" isDefault="true" width="1500" height="954"><media:title type="plain">Lessons Learned: Vision Pro, Large Scenes, and Threading</media:title></media:content></item><item><title>Lessons Learned While Adding Geometry Streaming</title><category>Game Engine Development</category><dc:creator>Harold Serrano</dc:creator><pubDate>Tue, 10 Feb 2026 07:12:17 +0000</pubDate><link>https://www.haroldserrano.com/blog/untold-engine-updates-geometry-streaming</link><guid isPermaLink="false">54851541e4b0fb60932ad015:54851687e4b016eb8428ff4b:698a77d74db5c017fb3f3ab6</guid><description><![CDATA[<p>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.</p><p>Both tasks ended up being more related than I initially expected.</p><p>BTW, here is version 0.10.0 of the engine with the <a href="https://github.com/untoldengine/UntoldEngine/discussions/967">Geometry Streaming Support</a>.</p><hr>











































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/ef9312f5-9921-4846-9804-5f2bed1462b1/Screenshot+2026-02-10+at+12.09.55%E2%80%AFAM.png" data-image-dimensions="3104x1806" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/ef9312f5-9921-4846-9804-5f2bed1462b1/Screenshot+2026-02-10+at+12.09.55%E2%80%AFAM.png?format=1000w" width="3104" height="1806" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/ef9312f5-9921-4846-9804-5f2bed1462b1/Screenshot+2026-02-10+at+12.09.55%E2%80%AFAM.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/ef9312f5-9921-4846-9804-5f2bed1462b1/Screenshot+2026-02-10+at+12.09.55%E2%80%AFAM.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/ef9312f5-9921-4846-9804-5f2bed1462b1/Screenshot+2026-02-10+at+12.09.55%E2%80%AFAM.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/ef9312f5-9921-4846-9804-5f2bed1462b1/Screenshot+2026-02-10+at+12.09.55%E2%80%AFAM.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/ef9312f5-9921-4846-9804-5f2bed1462b1/Screenshot+2026-02-10+at+12.09.55%E2%80%AFAM.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/ef9312f5-9921-4846-9804-5f2bed1462b1/Screenshot+2026-02-10+at+12.09.55%E2%80%AFAM.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/ef9312f5-9921-4846-9804-5f2bed1462b1/Screenshot+2026-02-10+at+12.09.55%E2%80%AFAM.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<h2>Geometry Streaming Wasn’t the Hard Part — Integration Was</h2><p>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.</p><p>The part that caused friction was not streaming itself, but getting it to behave correctly alongside two existing systems:</p><ul>
<li>the LOD system  </li>
<li>the static batching system</li>
</ul><p>Each of these systems already worked well in isolation. The instability showed up once they had to coexist.</p><p>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.</p><hr><h2>The Assumption That Broke Everything</h2><p>The thing that finally made it click was realizing that these systems don’t negotiate with each other — they <strong>react to upstream state</strong>.</p><p>Once I stopped thinking of them as equals and instead thought of them as layers in a pipeline, the engine immediately became more predictable.</p><p>A stable frame ended up looking like this:</p><ul>
<li>Geometry streaming updates asset residency  </li>
<li>LOD selection picks the best available representation  </li>
<li>Static batching groups the selected meshes  </li>
<li>The renderer submits batches to the GPU</li>
</ul><p>Once I enforced this flow in the update loop, a surprising number of bugs simply disappeared.</p><p>The key insight here was that <strong>ordering matters more than clever logic</strong>.<br>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.</p><hr><h3>The Kind of Bugs That Only Show Up Once Things Are “Mostly Working”</h3><p>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.</p><p>For example:</p><ul>
<li>I wasn’t clearing the octree properly, which caused the engine to look for entities that no longer existed.</li>
<li>One particularly frustrating bug refused to render a specific LOD whenever two or more entities were visible at the same time.</li>
</ul><p>That second one took an entire day to track down.</p><p>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.</p><p>That kind of bug is annoying, but it’s also a signal that the system boundaries are finally being exercised in realistic ways.</p><hr><h2>The Flickering Issue That Didn’t Behave Like a Flicker</h2><p>The flickering issue was a different kind of problem.</p><p>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.</p><p>That made it especially hard to reason about.</p><p>At first, I assumed it was a synchronization issue between render passes. I tried adding fences, forcing stricter ordering — none of that helped.</p><p>The clue ended up being that the flicker correlated with moments when nothing should have been changing visually.</p><hr><h2>The Real Cause: State Falling Out of Sync</h2><p>Eventually, I traced the issue back to the culling system.</p><p>In some frames, the culling pass was returning <strong>zero visible entities</strong> — not because nothing was visible, but because the <code>visibleEntityIds</code> buffer was getting overwritten.</p><p>The fix wasn’t to add more synchronization, but to acknowledge reality: the culling system was already using triple buffering, and <code>visibleEntityIds</code> needed to follow the same pattern.</p><p>Once I made <code>visibleEntityIds</code> triple-buffered as well, the flickering disappeared completely.</p><p>The takeaway here wasn’t “use triple buffering,” but:</p><blockquote>
<p>Any system that consumes frame-dependent data must respect the buffering strategy of the system producing it.</p>
</blockquote><hr><h2>Final Thoughts</h2><p>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.</p><p>Once those assumptions were corrected, the engine became noticeably more stable — not just faster, but easier to reason about.</p><p>That’s usually a good sign that the architecture is moving in the right direction.</p><p>Thanks for reading.</p>]]></description><media:content type="image/png" url="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1770707514152-WMLV4MRGR2VZG7A5GZ6K/Screenshot+2026-02-10+at+12.09.55%E2%80%AFAM.png?format=1500w" medium="image" isDefault="true" width="1500" height="873"><media:title type="plain">Lessons Learned While Adding Geometry Streaming</media:title></media:content></item><item><title>Untold Engine Updates: LOD, Static Batching and More !!!</title><category>Game Engine Development</category><dc:creator>Harold Serrano</dc:creator><pubDate>Thu, 05 Feb 2026 02:27:40 +0000</pubDate><link>https://www.haroldserrano.com/blog/untold-engine-updates-lod-static-batching-and-more-</link><guid isPermaLink="false">54851541e4b0fb60932ad015:54851687e4b016eb8428ff4b:6983d6ddab993644a66de745</guid><description><![CDATA[<p>Hey guys,</p><p>It’s me again with a new update on the Untold Engine — this time focused on <strong>user experience</strong> and <strong>performance</strong>.</p><p>You might find this a bit odd coming from an engineer, but user experience matters <em>a lot</em> to me. Sometimes, I even see it as more important than performance itself. I know, that sounds backwards. But honestly, I don’t care how fast a tool is if the user experience is bad. If it’s frustrating to use, then to me, it’s not a good product.</p><p>So let’s start with the user-experience improvements I’ve been working on.</p><p>BTW, you can read more about version 0.9.0 of the engine <a href="https://github.com/untoldengine/UntoldEngine/discussions/962">here</a>.</p><h2>Quick USDZ Preview</h2><p>I was never happy with the fact that every time I wanted to render a model with the Untold Engine, I had to create a full project first. That felt unnecessary and slow.</p><p>So I added a <strong>Quick Preview</strong> feature.</p><p>You can now preview a <code>.usdz</code> file directly without creating or importing it into a project. Just click the <strong>Quick Preview</strong> button, select your <code>.usdz</code> file, and you’re good to go.</p>











































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8161bf14-d714-4a9f-ac01-2f017597d095/Screenshot+2026-02-04+at+7.15.53%E2%80%AFPM.png" data-image-dimensions="3104x1806" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8161bf14-d714-4a9f-ac01-2f017597d095/Screenshot+2026-02-04+at+7.15.53%E2%80%AFPM.png?format=1000w" width="3104" height="1806" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8161bf14-d714-4a9f-ac01-2f017597d095/Screenshot+2026-02-04+at+7.15.53%E2%80%AFPM.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8161bf14-d714-4a9f-ac01-2f017597d095/Screenshot+2026-02-04+at+7.15.53%E2%80%AFPM.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8161bf14-d714-4a9f-ac01-2f017597d095/Screenshot+2026-02-04+at+7.15.53%E2%80%AFPM.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8161bf14-d714-4a9f-ac01-2f017597d095/Screenshot+2026-02-04+at+7.15.53%E2%80%AFPM.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8161bf14-d714-4a9f-ac01-2f017597d095/Screenshot+2026-02-04+at+7.15.53%E2%80%AFPM.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8161bf14-d714-4a9f-ac01-2f017597d095/Screenshot+2026-02-04+at+7.15.53%E2%80%AFPM.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8161bf14-d714-4a9f-ac01-2f017597d095/Screenshot+2026-02-04+at+7.15.53%E2%80%AFPM.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<h2>Improved Importing Workflow</h2><p>Next up: importing.</p><p>The old importing workflow was confusing at times and a bit error-prone. It was too easy to accidentally import a model into the wrong category, which is never a good experience.</p><p>Now, when you click <strong>Import</strong>, you’re explicitly asked <em>what</em> you want to import. This makes the process clearer and significantly reduces the chances of loading assets into the wrong place.</p>











































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/4ac9c10f-bcb7-4c8b-80f0-d4aced84b5b8/Screenshot+2026-02-04+at+7.17.04%E2%80%AFPM.png" data-image-dimensions="3104x1806" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/4ac9c10f-bcb7-4c8b-80f0-d4aced84b5b8/Screenshot+2026-02-04+at+7.17.04%E2%80%AFPM.png?format=1000w" width="3104" height="1806" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/4ac9c10f-bcb7-4c8b-80f0-d4aced84b5b8/Screenshot+2026-02-04+at+7.17.04%E2%80%AFPM.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/4ac9c10f-bcb7-4c8b-80f0-d4aced84b5b8/Screenshot+2026-02-04+at+7.17.04%E2%80%AFPM.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/4ac9c10f-bcb7-4c8b-80f0-d4aced84b5b8/Screenshot+2026-02-04+at+7.17.04%E2%80%AFPM.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/4ac9c10f-bcb7-4c8b-80f0-d4aced84b5b8/Screenshot+2026-02-04+at+7.17.04%E2%80%AFPM.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/4ac9c10f-bcb7-4c8b-80f0-d4aced84b5b8/Screenshot+2026-02-04+at+7.17.04%E2%80%AFPM.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/4ac9c10f-bcb7-4c8b-80f0-d4aced84b5b8/Screenshot+2026-02-04+at+7.17.04%E2%80%AFPM.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/4ac9c10f-bcb7-4c8b-80f0-d4aced84b5b8/Screenshot+2026-02-04+at+7.17.04%E2%80%AFPM.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<h2>Scenegraph Parenting Support</h2><p>At some point, I realized I really wanted to create parent–child relationships between entities directly from the editor — but the Scenegraph didn’t support that at all.</p><p>So I added it.</p><p>You can now parent entities directly in the Scenegraph by dragging one entity onto another.<br>To unparent an entity, just right-click it in the Scenegraph and select <strong>Unparent</strong>.</p><p>That said, I think I can make the hierarchy more visually obvious. That might be the next thing I tackle so parent–child relationships are easier to spot at a glance.</p>











































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/e8129073-0c1c-4fae-bf12-8304f2fe4c7a/Screenshot+2026-02-04+at+7.20.06%E2%80%AFPM.png" data-image-dimensions="3104x1806" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/e8129073-0c1c-4fae-bf12-8304f2fe4c7a/Screenshot+2026-02-04+at+7.20.06%E2%80%AFPM.png?format=1000w" width="3104" height="1806" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/e8129073-0c1c-4fae-bf12-8304f2fe4c7a/Screenshot+2026-02-04+at+7.20.06%E2%80%AFPM.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/e8129073-0c1c-4fae-bf12-8304f2fe4c7a/Screenshot+2026-02-04+at+7.20.06%E2%80%AFPM.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/e8129073-0c1c-4fae-bf12-8304f2fe4c7a/Screenshot+2026-02-04+at+7.20.06%E2%80%AFPM.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/e8129073-0c1c-4fae-bf12-8304f2fe4c7a/Screenshot+2026-02-04+at+7.20.06%E2%80%AFPM.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/e8129073-0c1c-4fae-bf12-8304f2fe4c7a/Screenshot+2026-02-04+at+7.20.06%E2%80%AFPM.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/e8129073-0c1c-4fae-bf12-8304f2fe4c7a/Screenshot+2026-02-04+at+7.20.06%E2%80%AFPM.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/e8129073-0c1c-4fae-bf12-8304f2fe4c7a/Screenshot+2026-02-04+at+7.20.06%E2%80%AFPM.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<h2>Viewport Child Selection</h2><p>This one was a complete oversight on my end.</p><p>If an entity had multiple child meshes and you tried to select one of those meshes in the viewport using a right-click, the <em>parent</em> entity would get selected instead. That’s… not great.</p><p>This was a terrible user experience, so I made it a priority to fix.</p><p>You can now select child entities directly in the viewport using <strong>Shift + Right Mouse Click</strong>, which makes working with hierarchical scenes much more intuitive.</p><hr><p>Now, let’s talk about <strong>performance improvements</strong>.</p><h2>LOD System</h2><p>The Untold Engine now supports an <strong>LOD system</strong>.</p><p>You can assign an LOD Component to an entity, provide multiple versions of a mesh, and the engine will automatically select the appropriate LOD based on distance. This is especially useful when you want to maintain a steady 60 FPS without rendering fully detailed meshes when they’re not needed.</p>











































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/c78b4897-c878-4d91-81c9-34e8e02d2de2/Screenshot+2026-02-04+at+7.21.50%E2%80%AFPM.png" data-image-dimensions="3104x1806" data-image-focal-point="0.8026918879592579,0.6320790098762346" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/c78b4897-c878-4d91-81c9-34e8e02d2de2/Screenshot+2026-02-04+at+7.21.50%E2%80%AFPM.png?format=1000w" width="3104" height="1806" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/c78b4897-c878-4d91-81c9-34e8e02d2de2/Screenshot+2026-02-04+at+7.21.50%E2%80%AFPM.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/c78b4897-c878-4d91-81c9-34e8e02d2de2/Screenshot+2026-02-04+at+7.21.50%E2%80%AFPM.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/c78b4897-c878-4d91-81c9-34e8e02d2de2/Screenshot+2026-02-04+at+7.21.50%E2%80%AFPM.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/c78b4897-c878-4d91-81c9-34e8e02d2de2/Screenshot+2026-02-04+at+7.21.50%E2%80%AFPM.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/c78b4897-c878-4d91-81c9-34e8e02d2de2/Screenshot+2026-02-04+at+7.21.50%E2%80%AFPM.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/c78b4897-c878-4d91-81c9-34e8e02d2de2/Screenshot+2026-02-04+at+7.21.50%E2%80%AFPM.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/c78b4897-c878-4d91-81c9-34e8e02d2de2/Screenshot+2026-02-04+at+7.21.50%E2%80%AFPM.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<h2>Static Batching System</h2><p>The engine now also supports <strong>Static Batching</strong>.</p><p>This is extremely useful for scenes with a large number of static objects. By batching these meshes together, the engine can significantly reduce the number of draw calls it needs to execute.</p>











































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1dbe3b23-6524-4b83-86ee-88e12360f81f/Screenshot+2026-02-04+at+7.23.46%E2%80%AFPM.png" data-image-dimensions="3104x1806" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1dbe3b23-6524-4b83-86ee-88e12360f81f/Screenshot+2026-02-04+at+7.23.46%E2%80%AFPM.png?format=1000w" width="3104" height="1806" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1dbe3b23-6524-4b83-86ee-88e12360f81f/Screenshot+2026-02-04+at+7.23.46%E2%80%AFPM.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1dbe3b23-6524-4b83-86ee-88e12360f81f/Screenshot+2026-02-04+at+7.23.46%E2%80%AFPM.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1dbe3b23-6524-4b83-86ee-88e12360f81f/Screenshot+2026-02-04+at+7.23.46%E2%80%AFPM.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1dbe3b23-6524-4b83-86ee-88e12360f81f/Screenshot+2026-02-04+at+7.23.46%E2%80%AFPM.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1dbe3b23-6524-4b83-86ee-88e12360f81f/Screenshot+2026-02-04+at+7.23.46%E2%80%AFPM.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1dbe3b23-6524-4b83-86ee-88e12360f81f/Screenshot+2026-02-04+at+7.23.46%E2%80%AFPM.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1dbe3b23-6524-4b83-86ee-88e12360f81f/Screenshot+2026-02-04+at+7.23.46%E2%80%AFPM.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<p>In one test scene, draw calls dropped from <strong>over 2,000 to just 34</strong>. That’s a massive improvement and makes a huge difference in frame stability.</p><hr><p>That’s all for now.</p><p>If you want to follow the development of the engine and stay up to date with future updates, make sure to check out the project on GitHub.</p><p>Thanks for reading.</p>]]></description><media:content type="image/png" url="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1770258393488-PSKI03XSPH72Z3FCECIJ/Screenshot+2026-02-04+at+7.15.53%E2%80%AFPM.png?format=1500w" medium="image" isDefault="true" width="1500" height="873"><media:title type="plain">Untold Engine Updates: LOD, Static Batching and More !!!</media:title></media:content></item><item><title>Untold Engine Updates: Multi-Platform support and Camera Behaviors</title><category>Game Engine Development</category><dc:creator>Harold Serrano</dc:creator><pubDate>Tue, 20 Jan 2026 05:24:17 +0000</pubDate><link>https://www.haroldserrano.com/blog/untold-engine-updates-multi-platform-support-and-camera-behaviors</link><guid isPermaLink="false">54851541e4b0fb60932ad015:54851687e4b016eb8428ff4b:696f0aa1c3b1a5554cc7240f</guid><description><![CDATA[<p>Hi guys,</p>
<p>Me again with some updates on the status of the Untold Engine. As always, I’ve been working diligently on the engine. Over the past two weeks, I focused on implementing multi-platform support, fixing several async issues with scene loading, and starting work on a couple of camera behaviors that I needed for benchmarking and game testing. So let me walk you through what’s new <a href="https://github.com/untoldengine/UntoldEditor/releases">Untold Engine Studio</a></p>
<h2>Multi-Platform Xcode Project Support</h2>
<p>With the Untold Engine, you’re no longer limited to building a game for a single platform. You can now create an Xcode project with multi-platform support, either through the CLI or directly from the Untold Engine Studio.</p>












































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/cf909b94-ae56-4c53-9bc6-7fa5e48f8138/Multi-platform-project.png" data-image-dimensions="3104x1806" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/cf909b94-ae56-4c53-9bc6-7fa5e48f8138/Multi-platform-project.png?format=1000w" width="3104" height="1806" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/cf909b94-ae56-4c53-9bc6-7fa5e48f8138/Multi-platform-project.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/cf909b94-ae56-4c53-9bc6-7fa5e48f8138/Multi-platform-project.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/cf909b94-ae56-4c53-9bc6-7fa5e48f8138/Multi-platform-project.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/cf909b94-ae56-4c53-9bc6-7fa5e48f8138/Multi-platform-project.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/cf909b94-ae56-4c53-9bc6-7fa5e48f8138/Multi-platform-project.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/cf909b94-ae56-4c53-9bc6-7fa5e48f8138/Multi-platform-project.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/cf909b94-ae56-4c53-9bc6-7fa5e48f8138/Multi-platform-project.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<p>This makes game development a lot smoother: you code once, and your game runs on macOS, iOS, iOS + AR, and Vision Pro. The only platform still missing is tvOS, but that will be added soon.</p>
<h2>Fixed Async Issues</h2>
<p>There was an issue when loading large scenes in async mode that could cause a runtime crash. This was tricky to debug, but I eventually tracked it down, and the crash is no longer present.</p>
<p>If you’ve been following the progress of the engine, you’ll know that one of my core rules is that the engine should never crash. If you do encounter any crashes while loading a scene, please let me know by opening a GitHub issue.</p>
<h2>Camera Behaviors</h2>
<p>Finally, I started implementing the following camera behaviors:</p>
<ul>
<li>Camera path follow using waypoints</li>
<li>Camera follow with a dead zone</li>
</ul>
<p>It’s interesting that I still haven’t implemented first-person or third-person camera behaviors, yet I decided to work on these first. The reason is simple: I’ve been benchmarking the engine, and I needed a camera that could follow a predefined path using waypoints. The goal is to measure frame rate as the camera traverses a heavy scene. Since this behavior didn’t exist, I implemented it and made it part of the engine, as I do see it being useful for game development in general.</p>
<p>The second behavior is one you often see in soccer (fútbol) games. The camera only follows the entity if it moves beyond the boundaries of a box; otherwise, the camera remains stationary. I implemented this because I’m currently developing a soccer video game using the new Untold Engine and Editor.</p>
<p>There’s a clear purpose behind developing this game, and the effort has already paid dividends. I’ve run into several bugs and API confusions that I would not have discovered otherwise. Even though I’m not in the business of making games, I do see game development as a core part of building a game engine. I mean, how else would I really test my own tools?</p>
<p>That’s it for now. I’m going to get back to coding. Stay tuned for upcoming features.</p>
<p>Thanks for reading.
And don't forget to download the <a href="https://github.com/untoldengine/UntoldEditor/releases">Untold Engine Studio</a></p>]]></description><media:content type="image/png" url="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1768886615988-Y7JQ3STYJPMQQZDOND1D/Multi-platform-project.png?format=1500w" medium="image" isDefault="true" width="1500" height="873"><media:title type="plain">Untold Engine Updates: Multi-Platform support and Camera Behaviors</media:title></media:content></item><item><title>Untold Engine Updates: Faster Scene Loading, SSAO improvements, CLI</title><category>Game Engine Development</category><dc:creator>Harold Serrano</dc:creator><pubDate>Tue, 20 Jan 2026 04:36:22 +0000</pubDate><link>https://www.haroldserrano.com/blog/untold-engine-updates-multi-platform-support-async-loading-camera-behaviors</link><guid isPermaLink="false">54851541e4b0fb60932ad015:54851687e4b016eb8428ff4b:696f05517fd68e1f5f2159f6</guid><description><![CDATA[<p>Hey guys, I’ve been working hard on improving the engine lately—both performance-wise and editor (user-experience) wise.</p>











































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/dcc0f40c-95c1-4993-8960-b298f2fedc72/Screenshot+2026-01-19+at+9.42.32%E2%80%AFPM.png" data-image-dimensions="3104x1806" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/dcc0f40c-95c1-4993-8960-b298f2fedc72/Screenshot+2026-01-19+at+9.42.32%E2%80%AFPM.png?format=1000w" width="3104" height="1806" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/dcc0f40c-95c1-4993-8960-b298f2fedc72/Screenshot+2026-01-19+at+9.42.32%E2%80%AFPM.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/dcc0f40c-95c1-4993-8960-b298f2fedc72/Screenshot+2026-01-19+at+9.42.32%E2%80%AFPM.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/dcc0f40c-95c1-4993-8960-b298f2fedc72/Screenshot+2026-01-19+at+9.42.32%E2%80%AFPM.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/dcc0f40c-95c1-4993-8960-b298f2fedc72/Screenshot+2026-01-19+at+9.42.32%E2%80%AFPM.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/dcc0f40c-95c1-4993-8960-b298f2fedc72/Screenshot+2026-01-19+at+9.42.32%E2%80%AFPM.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/dcc0f40c-95c1-4993-8960-b298f2fedc72/Screenshot+2026-01-19+at+9.42.32%E2%80%AFPM.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/dcc0f40c-95c1-4993-8960-b298f2fedc72/Screenshot+2026-01-19+at+9.42.32%E2%80%AFPM.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<h2>Faster Scene Loading with Async Asset Loading</h2><p>One of the main issues I fixed in v0.7.0 was the long wait times when loading heavy scenes. I don’t have hard numbers, but it was definitely taking longer than what’s acceptable for a game engine. The main issue was that we didn’t have an async loading system in place. All models were being loaded synchronously, which caused long stalls.</p><p>With v0.7.0, scenes are now loaded through an async loading system, which makes the overall experience much better and more responsive. </p><p>Here is an example of how you can use the new async loading:</p><pre><code>// Create entity
let entityId = createEntity()

// Load mesh asynchronously (fire and forget)
setEntityMeshAsync( entityId: entityId, filename: "large_model", withExtension: "usdz")</code></pre><p>For more info, read the UsingAsyncLoading.md under the docs folder.</p><h2>SSAO Performance Improvements (Especially on Vision Pro)</h2><p>Another issue I worked on was improving SSAO performance, especially on mobile and Vision Pro. I added three quality modes for SSAO computation:</p><ul>
<li><strong>Fast</strong></li>
<li><strong>Balanced</strong></li>
<li><strong>High</strong></li>
</ul><p>Fast mode is the most performant but has the lowest quality, while High mode provides the best quality at the cost of performance. Fast mode did improve performance on Vision Pro, but unfortunately, not enough—the FPS is still not acceptable. Until I find a better solution, I recommend disabling SSAO entirely when using Vision Pro.</p><p>To use it in code, simply set the quality as shown below:</p><p><code>SSAOParams.shared.quality = .balanced //.fast or .high</code></p><h2>Command-Line Project Creation (Xcode Integration)</h2><p>The third feature I added, which I think will be really helpful, is the ability to create an Xcode game project with Untold Engine as a dependency directly from the command line. This is especially useful for users who want to bypass the editor and work directly in Xcode. That said, this doesn’t mean you can’t use the editor later—projects created this way can still be opened in Untold Engine Studio.</p><p>Here is an example on how to install and use the cli tool:</p><pre><code># clone the repo
git clone https://github.com/untoldengine/UntoldEngine.git
cd UntoldEngine

# Install the CLI globally:
./scripts/install-create.sh

# Verify installation:
untoldengine-create --version
untoldengine-create --help

# After installing the CLI, create a project from anywhere:

#  Create project directory
cd ~/anywhere
mkdir MyGame &amp;&amp; cd MyGame

#  Create the project
untoldengine-create create MyGame

#  Open in Xcode
open MyGame/MyGame.xcodeproj</code></pre><p>For more information, see: Tools/UntoldEngineCLI/README.md</p><h2>Editor Workflow Improvements</h2><p>I also spent time improving the user experience in the Untold Editor. The workflow is starting to take shape:</p><ol>
<li>User opens Untold Engine Studio  </li>
<li>Creates a new project or opens an existing one  </li>
<li>Populates the scene  </li>
<li>Writes game logic using Swift or scripting  </li>
<li>Builds &amp; plays  </li>
<li>Repeat steps 3–5</li>
</ol><p>This is still a work in progress, but I’m liking how everything is coming together.</p><h2>What’s Next</h2><p>For this week, I’m planning to focus on:</p><ol>
<li>Benchmarking + metrics harness</li>
<li>Improving performance on Vision Pro and iOS</li>
</ol><p>That’s the plan.<br>Feel free to checkout the the <a href="https://github.com/untoldengine/UntoldEditor/releases/tag/v0.7.0">Untold Engine Studio</a> v0.7.0.</p><p>Thanks for reading.</p>]]></description><media:content type="image/png" url="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1768884225954-J3DRGG77L6AA06O5WHJR/Screenshot+2026-01-19+at+9.42.32%E2%80%AFPM.png?format=1500w" medium="image" isDefault="true" width="1500" height="873"><media:title type="plain">Untold Engine Updates: Faster Scene Loading, SSAO improvements, CLI</media:title></media:content></item><item><title>Untold Engine is Growing Up</title><category>Game Engine Development</category><dc:creator>Harold Serrano</dc:creator><pubDate>Sat, 20 Dec 2025 14:30:19 +0000</pubDate><link>https://www.haroldserrano.com/blog/making-the-untold-engine-easier-to-use</link><guid isPermaLink="false">54851541e4b0fb60932ad015:54851687e4b016eb8428ff4b:6946a49174c79224ea8c2fde</guid><description><![CDATA[<p>I’ve been working on the Untold Engine for nearly 12 years.</p><p>I started in 2013, back when I didn’t even know what version control was. The early versions of the engine were crude, fragile, and limited—but seeing it improve, fix by fix, was deeply motivating.</p>











































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/265d2464-95d8-4b1f-ba7b-ef05a4bc7ca5/EditorMainShot.png" data-image-dimensions="3104x1806" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/265d2464-95d8-4b1f-ba7b-ef05a4bc7ca5/EditorMainShot.png?format=1000w" width="3104" height="1806" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/265d2464-95d8-4b1f-ba7b-ef05a4bc7ca5/EditorMainShot.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/265d2464-95d8-4b1f-ba7b-ef05a4bc7ca5/EditorMainShot.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/265d2464-95d8-4b1f-ba7b-ef05a4bc7ca5/EditorMainShot.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/265d2464-95d8-4b1f-ba7b-ef05a4bc7ca5/EditorMainShot.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/265d2464-95d8-4b1f-ba7b-ef05a4bc7ca5/EditorMainShot.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/265d2464-95d8-4b1f-ba7b-ef05a4bc7ca5/EditorMainShot.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/265d2464-95d8-4b1f-ba7b-ef05a4bc7ca5/EditorMainShot.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<p>There were many points along the way where I wanted to quit. Days where I was tired of touching the engine at all. At one point, I stopped entirely for about six months before coming back.</p>
<p>What I learned over time is that building a game engine isn’t technically hard in the way people expect. The real difficulty isn’t math, rendering, or architecture—it’s consistency. Showing up every day after the excitement wears off. Continuing when motivation is gone.</p>
<p>That’s the part most people underestimate.</p>
<p>I eventually realized something about myself: I’m not a good engineer because I write good code. I’m a good engineer because I don’t leave problems unfinished. I stay with them. I’ve done that since I was a kid—I just didn’t recognize it until much later.</p>
<p>That persistence is the reason Untold Engine still exists today.</p>
<p>After rebuilding the engine twice—from C++ to Swift—the project is finally reaching a point where it feels grown up. Today, I’m releasing <a href="https://github.com/untoldengine/UntoldEditor/releases">Untold Engine Studio</a>, the first bundled desktop release of the Untold Engine ecosystem.</p>
<p>This release exists for one reason: <strong>to remove friction</strong>.</p>
<p>With Untold Engine Studio, developers can:</p>
<ul>
<li>Download a single <strong>DMG</strong> and start immediately</li>
<li>Skip repository cloning and local build setup</li>
<li>Create and manage projects from a standalone app</li>
<li>Work visually with scenes, assets, and entities</li>
<li>Iterate quickly using Play Mode and Scripting language</li>
</ul>
<p>From the beginning, my focus has been user experience. I’m not trying to compete with Unreal on performance or Unity on market share. My goal is simpler—and harder: to build a tool that feels intuitive, stable, and dependable. Something that doesn’t fight you. Something that just works.</p>
<p>That’s not easy. It requires constant iteration and restraint. But this release is a meaningful step in that direction.</p>
<p>If you’re curious, I encourage you to download <a href="https://github.com/untoldengine/UntoldEditor/releases">Untold Engine Studio</a> and try it for yourself. Your feedback—good or bad—is genuinely valuable, and you can share it through the Untold Engine GitHub <a href="https://github.com/untoldengine/UntoldEngine/issues">issues</a>.</p>
<p>I’ve also invested a significant amount of time in <a href="https://untoldengine.github.io/UntoldEngine/docs/Getting%20Started/intro">documentation</a> to make getting started easier, and I hope it helps.</p>












































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/0d85535c-a75a-42b2-84c6-621bb690a32a/EditorSideShotWide.png" data-image-dimensions="3104x1806" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/0d85535c-a75a-42b2-84c6-621bb690a32a/EditorSideShotWide.png?format=1000w" width="3104" height="1806" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/0d85535c-a75a-42b2-84c6-621bb690a32a/EditorSideShotWide.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/0d85535c-a75a-42b2-84c6-621bb690a32a/EditorSideShotWide.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/0d85535c-a75a-42b2-84c6-621bb690a32a/EditorSideShotWide.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/0d85535c-a75a-42b2-84c6-621bb690a32a/EditorSideShotWide.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/0d85535c-a75a-42b2-84c6-621bb690a32a/EditorSideShotWide.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/0d85535c-a75a-42b2-84c6-621bb690a32a/EditorSideShotWide.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/0d85535c-a75a-42b2-84c6-621bb690a32a/EditorSideShotWide.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<p>As 2025 comes to a close, I’m proud of where Untold Engine is today. I have ambitious plans for 2026, and I’m excited to see how much further this project can go.</p><p>Thank you for reading.</p>]]></description><media:content type="image/png" url="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1766241009377-3IJN1087XN9ECJYPGFCK/EditorMainShot.png?format=1500w" medium="image" isDefault="true" width="1500" height="873"><media:title type="plain">Untold Engine is Growing Up</media:title></media:content></item><item><title>Untold Engine Update: Gaussian Splats, Scripting Support, and macOS Build System</title><category>Game Engine Development</category><dc:creator>Harold Serrano</dc:creator><pubDate>Sun, 30 Nov 2025 14:37:17 +0000</pubDate><link>https://www.haroldserrano.com/blog/major-untold-engine-update-gaussian-splats-scripting-support-and-macos-build-system</link><guid isPermaLink="false">54851541e4b0fb60932ad015:54851687e4b016eb8428ff4b:692c51d36d1b1f755f97fd44</guid><description><![CDATA[<p>It has been a while since my last update, but I’ve been quietly working behind the scenes on several major features. Today, I want to share three big milestones for the Untold Engine.</p><h2>Gaussian Splats</h2><p>The first major update is that the Untold Engine now supports <strong>Gaussian Splat Rendering</strong>. This is a feature I’ve wanted to implement since I first learned about Gaussian Splatting last year. Other priorities kept delaying it, but a few weeks ago I finally had enough time to focus on it—and I got it working.</p><p>Gaussian Splats now run inside the <strong>Untold Editor</strong>, on <strong>iOS</strong>, in <strong>AR</strong>, and on the <strong>Vision Pro</strong>, running directly on the device. This means both 3D models <em>and</em> splats render natively on visionOS hardware, not just the simulator.</p>











































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/17f95513-d462-4f98-a72c-4b2243ab7447/gaussianInEditor.png" data-image-dimensions="2032x1155" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/17f95513-d462-4f98-a72c-4b2243ab7447/gaussianInEditor.png?format=1000w" width="2032" height="1155" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/17f95513-d462-4f98-a72c-4b2243ab7447/gaussianInEditor.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/17f95513-d462-4f98-a72c-4b2243ab7447/gaussianInEditor.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/17f95513-d462-4f98-a72c-4b2243ab7447/gaussianInEditor.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/17f95513-d462-4f98-a72c-4b2243ab7447/gaussianInEditor.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/17f95513-d462-4f98-a72c-4b2243ab7447/gaussianInEditor.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/17f95513-d462-4f98-a72c-4b2243ab7447/gaussianInEditor.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/17f95513-d462-4f98-a72c-4b2243ab7447/gaussianInEditor.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<p>There are a few current limitations:</p><ul>
<li>I’m using <strong>Bitonic Sort</strong> instead of <strong>Radix Sort</strong> for depth sorting. Bitonic Sort works, but it is slower for large splat counts. Radix Sort is the long-term goal.</li>
<li><strong>Spherical Harmonics</strong> support is not implemented yet. I’m hoping to add this before the end of the year.</li>
</ul><p>Even with these limitations, this is a major step forward for the engine’s rendering capabilities.</p><h2>Scripting Support</h2><p>The engine now supports <strong>runtime scripting</strong> directly through the Untold Editor.</p><p>You can write game logic in Xcode, attach scripts to entities, and instantly see the results while the engine is running. All scripts appear in the Asset Browser, and you can add, link, or reload them with a click. This makes the development workflow smoother and faster.</p>











































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/63d2ad29-7cbe-44d9-bbec-030c6bee67c2/Screenshot+2025-11-24+at+8.13.03%E2%80%AFAM.png" data-image-dimensions="3104x1806" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/63d2ad29-7cbe-44d9-bbec-030c6bee67c2/Screenshot+2025-11-24+at+8.13.03%E2%80%AFAM.png?format=1000w" width="3104" height="1806" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/63d2ad29-7cbe-44d9-bbec-030c6bee67c2/Screenshot+2025-11-24+at+8.13.03%E2%80%AFAM.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/63d2ad29-7cbe-44d9-bbec-030c6bee67c2/Screenshot+2025-11-24+at+8.13.03%E2%80%AFAM.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/63d2ad29-7cbe-44d9-bbec-030c6bee67c2/Screenshot+2025-11-24+at+8.13.03%E2%80%AFAM.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/63d2ad29-7cbe-44d9-bbec-030c6bee67c2/Screenshot+2025-11-24+at+8.13.03%E2%80%AFAM.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/63d2ad29-7cbe-44d9-bbec-030c6bee67c2/Screenshot+2025-11-24+at+8.13.03%E2%80%AFAM.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/63d2ad29-7cbe-44d9-bbec-030c6bee67c2/Screenshot+2025-11-24+at+8.13.03%E2%80%AFAM.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/63d2ad29-7cbe-44d9-bbec-030c6bee67c2/Screenshot+2025-11-24+at+8.13.03%E2%80%AFAM.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<p>This feature was long overdue, but I approached it carefully because of past experience integrating scripting languages. Instead of Lua or Python, I built a lightweight <strong>DSL (Domain-Specific Language)</strong> specifically for the Untold Engine. I used ChatGPT heavily at the beginning to help with the initial structure, and once everything made sense, I took over and customized the system for the engine.</p>
<p>If you see anything that can be improved, please let me know—this is a brand-new system and will evolve with feedback.</p>
<p>Feel free to check out the <a href="https://untoldengine.github.io/UntoldEngine/docs/Getting%20Started/intro">Scripting Section</a> in the Docs to learn more.</p>
<h2>Build System</h2>
<p>Another major milestone is the new <strong>Build System</strong>.</p>
<p>After you set up your scene in the editor and attach scripts, the Untold Engine can now generate a <strong>macOS build</strong> of your game. The Build System packages your scenes, assets, and scripts and produces an app ready for the App Store. All you need to do is provide a project name and a destination path.</p>












































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/237da906-7b40-451c-88d6-ecae4f5e9105/Screenshot+2025-11-24+at+8.13.12%E2%80%AFAM.png" data-image-dimensions="3104x1806" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/237da906-7b40-451c-88d6-ecae4f5e9105/Screenshot+2025-11-24+at+8.13.12%E2%80%AFAM.png?format=1000w" width="3104" height="1806" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/237da906-7b40-451c-88d6-ecae4f5e9105/Screenshot+2025-11-24+at+8.13.12%E2%80%AFAM.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/237da906-7b40-451c-88d6-ecae4f5e9105/Screenshot+2025-11-24+at+8.13.12%E2%80%AFAM.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/237da906-7b40-451c-88d6-ecae4f5e9105/Screenshot+2025-11-24+at+8.13.12%E2%80%AFAM.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/237da906-7b40-451c-88d6-ecae4f5e9105/Screenshot+2025-11-24+at+8.13.12%E2%80%AFAM.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/237da906-7b40-451c-88d6-ecae4f5e9105/Screenshot+2025-11-24+at+8.13.12%E2%80%AFAM.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/237da906-7b40-451c-88d6-ecae4f5e9105/Screenshot+2025-11-24+at+8.13.12%E2%80%AFAM.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/237da906-7b40-451c-88d6-ecae4f5e9105/Screenshot+2025-11-24+at+8.13.12%E2%80%AFAM.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<p>At the moment, the Build System supports macOS only, but iOS and visionOS support is planned.</p><h2>What’s Next?</h2><p>I’m currently working on a packaged <strong>app bundle</strong> for the Untold Engine. The idea is to let developers download a <code>.dmg</code> file and start building games immediately—without cloning the repo or setting up dependencies. Developers who want full control can still clone and build from source.</p><p>This bundled version will be called <strong>Untold Engine Studio</strong>, and it will include everything required: the Untold Engine, the Untold Editor, and all dependencies. My goal is to make the development experience as smooth and accessible as possible.</p><p>More updates coming soon. Thanks for following the journey!</p>]]></description><media:content type="image/png" url="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1764513293391-LDE9V0MEGZJFX4ZT8IVE/gaussianInEditor.png?format=1500w" medium="image" isDefault="true" width="1500" height="853"><media:title type="plain">Untold Engine Update: Gaussian Splats, Scripting Support, and macOS Build System</media:title></media:content></item><item><title>Untold Engine Progress Update – New Editor and VisionOS Support!</title><category>Game Engine Development</category><dc:creator>Harold Serrano</dc:creator><pubDate>Sun, 26 Oct 2025 21:34:11 +0000</pubDate><link>https://www.haroldserrano.com/blog/-untold-engine-weekly-1-new-editor-website-visionos-support-amp-first-contribution</link><guid isPermaLink="false">54851541e4b0fb60932ad015:54851687e4b016eb8428ff4b:68fe80195bf3aa029af899f1</guid><description><![CDATA[<p>This past couple of months have been amazing for the Untold Engine — from getting its first contributor and sponsorship to adding VisionOS support. </p>
<p>Let me tell you all about it. </p>
<h2>Engine &amp; Editor</h2>
<p>You may recall that I had both the core and the editor integrated tightly in the engine. It worked nicely, but the coupling was going to give us headaches in the future. </p>
<p>Thanks to the effort of our first contributor <a href="https://github.com/miogds">miogds</a>, the core of the engine and the editor are now de-coupled.</p>
<p>So, this is the new architecture of the engine:</p>
<ul>
<li><strong>Core:</strong> Handles the runtime — rendering, physics, ECS, and all engine systems.</li>
<li><strong>Editor:</strong> A dedicated app for scene creation, entity manipulation, and asset management.</li>
</ul>

&nbsp;










































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/c1c11a73-47b3-4a50-955c-cb9992c29f00/Screenshot+2025-10-26+at+1.44.35%E2%80%AFPM.png" data-image-dimensions="1824x1488" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/c1c11a73-47b3-4a50-955c-cb9992c29f00/Screenshot+2025-10-26+at+1.44.35%E2%80%AFPM.png?format=1000w" width="1824" height="1488" sizes="(max-width: 640px) 100vw, (max-width: 767px) 66.66666666666666vw, 66.66666666666666vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/c1c11a73-47b3-4a50-955c-cb9992c29f00/Screenshot+2025-10-26+at+1.44.35%E2%80%AFPM.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/c1c11a73-47b3-4a50-955c-cb9992c29f00/Screenshot+2025-10-26+at+1.44.35%E2%80%AFPM.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/c1c11a73-47b3-4a50-955c-cb9992c29f00/Screenshot+2025-10-26+at+1.44.35%E2%80%AFPM.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/c1c11a73-47b3-4a50-955c-cb9992c29f00/Screenshot+2025-10-26+at+1.44.35%E2%80%AFPM.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/c1c11a73-47b3-4a50-955c-cb9992c29f00/Screenshot+2025-10-26+at+1.44.35%E2%80%AFPM.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/c1c11a73-47b3-4a50-955c-cb9992c29f00/Screenshot+2025-10-26+at+1.44.35%E2%80%AFPM.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/c1c11a73-47b3-4a50-955c-cb9992c29f00/Screenshot+2025-10-26+at+1.44.35%E2%80%AFPM.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
          
          <figcaption class="image-caption-wrapper">
            <p class="">Untold Engine - Core</p>
          </figcaption>
        
      
        </figure>
      

    
  


  


&nbsp;<p>This separation makes development cleaner, more modular, and sets the stage for headless or custom integration workflows.  </p>
<p>Additionally, the core engine will continue in its original repository <a href="https://github.com/untoldengine/UntoldEngine">UntoldEngine</a>, while the editor now lives in a new, dedicated repo <a href="https://github.com/untoldengine/UntoldEditor">UntoldEditor</a>. </p>

&nbsp;










































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/0d8c88f4-0999-4aca-9dd5-3e3e02db9eb9/Screenshot+2025-10-26+at+1.47.54%E2%80%AFPM.png" data-image-dimensions="3104x1810" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/0d8c88f4-0999-4aca-9dd5-3e3e02db9eb9/Screenshot+2025-10-26+at+1.47.54%E2%80%AFPM.png?format=1000w" width="3104" height="1810" sizes="(max-width: 640px) 100vw, (max-width: 767px) 66.66666666666666vw, 66.66666666666666vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/0d8c88f4-0999-4aca-9dd5-3e3e02db9eb9/Screenshot+2025-10-26+at+1.47.54%E2%80%AFPM.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/0d8c88f4-0999-4aca-9dd5-3e3e02db9eb9/Screenshot+2025-10-26+at+1.47.54%E2%80%AFPM.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/0d8c88f4-0999-4aca-9dd5-3e3e02db9eb9/Screenshot+2025-10-26+at+1.47.54%E2%80%AFPM.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/0d8c88f4-0999-4aca-9dd5-3e3e02db9eb9/Screenshot+2025-10-26+at+1.47.54%E2%80%AFPM.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/0d8c88f4-0999-4aca-9dd5-3e3e02db9eb9/Screenshot+2025-10-26+at+1.47.54%E2%80%AFPM.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/0d8c88f4-0999-4aca-9dd5-3e3e02db9eb9/Screenshot+2025-10-26+at+1.47.54%E2%80%AFPM.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/0d8c88f4-0999-4aca-9dd5-3e3e02db9eb9/Screenshot+2025-10-26+at+1.47.54%E2%80%AFPM.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
          
          <figcaption class="image-caption-wrapper">
            <p class="">Untold Engine Editor</p>
          </figcaption>
        
      
        </figure>
      

    
  


  


&nbsp;<h2>Unit Tests &amp; Workflows</h2><p>I've also been working on making the Untold Engine repository more professional.<br>This includes adding <strong>unit tests</strong>, <strong>GitHub Actions workflows</strong>, and <strong>automatic formatting and linting</strong>. </p><p>My hope is that these improvements will make contributing to the project much easier and more reliable. </p>
&nbsp;










































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/7cf545ff-a680-4852-be07-ae33766b4e05/Screenshot+2025-10-26+at+2.30.33%E2%80%AFPM.png" data-image-dimensions="2770x1056" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/7cf545ff-a680-4852-be07-ae33766b4e05/Screenshot+2025-10-26+at+2.30.33%E2%80%AFPM.png?format=1000w" width="2770" height="1056" sizes="(max-width: 640px) 100vw, (max-width: 767px) 66.66666666666666vw, 66.66666666666666vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/7cf545ff-a680-4852-be07-ae33766b4e05/Screenshot+2025-10-26+at+2.30.33%E2%80%AFPM.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/7cf545ff-a680-4852-be07-ae33766b4e05/Screenshot+2025-10-26+at+2.30.33%E2%80%AFPM.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/7cf545ff-a680-4852-be07-ae33766b4e05/Screenshot+2025-10-26+at+2.30.33%E2%80%AFPM.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/7cf545ff-a680-4852-be07-ae33766b4e05/Screenshot+2025-10-26+at+2.30.33%E2%80%AFPM.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/7cf545ff-a680-4852-be07-ae33766b4e05/Screenshot+2025-10-26+at+2.30.33%E2%80%AFPM.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/7cf545ff-a680-4852-be07-ae33766b4e05/Screenshot+2025-10-26+at+2.30.33%E2%80%AFPM.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/7cf545ff-a680-4852-be07-ae33766b4e05/Screenshot+2025-10-26+at+2.30.33%E2%80%AFPM.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


&nbsp;<h2>Website &amp; Documentation</h2><p>Another area of progress has been the new <strong>website and documentation</strong>.<br>The documentation site covers how to install the engine, explore the APIs, and contribute to development. </p><p>You can check it out here: <a href="https://www.untoldengine.com">Untold Engine</a></p><blockquote>
<p><em>Each engine release will include its own version of the docs for consistent developer onboarding.</em></p>
</blockquote>
&nbsp;










































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/846aa6e1-3821-4953-b5e3-ac5f977ca611/Screenshot+2025-10-26+at+1.55.37%E2%80%AFPM.png" data-image-dimensions="2872x1806" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/846aa6e1-3821-4953-b5e3-ac5f977ca611/Screenshot+2025-10-26+at+1.55.37%E2%80%AFPM.png?format=1000w" width="2872" height="1806" sizes="(max-width: 640px) 100vw, (max-width: 767px) 66.66666666666666vw, 66.66666666666666vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/846aa6e1-3821-4953-b5e3-ac5f977ca611/Screenshot+2025-10-26+at+1.55.37%E2%80%AFPM.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/846aa6e1-3821-4953-b5e3-ac5f977ca611/Screenshot+2025-10-26+at+1.55.37%E2%80%AFPM.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/846aa6e1-3821-4953-b5e3-ac5f977ca611/Screenshot+2025-10-26+at+1.55.37%E2%80%AFPM.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/846aa6e1-3821-4953-b5e3-ac5f977ca611/Screenshot+2025-10-26+at+1.55.37%E2%80%AFPM.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/846aa6e1-3821-4953-b5e3-ac5f977ca611/Screenshot+2025-10-26+at+1.55.37%E2%80%AFPM.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/846aa6e1-3821-4953-b5e3-ac5f977ca611/Screenshot+2025-10-26+at+1.55.37%E2%80%AFPM.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/846aa6e1-3821-4953-b5e3-ac5f977ca611/Screenshot+2025-10-26+at+1.55.37%E2%80%AFPM.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


&nbsp;<h2>VisionOS Support</h2><p>Lastly, the engine now <strong>compiles and runs on the VisionOS simulator</strong> — the first step toward supporting Apple’s Vision Pro platform.  </p><p>However, this is still early support — the engine has <strong>not yet been tested on an actual Vision Pro device</strong>.<br>We’ve already received an issue report related to Vision Pro hardware, so if you happen to have one and would like to help debug, you’re more than welcome to contribute!  </p>
&nbsp;










































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1a9409b1-aa03-4af8-81b5-c505b4b6ac7e/visionProSimulator.png" data-image-dimensions="2744x1746" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1a9409b1-aa03-4af8-81b5-c505b4b6ac7e/visionProSimulator.png?format=1000w" width="2744" height="1746" sizes="(max-width: 640px) 100vw, (max-width: 767px) 66.66666666666666vw, 66.66666666666666vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1a9409b1-aa03-4af8-81b5-c505b4b6ac7e/visionProSimulator.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1a9409b1-aa03-4af8-81b5-c505b4b6ac7e/visionProSimulator.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1a9409b1-aa03-4af8-81b5-c505b4b6ac7e/visionProSimulator.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1a9409b1-aa03-4af8-81b5-c505b4b6ac7e/visionProSimulator.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1a9409b1-aa03-4af8-81b5-c505b4b6ac7e/visionProSimulator.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1a9409b1-aa03-4af8-81b5-c505b4b6ac7e/visionProSimulator.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1a9409b1-aa03-4af8-81b5-c505b4b6ac7e/visionProSimulator.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


&nbsp;<p>Thanks for reading.</p>]]></description><media:content type="image/png" url="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1761514380909-5PE67CKUCAFKNZWWHA0N/Screenshot+2025-10-26+at+1.47.54%E2%80%AFPM.png?format=1500w" medium="image" isDefault="true" width="1500" height="875"><media:title type="plain">Untold Engine Progress Update – New Editor and VisionOS Support!</media:title></media:content></item><item><title>Debugging a Flickering Issue Caused by Asynchronous Culling</title><category>Game Engine Development</category><dc:creator>Harold Serrano</dc:creator><pubDate>Sun, 14 Sep 2025 15:35:50 +0000</pubDate><link>https://www.haroldserrano.com/blog/when-flickering-isnt-a-shader-bug-fixing-a-gpu-data-race-in-the-untold-engine</link><guid isPermaLink="false">54851541e4b0fb60932ad015:54851687e4b016eb8428ff4b:68c6d3cb8bcc9b3bb317187a</guid><description><![CDATA[<p>After implementing frustum culling in the <a href="https://github.com/untoldengine/UntoldEngine">Untold Engine</a>, performance improved, but right away I noticed <strong>flickering</strong>. It didn’t happen every frame, but it was noticeable whenever most of the models were in view. </p>
<p>So, I opened up Instruments to profile the issue. I noticed warnings that the engine was holding on to the <strong>drawable</strong> too long. I tried restructuring things to hold on to the drawable for as short a time as possible, but nothing helped. </p>
<p>According to Instruments, the engine was not CPU-bound or GPU-bound. There was no clear indication of the root cause of the flickering. </p>
<hr>
<h2>Digging Deeper</h2>
<p>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. </p>
<ul>
<li>Frame 1: a certain set of models was visible.</li>
<li>Frame 2: a completely different set was visible.</li>
<li>Frame 3: some disappeared, others suddenly appeared.</li>
</ul>
<p>Models were <strong>popping in and out</strong>, almost as if something was out of sync. </p>
<p>This was a huge hint: it looked like a <strong>data race</strong>. </p>












































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/e5d40c84-e781-4fb0-8110-b114a15ea95f/engine-flickering-slowed.gif" data-image-dimensions="1884x1080" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/e5d40c84-e781-4fb0-8110-b114a15ea95f/engine-flickering-slowed.gif?format=1000w" width="1884" height="1080" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/e5d40c84-e781-4fb0-8110-b114a15ea95f/engine-flickering-slowed.gif?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/e5d40c84-e781-4fb0-8110-b114a15ea95f/engine-flickering-slowed.gif?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/e5d40c84-e781-4fb0-8110-b114a15ea95f/engine-flickering-slowed.gif?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/e5d40c84-e781-4fb0-8110-b114a15ea95f/engine-flickering-slowed.gif?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/e5d40c84-e781-4fb0-8110-b114a15ea95f/engine-flickering-slowed.gif?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/e5d40c84-e781-4fb0-8110-b114a15ea95f/engine-flickering-slowed.gif?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/e5d40c84-e781-4fb0-8110-b114a15ea95f/engine-flickering-slowed.gif?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<hr><h2>The Culprit</h2><p>Looking at the code confirmed it.  </p><p>In the frustum culling <strong>command buffer completion handler</strong>, I was updating the <code>visibleEntityId</code> array. This array held all the entities that passed the culling test.  </p><p>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).  </p>
&nbsp;










































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/bfa0d3bc-8e69-4e7e-80bf-77425dd37fe5/cullingfrustumissue.png" data-image-dimensions="1678x472" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/bfa0d3bc-8e69-4e7e-80bf-77425dd37fe5/cullingfrustumissue.png?format=1000w" width="1678" height="472" sizes="(max-width: 640px) 100vw, (max-width: 767px) 66.66666666666666vw, 66.66666666666666vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/bfa0d3bc-8e69-4e7e-80bf-77425dd37fe5/cullingfrustumissue.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/bfa0d3bc-8e69-4e7e-80bf-77425dd37fe5/cullingfrustumissue.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/bfa0d3bc-8e69-4e7e-80bf-77425dd37fe5/cullingfrustumissue.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/bfa0d3bc-8e69-4e7e-80bf-77425dd37fe5/cullingfrustumissue.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/bfa0d3bc-8e69-4e7e-80bf-77425dd37fe5/cullingfrustumissue.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/bfa0d3bc-8e69-4e7e-80bf-77425dd37fe5/cullingfrustumissue.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/bfa0d3bc-8e69-4e7e-80bf-77425dd37fe5/cullingfrustumissue.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


&nbsp;<p>In other words, the CPU was iterating over <code>visibleEntityId</code> at the same time the GPU might be modifying it. </p><p>Classic data race. </p><hr><h2>The Fix: Triple Buffering</h2><p>The solution was to add a <strong>triple-buffered visible entity list</strong>. </p><p>During culling, the GPU writes results into buffer <em>n+1</em>.</p>
&nbsp;










































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/a69178a4-406c-4c94-bc25-8cfa0e50cf3b/cullingfix.png" data-image-dimensions="1542x472" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/a69178a4-406c-4c94-bc25-8cfa0e50cf3b/cullingfix.png?format=1000w" width="1542" height="472" sizes="(max-width: 640px) 100vw, (max-width: 767px) 66.66666666666666vw, 66.66666666666666vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/a69178a4-406c-4c94-bc25-8cfa0e50cf3b/cullingfix.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/a69178a4-406c-4c94-bc25-8cfa0e50cf3b/cullingfix.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/a69178a4-406c-4c94-bc25-8cfa0e50cf3b/cullingfix.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/a69178a4-406c-4c94-bc25-8cfa0e50cf3b/cullingfix.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/a69178a4-406c-4c94-bc25-8cfa0e50cf3b/cullingfix.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/a69178a4-406c-4c94-bc25-8cfa0e50cf3b/cullingfix.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/a69178a4-406c-4c94-bc25-8cfa0e50cf3b/cullingfix.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


&nbsp;<p>During rendering, the CPU continues to read from buffer <em>n</em>.</p>
&nbsp;










































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/3c0debb2-33d1-4328-b8c0-4e4d5f8974ee/cpucommandbuffer-2.png" data-image-dimensions="1850x508" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/3c0debb2-33d1-4328-b8c0-4e4d5f8974ee/cpucommandbuffer-2.png?format=1000w" width="1850" height="508" sizes="(max-width: 640px) 100vw, (max-width: 767px) 66.66666666666666vw, 66.66666666666666vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/3c0debb2-33d1-4328-b8c0-4e4d5f8974ee/cpucommandbuffer-2.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/3c0debb2-33d1-4328-b8c0-4e4d5f8974ee/cpucommandbuffer-2.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/3c0debb2-33d1-4328-b8c0-4e4d5f8974ee/cpucommandbuffer-2.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/3c0debb2-33d1-4328-b8c0-4e4d5f8974ee/cpucommandbuffer-2.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/3c0debb2-33d1-4328-b8c0-4e4d5f8974ee/cpucommandbuffer-2.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/3c0debb2-33d1-4328-b8c0-4e4d5f8974ee/cpucommandbuffer-2.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/3c0debb2-33d1-4328-b8c0-4e4d5f8974ee/cpucommandbuffer-2.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


&nbsp;<p>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 <em>n+1</em> on the next frame.</p><p>This guarantees that the CPU never reads data being modified by the GPU. The renderer always sees a <strong>stable snapshot</strong> of the visible entities.  </p><hr><h2>The Result</h2><p>With triple buffering in place, the flickering disappeared instantly. Models no longer popped in and out between frames.  </p>











































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/fe5755ac-9e92-4573-b096-2b5d2eb5001b/cullingflickerfix.gif" data-image-dimensions="1164x720" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/fe5755ac-9e92-4573-b096-2b5d2eb5001b/cullingflickerfix.gif?format=1000w" width="1164" height="720" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/fe5755ac-9e92-4573-b096-2b5d2eb5001b/cullingflickerfix.gif?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/fe5755ac-9e92-4573-b096-2b5d2eb5001b/cullingflickerfix.gif?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/fe5755ac-9e92-4573-b096-2b5d2eb5001b/cullingflickerfix.gif?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/fe5755ac-9e92-4573-b096-2b5d2eb5001b/cullingflickerfix.gif?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/fe5755ac-9e92-4573-b096-2b5d2eb5001b/cullingflickerfix.gif?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/fe5755ac-9e92-4573-b096-2b5d2eb5001b/cullingflickerfix.gif?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/fe5755ac-9e92-4573-b096-2b5d2eb5001b/cullingflickerfix.gif?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<p>This bug was a good reminder: sometimes what looks like a rendering artifact isn’t a math error at all, but a <strong>synchronization issue between CPU and GPU</strong>.  </p><hr><h2>Lesson Learned</h2><p>Whenever the GPU produces results asynchronously, the CPU should never iterate over those results directly. Always work with a <strong>snapshot</strong>. Triple buffering (or even double buffering) is a small architectural change that guarantees stability and avoids subtle bugs that can masquerade as rendering issues.  </p><p>This experience reinforced for me how crucial <strong>synchronization and data ownership</strong> 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.  </p>]]></description><media:content type="image/png" url="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1757864009331-0TWN2Q6CIEJFHDRIJK93/async+cull.png?format=1500w" medium="image" isDefault="true" width="1280" height="720"><media:title type="plain">Debugging a Flickering Issue Caused by Asynchronous Culling</media:title></media:content></item><item><title>Deferred Entity Destruction in ECS: A Mark-and-Sweep Approach</title><category>Game Engine Development</category><dc:creator>Harold Serrano</dc:creator><pubDate>Thu, 04 Sep 2025 13:04:39 +0000</pubDate><link>https://www.haroldserrano.com/blog/how-a-swift-formatter-exposed-a-hidden-bug-in-my-engine</link><guid isPermaLink="false">54851541e4b0fb60932ad015:54851687e4b016eb8428ff4b:68b8a6220ef18e21be51b84d</guid><description><![CDATA[<p>I found a bug in the <a href="https://github.com/untoldengine/UntoldEngine">Untold Engine</a> in the weirdest way possible. After merging several branches into my develop branch, I decided to run a Swift formatter on the engine. Three files were changed. I ran the unit tests, they all passed, and then I figured I’d do a final performance check before pushing the branch to my repo.</p>
<p>So, I launched the engine, loaded a scene, and then deleted the scene. </p>












































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/53d45721-5b6e-43bf-bd78-4a1b4be2ba09/entitynotfound+-+Frame+524.png" data-image-dimensions="3870x2160" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/53d45721-5b6e-43bf-bd78-4a1b4be2ba09/entitynotfound+-+Frame+524.png?format=1000w" width="3870" height="2160" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/53d45721-5b6e-43bf-bd78-4a1b4be2ba09/entitynotfound+-+Frame+524.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/53d45721-5b6e-43bf-bd78-4a1b4be2ba09/entitynotfound+-+Frame+524.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/53d45721-5b6e-43bf-bd78-4a1b4be2ba09/entitynotfound+-+Frame+524.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/53d45721-5b6e-43bf-bd78-4a1b4be2ba09/entitynotfound+-+Frame+524.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/53d45721-5b6e-43bf-bd78-4a1b4be2ba09/entitynotfound+-+Frame+524.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/53d45721-5b6e-43bf-bd78-4a1b4be2ba09/entitynotfound+-+Frame+524.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/53d45721-5b6e-43bf-bd78-4a1b4be2ba09/entitynotfound+-+Frame+524.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<p>The moment I did that, the console log started flooding with messages like:</p>
<ul>
<li>Entity is missing or does not exist.</li>
<li>Does not have a Render Component.</li>
</ul>












































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/f760707f-79fc-4289-8064-20f020b44ced/entitynotfound+-+Frame+984.png" data-image-dimensions="3870x2160" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/f760707f-79fc-4289-8064-20f020b44ced/entitynotfound+-+Frame+984.png?format=1000w" width="3870" height="2160" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/f760707f-79fc-4289-8064-20f020b44ced/entitynotfound+-+Frame+984.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/f760707f-79fc-4289-8064-20f020b44ced/entitynotfound+-+Frame+984.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/f760707f-79fc-4289-8064-20f020b44ced/entitynotfound+-+Frame+984.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/f760707f-79fc-4289-8064-20f020b44ced/entitynotfound+-+Frame+984.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/f760707f-79fc-4289-8064-20f020b44ced/entitynotfound+-+Frame+984.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/f760707f-79fc-4289-8064-20f020b44ced/entitynotfound+-+Frame+984.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/f760707f-79fc-4289-8064-20f020b44ced/entitynotfound+-+Frame+984.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<p>This was the first time I had ever seen the engine behave like this when removing all entities from a scene. My first reaction was: <em>the formatter broke something.</em></p><p>But the formatter’s changes were only cosmetic. There was no reason for this kind of bug. </p>











































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8cbf40e0-cb9a-4e3a-b8ff-6833777be2f9/Screenshot+2025-09-03+at+5.53.04%E2%80%AFAM.png" data-image-dimensions="2984x1828" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8cbf40e0-cb9a-4e3a-b8ff-6833777be2f9/Screenshot+2025-09-03+at+5.53.04%E2%80%AFAM.png?format=1000w" width="2984" height="1828" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8cbf40e0-cb9a-4e3a-b8ff-6833777be2f9/Screenshot+2025-09-03+at+5.53.04%E2%80%AFAM.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8cbf40e0-cb9a-4e3a-b8ff-6833777be2f9/Screenshot+2025-09-03+at+5.53.04%E2%80%AFAM.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8cbf40e0-cb9a-4e3a-b8ff-6833777be2f9/Screenshot+2025-09-03+at+5.53.04%E2%80%AFAM.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8cbf40e0-cb9a-4e3a-b8ff-6833777be2f9/Screenshot+2025-09-03+at+5.53.04%E2%80%AFAM.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8cbf40e0-cb9a-4e3a-b8ff-6833777be2f9/Screenshot+2025-09-03+at+5.53.04%E2%80%AFAM.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8cbf40e0-cb9a-4e3a-b8ff-6833777be2f9/Screenshot+2025-09-03+at+5.53.04%E2%80%AFAM.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8cbf40e0-cb9a-4e3a-b8ff-6833777be2f9/Screenshot+2025-09-03+at+5.53.04%E2%80%AFAM.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<p>At that point I was lost. So, I asked ChatGPT for some guidance, and it mentioned something interesting: maybe the formatter’s modifications had affected timing. That hint got me thinking.</p>
<p>After tinkering a bit, I realized the truth: <em>this bug was always there. The formatter just exposed it earlier.</em></p>
<h3>The Real Problem</h3>
<p>My engine’s editor runs asynchronously from the engine’s core functions. When I clicked the button to remove all entities, the editor tried to clear the scene immediately — even if those entities were still being processed by a kernel or the render graph.</p>
<p>In other words, the engine was destroying entities while they were still in use. That’s why systems started complaining about missing entities and missing components.</p>
<h3>The Solution: A Mini Garbage Collector</h3>
<p>What I needed was a safe way to destroy entities. The fix was to implement a simple “garbage collector” for my ECS, with two phases:</p>
<ul>
<li>Mark Phase – Instead of destroying entities right away, I mark them as pendingDestroy.</li>
<li>Sweep Phase – Once I know the command buffer has completed, I set a flag. In the next update() call, that flag triggers the sweep, where I finally destroy all entities that were marked.</li>
</ul>
<p>This way, entity destruction only happens at a safe point in the loop, when nothing else is iterating over them.</p>
<h3>Conclusion</h3>
<p>What looked like a weird formatter bug turned out to be a timing bug in my engine. Immediate destruction was unsafe — the real fix was to defer destruction until the right time.</p>
<p>By adding a simple mark-and-sweep system, I now have a mini garbage collector for entities. It keeps the engine stable, avoids “entity does not exist” spam, and gives me confidence that clearing a scene won’t blow everything up mid-frame.</p>
<p>Thanks for reading.</p>]]></description><media:content type="image/png" url="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1756991056830-I84TJAGBJ8C64LMAQ9T9/mark+and+sweep.png?format=1500w" medium="image" isDefault="true" width="1280" height="720"><media:title type="plain">Deferred Entity Destruction in ECS: A Mark-and-Sweep Approach</media:title></media:content></item><item><title>From 26.7 ms to 16.7 ms: How a simple Optimization Boosted Performance</title><category>Game Engine Development</category><dc:creator>Harold Serrano</dc:creator><pubDate>Thu, 28 Aug 2025 06:44:55 +0000</pubDate><link>https://www.haroldserrano.com/blog/how-a-one-liner-killed-my-engines-performance</link><guid isPermaLink="false">54851541e4b0fb60932ad015:54851687e4b016eb8428ff4b:68afdec721b59a4cdb20ec93</guid><description><![CDATA[<p>In my <a href="https://www.haroldserrano.com/blog/hunting-bottlenecks-in-my-engine-from-29fps-to-37fps">previous article</a>, I talked about my attempts to improve the performance of the <a href="https://github.com/untoldengine/UntoldEngine">Untold Engine</a>. Even after adding GPU frustum culling to reduce the CPU workload, the engine was still CPU-bound — stuck at around 26.7 ms per frame.</p>

<p>Profiling with Xcode Instruments pointed the finger at Metal’s encoder preparation, which appeared to take ~15 ms. Based on that, my next move seemed obvious: switch to a bindless rendering.</p><p>What does that mean? Instead of rebinding textures and material properties for every draw call, I would move everything into a single argument buffer. Each draw would reference materials by index. In theory, this should drastically cut CPU overhead and pair nicely with GPU-driven culling.</p><p>But reality didn’t match theory. After spending days moving to a bindless model, I ran the engine with 500 models — and the performance needle didn’t budge. In fact, things got worse: encoder prep time increased from ~15 ms to ~17 ms.</p>











































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/b5b3f2bc-f60c-4bcb-958c-98225e2fbafa/frustumCulling-metalencoder.png" data-image-dimensions="3104x1806" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/b5b3f2bc-f60c-4bcb-958c-98225e2fbafa/frustumCulling-metalencoder.png?format=1000w" width="3104" height="1806" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/b5b3f2bc-f60c-4bcb-958c-98225e2fbafa/frustumCulling-metalencoder.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/b5b3f2bc-f60c-4bcb-958c-98225e2fbafa/frustumCulling-metalencoder.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/b5b3f2bc-f60c-4bcb-958c-98225e2fbafa/frustumCulling-metalencoder.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/b5b3f2bc-f60c-4bcb-958c-98225e2fbafa/frustumCulling-metalencoder.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/b5b3f2bc-f60c-4bcb-958c-98225e2fbafa/frustumCulling-metalencoder.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/b5b3f2bc-f60c-4bcb-958c-98225e2fbafa/frustumCulling-metalencoder.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/b5b3f2bc-f60c-4bcb-958c-98225e2fbafa/frustumCulling-metalencoder.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<p>You can imagine my disappointment. But I kept digging. And then I found the real bottleneck. Instruments showed the CPU was spending almost 9.5 ms just preparing data for GPU frustum culling.</p>












































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/c673ae9e-77b7-41d7-8b35-bf5f46f57869/FrustumCulling-Time-Loupe.png" data-image-dimensions="3104x1806" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/c673ae9e-77b7-41d7-8b35-bf5f46f57869/FrustumCulling-Time-Loupe.png?format=1000w" width="3104" height="1806" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/c673ae9e-77b7-41d7-8b35-bf5f46f57869/FrustumCulling-Time-Loupe.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/c673ae9e-77b7-41d7-8b35-bf5f46f57869/FrustumCulling-Time-Loupe.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/c673ae9e-77b7-41d7-8b35-bf5f46f57869/FrustumCulling-Time-Loupe.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/c673ae9e-77b7-41d7-8b35-bf5f46f57869/FrustumCulling-Time-Loupe.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/c673ae9e-77b7-41d7-8b35-bf5f46f57869/FrustumCulling-Time-Loupe.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/c673ae9e-77b7-41d7-8b35-bf5f46f57869/FrustumCulling-Time-Loupe.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/c673ae9e-77b7-41d7-8b35-bf5f46f57869/FrustumCulling-Time-Loupe.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<p>So the encoder wasn’t the problem after all. As I dug into the code, I discovered the true culprit: a single function that queries all entities with specific component IDs.</p>
&nbsp;










































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/d92d11a9-c243-491f-9b22-cc76a3151afb/performancebottleneck.png" data-image-dimensions="1054x256" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/d92d11a9-c243-491f-9b22-cc76a3151afb/performancebottleneck.png?format=1000w" width="1054" height="256" sizes="(max-width: 640px) 100vw, (max-width: 767px) 66.66666666666666vw, 66.66666666666666vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/d92d11a9-c243-491f-9b22-cc76a3151afb/performancebottleneck.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/d92d11a9-c243-491f-9b22-cc76a3151afb/performancebottleneck.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/d92d11a9-c243-491f-9b22-cc76a3151afb/performancebottleneck.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/d92d11a9-c243-491f-9b22-cc76a3151afb/performancebottleneck.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/d92d11a9-c243-491f-9b22-cc76a3151afb/performancebottleneck.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/d92d11a9-c243-491f-9b22-cc76a3151afb/performancebottleneck.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/d92d11a9-c243-491f-9b22-cc76a3151afb/performancebottleneck.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


&nbsp;<p>Here’s what was happening:</p><p>👉 My component mask was stored as an array of 64 booleans. Every time I checked an entity, the code looped through all 64 slots, read from two arrays, and branched on each one. With 500 entities, that meant tens of thousands of tiny checks every single frame. No wonder the CPU was choking.</p><p>The fix? Replace the boolean array with a single 64-bit integer and use a bitwise AND. That collapses the entire check into just two instructions.
Here’s the new function:</p>
&nbsp;










































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/275c74be-0347-4616-84f8-89911d55a1b5/performancefix.png" data-image-dimensions="868x256" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/275c74be-0347-4616-84f8-89911d55a1b5/performancefix.png?format=1000w" width="868" height="256" sizes="(max-width: 640px) 100vw, (max-width: 767px) 66.66666666666666vw, 66.66666666666666vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/275c74be-0347-4616-84f8-89911d55a1b5/performancefix.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/275c74be-0347-4616-84f8-89911d55a1b5/performancefix.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/275c74be-0347-4616-84f8-89911d55a1b5/performancefix.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/275c74be-0347-4616-84f8-89911d55a1b5/performancefix.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/275c74be-0347-4616-84f8-89911d55a1b5/performancefix.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/275c74be-0347-4616-84f8-89911d55a1b5/performancefix.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/275c74be-0347-4616-84f8-89911d55a1b5/performancefix.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


&nbsp;<p>That one change dropped the CPU frame time from 26.7 ms down to 16.7 ms. The GPU frame time sits at 9.3 ms. </p>











































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/cde794c6-86d6-43a7-ab9e-eafc9cb8c8b2/performanceFix_frametime.png" data-image-dimensions="3104x1828" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/cde794c6-86d6-43a7-ab9e-eafc9cb8c8b2/performanceFix_frametime.png?format=1000w" width="3104" height="1828" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/cde794c6-86d6-43a7-ab9e-eafc9cb8c8b2/performanceFix_frametime.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/cde794c6-86d6-43a7-ab9e-eafc9cb8c8b2/performanceFix_frametime.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/cde794c6-86d6-43a7-ab9e-eafc9cb8c8b2/performanceFix_frametime.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/cde794c6-86d6-43a7-ab9e-eafc9cb8c8b2/performanceFix_frametime.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/cde794c6-86d6-43a7-ab9e-eafc9cb8c8b2/performanceFix_frametime.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/cde794c6-86d6-43a7-ab9e-eafc9cb8c8b2/performanceFix_frametime.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/cde794c6-86d6-43a7-ab9e-eafc9cb8c8b2/performanceFix_frametime.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<p>In other words, the engine now runs at a solid 60 fps.</p>











































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/2fcc2333-7a63-4712-bf48-8dc8dcd486ac/perforamance_fps.png" data-image-dimensions="3104x1828" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/2fcc2333-7a63-4712-bf48-8dc8dcd486ac/perforamance_fps.png?format=1000w" width="3104" height="1828" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/2fcc2333-7a63-4712-bf48-8dc8dcd486ac/perforamance_fps.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/2fcc2333-7a63-4712-bf48-8dc8dcd486ac/perforamance_fps.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/2fcc2333-7a63-4712-bf48-8dc8dcd486ac/perforamance_fps.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/2fcc2333-7a63-4712-bf48-8dc8dcd486ac/perforamance_fps.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/2fcc2333-7a63-4712-bf48-8dc8dcd486ac/perforamance_fps.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/2fcc2333-7a63-4712-bf48-8dc8dcd486ac/perforamance_fps.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/2fcc2333-7a63-4712-bf48-8dc8dcd486ac/perforamance_fps.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<p>I’m happy with the results: the engine is no longer CPU-bound or GPU-bound.</p><p>But I’m not done yet. The next step is implementing occlusion culling — and I’m excited to see how far I can push performance.</p><p>Thanks for reading.</p>]]></description><media:content type="image/png" url="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1756363282135-60LGLHAAFCSGO174FPBI/Copy+of+frustumculling.png?format=1500w" medium="image" isDefault="true" width="1280" height="720"><media:title type="plain">From 26.7 ms to 16.7 ms: How a simple Optimization Boosted Performance</media:title></media:content></item><item><title>Profiling My CPU-Bound Game Engine: 50% Faster Encoder Setup</title><dc:creator>Harold Serrano</dc:creator><pubDate>Sun, 24 Aug 2025 16:05:09 +0000</pubDate><link>https://www.haroldserrano.com/blog/hunting-bottlenecks-in-my-engine-from-29fps-to-37fps</link><guid isPermaLink="false">54851541e4b0fb60932ad015:54851687e4b016eb8428ff4b:68ab1dd2bb5e895bc5f20c75</guid><description><![CDATA[<p>After adding several cool features to the <a href="https://github.com/untoldengine/UntoldEngine">Untold Engine</a> (did I mention I added a console log), it was time to shift gears and focus on performance. </p>

&nbsp;&nbsp;<p>At the moment, rendering around 214,000 vertices (500 models), the engine was only hitting 29.51 FPS. That’s rough for real-time rendering. Clearly, something needed fixing.</p>











































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/7706094c-e174-4d64-85e0-17ef7adc7d1a/fps-beforeculing.png" data-image-dimensions="3104x1828" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/7706094c-e174-4d64-85e0-17ef7adc7d1a/fps-beforeculing.png?format=1000w" width="3104" height="1828" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/7706094c-e174-4d64-85e0-17ef7adc7d1a/fps-beforeculing.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/7706094c-e174-4d64-85e0-17ef7adc7d1a/fps-beforeculing.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/7706094c-e174-4d64-85e0-17ef7adc7d1a/fps-beforeculing.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/7706094c-e174-4d64-85e0-17ef7adc7d1a/fps-beforeculing.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/7706094c-e174-4d64-85e0-17ef7adc7d1a/fps-beforeculing.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/7706094c-e174-4d64-85e0-17ef7adc7d1a/fps-beforeculing.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/7706094c-e174-4d64-85e0-17ef7adc7d1a/fps-beforeculing.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
          
          <figcaption class="image-caption-wrapper">
            <p data-rte-preserve-empty="true">Current State of the Engine: FPS 29.51</p>
          </figcaption>
        
      
        </figure>
      

    
  


  


<h2>Profiling the Problem</h2><p>I fired up Xcode’s GPU tools and the results were clear: the engine is CPU-bound. </p><ul>
<li>CPU Frame Time: ~33.9ms </li>
<li>GPU Frame Time: ~8.1ms</li>
</ul><p>So while the GPU was waiting around, the CPU was overloaded preparing work.</p>











































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8e7a493a-84db-4cf4-bef2-9f50d1db5104/cpu-bound-beforeculling.png" data-image-dimensions="3104x1828" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8e7a493a-84db-4cf4-bef2-9f50d1db5104/cpu-bound-beforeculling.png?format=1000w" width="3104" height="1828" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8e7a493a-84db-4cf4-bef2-9f50d1db5104/cpu-bound-beforeculling.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8e7a493a-84db-4cf4-bef2-9f50d1db5104/cpu-bound-beforeculling.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8e7a493a-84db-4cf4-bef2-9f50d1db5104/cpu-bound-beforeculling.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8e7a493a-84db-4cf4-bef2-9f50d1db5104/cpu-bound-beforeculling.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8e7a493a-84db-4cf4-bef2-9f50d1db5104/cpu-bound-beforeculling.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8e7a493a-84db-4cf4-bef2-9f50d1db5104/cpu-bound-beforeculling.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8e7a493a-84db-4cf4-bef2-9f50d1db5104/cpu-bound-beforeculling.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
          
          <figcaption class="image-caption-wrapper">
            <p data-rte-preserve-empty="true">Untold Engine is CPU-Bound</p>
          </figcaption>
        
      
        </figure>
      

    
  


  


<p>Looking deeper with Instruments, I found the major culprit: Metal Encoder Setup Time. The CPU was spending ~31ms every frame just encoding commands into the GPU.</p>











































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/3ceb98fb-bcb9-4922-bb8a-c8a499d1037c/MetalEncoderDuration-Before.png" data-image-dimensions="3104x1806" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/3ceb98fb-bcb9-4922-bb8a-c8a499d1037c/MetalEncoderDuration-Before.png?format=1000w" width="3104" height="1806" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/3ceb98fb-bcb9-4922-bb8a-c8a499d1037c/MetalEncoderDuration-Before.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/3ceb98fb-bcb9-4922-bb8a-c8a499d1037c/MetalEncoderDuration-Before.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/3ceb98fb-bcb9-4922-bb8a-c8a499d1037c/MetalEncoderDuration-Before.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/3ceb98fb-bcb9-4922-bb8a-c8a499d1037c/MetalEncoderDuration-Before.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/3ceb98fb-bcb9-4922-bb8a-c8a499d1037c/MetalEncoderDuration-Before.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/3ceb98fb-bcb9-4922-bb8a-c8a499d1037c/MetalEncoderDuration-Before.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/3ceb98fb-bcb9-4922-bb8a-c8a499d1037c/MetalEncoderDuration-Before.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
          
          <figcaption class="image-caption-wrapper">
            <p data-rte-preserve-empty="true">Metal Encoder Preparation</p>
          </figcaption>
        
      
        </figure>
      

    
  


  


<h2>Why So Slow?</h2><p>The bottleneck came from the Shadow and Geometry passes. Each frame, the CPU had to prepare encoders and push all material data for every model—base color, roughness, metallic textures, etc. With hundreds of models, this ballooned into a huge overhead.</p><h2>First Fix: GPU Frustum Culling</h2><p>The engine didn’t have any form of culling, so I decided to implement Frustum Culling. To avoid piling more work on the CPU, I pushed this logic onto the GPU.</p><p>The approach:</p><ul>
<li>Construct the camera frustum.</li>
<li>Compute each entity’s world-space AABB.</li>
<li>Send bounding boxes to the GPU.</li>
<li>GPU checks if each AABB is inside the frustum.</li>
<li>If visible, the entity ID is written into an array via an atomic add.</li>
</ul><p>The key here is that once the GPU returned the list of visible entities, the CPU only needed to encode draw calls for those entities—cutting encoder overhead. It’s a brute-force implementation, but it worked.</p><h2>The Results</h2><p>From the same view location, </p><ul>
<li>FPS jumped from 29 → 37.</li>
<li>CPU Frame Time: 33.9ms → 26.7ms</li>
<li>Metal Encoder Setup Time: 31.0ms → 14.6ms</li>
<li>GPU Frame Time: 8.1ms (unchanged)</li>
</ul>











































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/cb56f4f7-2b9d-4ec9-ac9a-4f5d998a4252/fps-afterculling.png" data-image-dimensions="3104x1828" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/cb56f4f7-2b9d-4ec9-ac9a-4f5d998a4252/fps-afterculling.png?format=1000w" width="3104" height="1828" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/cb56f4f7-2b9d-4ec9-ac9a-4f5d998a4252/fps-afterculling.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/cb56f4f7-2b9d-4ec9-ac9a-4f5d998a4252/fps-afterculling.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/cb56f4f7-2b9d-4ec9-ac9a-4f5d998a4252/fps-afterculling.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/cb56f4f7-2b9d-4ec9-ac9a-4f5d998a4252/fps-afterculling.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/cb56f4f7-2b9d-4ec9-ac9a-4f5d998a4252/fps-afterculling.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/cb56f4f7-2b9d-4ec9-ac9a-4f5d998a4252/fps-afterculling.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/cb56f4f7-2b9d-4ec9-ac9a-4f5d998a4252/fps-afterculling.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
          
          <figcaption class="image-caption-wrapper">
            <p data-rte-preserve-empty="true">Improved FPS</p>
          </figcaption>
        
      
        </figure>
      

    
  


  













































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8bc4c7fb-e2ac-421b-b53d-aba7d1e65baf/cpu-bound-afterculling.png" data-image-dimensions="3104x1828" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8bc4c7fb-e2ac-421b-b53d-aba7d1e65baf/cpu-bound-afterculling.png?format=1000w" width="3104" height="1828" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8bc4c7fb-e2ac-421b-b53d-aba7d1e65baf/cpu-bound-afterculling.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8bc4c7fb-e2ac-421b-b53d-aba7d1e65baf/cpu-bound-afterculling.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8bc4c7fb-e2ac-421b-b53d-aba7d1e65baf/cpu-bound-afterculling.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8bc4c7fb-e2ac-421b-b53d-aba7d1e65baf/cpu-bound-afterculling.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8bc4c7fb-e2ac-421b-b53d-aba7d1e65baf/cpu-bound-afterculling.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8bc4c7fb-e2ac-421b-b53d-aba7d1e65baf/cpu-bound-afterculling.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/8bc4c7fb-e2ac-421b-b53d-aba7d1e65baf/cpu-bound-afterculling.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
          
          <figcaption class="image-caption-wrapper">
            <p data-rte-preserve-empty="true">Engine still CPU-Bound but is an improvement</p>
          </figcaption>
        
      
        </figure>
      

    
  


  


<h2>Summary</h2><p>Here’s the before-and-after snapshot:</p><ul>
<li>FPS: 29 to 37 (+27% improvement)</li>
<li>CPU Frame Time: 33.9 ms to 26.7 ms (Encoder bottleneck reduced)</li>
<li>GPU Frame Time: 8.1 ms to 8.1 ms (Unchanged)</li>
<li>Metal Encoder Setup Time: 31.0 ms to 14.6 ms (Biggest gain)</li>
</ul>











































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/5b47fc6e-ca82-451d-8ecf-48169fdcc989/MetalEncoderDuration-After.png" data-image-dimensions="3104x1806" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/5b47fc6e-ca82-451d-8ecf-48169fdcc989/MetalEncoderDuration-After.png?format=1000w" width="3104" height="1806" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/5b47fc6e-ca82-451d-8ecf-48169fdcc989/MetalEncoderDuration-After.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/5b47fc6e-ca82-451d-8ecf-48169fdcc989/MetalEncoderDuration-After.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/5b47fc6e-ca82-451d-8ecf-48169fdcc989/MetalEncoderDuration-After.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/5b47fc6e-ca82-451d-8ecf-48169fdcc989/MetalEncoderDuration-After.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/5b47fc6e-ca82-451d-8ecf-48169fdcc989/MetalEncoderDuration-After.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/5b47fc6e-ca82-451d-8ecf-48169fdcc989/MetalEncoderDuration-After.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/5b47fc6e-ca82-451d-8ecf-48169fdcc989/MetalEncoderDuration-After.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
          
          <figcaption class="image-caption-wrapper">
            <p data-rte-preserve-empty="true">Metal Encoder Duration decreased</p>
          </figcaption>
        
      
        </figure>
      

    
  


  


<h2>Where Things Stand</h2>
<p>The engine is still CPU-bound, but it’s in a noticeably better state than it was a week ago. By filtering out invisible objects early, I reduced the CPU’s workload and freed up encoder time. It’s not at 60 FPS yet—but the path forward is clearer.</p>
<h2>What’s Next</h2>
<p>Frustum culling was just the first step. To keep pushing toward 60 FPS, here are the next optimization I plan to explore:</p>
<ul>
<li>Metal Bindless Rendering – Instead of rebinding textures and material properties for every draw, I’ll move to a bindless model. All materials will live in a single argument buffer, and each draw will reference them with a simple index. This should drastically cut down CPU encoder overhead and pair nicely with GPU-driven culling.</li>
</ul>
<p>That’s where the engine stands today: better than last week, not yet where it needs to be. But the direction is clear, and each step forward is one step closer to real-time rendering.</p>
<p>Thanks for reading.</p>]]></description><media:content type="image/png" url="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1756181809088-DL2KLB17CIYQ3ZJESGCM/frustumculling-6.png?format=1500w" medium="image" isDefault="true" width="1280" height="720"><media:title type="plain">Profiling My CPU-Bound Game Engine: 50% Faster Encoder Setup</media:title></media:content></item><item><title>Optimizing My Engine’s Light Pass: Lessons from GPU Profiling</title><category>Game Engine Development</category><dc:creator>Harold Serrano</dc:creator><pubDate>Mon, 18 Aug 2025 17:22:07 +0000</pubDate><link>https://www.haroldserrano.com/blog/optimizing-the-light-pass-in-my-engine</link><guid isPermaLink="false">54851541e4b0fb60932ad015:54851687e4b016eb8428ff4b:68a1eebd6d9125395f557424</guid><description><![CDATA[<p>Now that the <a href="https://github.com/untoldengine/UntoldEngine">Untold Engine</a> has most of the features that make it usable, I wanted to spend the next couple of months focusing on performance.</p>

&nbsp;&nbsp;<p>I decided to start with the Light Pass. For testing, I loaded a scene with around 214,000 vertices. Not a huge scene, but enough to get meaningful profiler data. After running the profiler, these were the numbers for the Light Pass:</p><ul>
<li>GPU Time: 2.53 ms</li>
<li>ALU Limiter: 37.65%</li>
<li>ALU Utilization: 35.77%</li>
<li>Texture Read Limiter: 57.8%</li>
<li>Texture Read Utilization: 26.83%</li>
<li>MMU Limiter: 32.19%</li>
</ul>











































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/e0e837d4-168b-42df-919a-f3bbcbcab356/lightpass-beforeoptimization.png" data-image-dimensions="3104x1828" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/e0e837d4-168b-42df-919a-f3bbcbcab356/lightpass-beforeoptimization.png?format=1000w" width="3104" height="1828" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/e0e837d4-168b-42df-919a-f3bbcbcab356/lightpass-beforeoptimization.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/e0e837d4-168b-42df-919a-f3bbcbcab356/lightpass-beforeoptimization.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/e0e837d4-168b-42df-919a-f3bbcbcab356/lightpass-beforeoptimization.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/e0e837d4-168b-42df-919a-f3bbcbcab356/lightpass-beforeoptimization.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/e0e837d4-168b-42df-919a-f3bbcbcab356/lightpass-beforeoptimization.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/e0e837d4-168b-42df-919a-f3bbcbcab356/lightpass-beforeoptimization.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/e0e837d4-168b-42df-919a-f3bbcbcab356/lightpass-beforeoptimization.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<p>The biggest limiter was Texture Read, at almost 58%. This means the GPU was spending a lot of time fetching data from textures—likely because they weren’t being cached efficiently.
In hindsight, I should have started by tackling the biggest limiter. Instead, I first focused on improving the MMU. Not the best choice, but that’s how you learn.</p><blockquote>
<p>MMU Limiter means your GPU performance is constrained by memory address lookups and fetches, not arithmetic.</p>
</blockquote><h2>Optimization 1: Buffer Pre-Loading</h2><p>Buffer pre-loading combines related data into a single buffer so the GPU can fetch it more efficiently, instead of bouncing between multiple buffers.
In my original shader, I was sending light data through separate buffers:</p><p>constant PointLightUniform *pointLights [[buffer(lightPassPointLightsIndex)]]</p><p>constant int *pointLightsCount [[buffer(lightPassPointLightsCountIndex)]]</p><p>I restructured this into a single struct that packages light data together. This change reduced the MMU Limiter from 32.19% → 26.43%.</p>











































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/737c7971-4a39-458e-8a31-b79725039eb5/Buffer-Preloading.png" data-image-dimensions="3104x1828" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/737c7971-4a39-458e-8a31-b79725039eb5/Buffer-Preloading.png?format=1000w" width="3104" height="1828" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/737c7971-4a39-458e-8a31-b79725039eb5/Buffer-Preloading.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/737c7971-4a39-458e-8a31-b79725039eb5/Buffer-Preloading.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/737c7971-4a39-458e-8a31-b79725039eb5/Buffer-Preloading.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/737c7971-4a39-458e-8a31-b79725039eb5/Buffer-Preloading.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/737c7971-4a39-458e-8a31-b79725039eb5/Buffer-Preloading.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/737c7971-4a39-458e-8a31-b79725039eb5/Buffer-Preloading.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/737c7971-4a39-458e-8a31-b79725039eb5/Buffer-Preloading.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<h2>Optimization 2: Use .read() Instead of .sample()</h2><p>During the Light Pass, I fetch data from multiple G-buffer textures such as Position, Normal, Albedo, and SSAO. Originally, I used .sample(), but this does more work than necessary—it applies filtering and mipmap logic, which adds both memory traffic and math.
Switching to .read() (a direct texel fetch) gave a noticeable improvement:</p><ul>
<li>Texture Read Limiter: 57.8% → 46.79%</li>
<li>Texture Read Utilization: 26.83% → 30.03%</li>
</ul>











































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/2e95472f-3ecb-493c-835f-406021f68c32/readtextures.png" data-image-dimensions="3104x1828" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/2e95472f-3ecb-493c-835f-406021f68c32/readtextures.png?format=1000w" width="3104" height="1828" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/2e95472f-3ecb-493c-835f-406021f68c32/readtextures.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/2e95472f-3ecb-493c-835f-406021f68c32/readtextures.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/2e95472f-3ecb-493c-835f-406021f68c32/readtextures.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/2e95472f-3ecb-493c-835f-406021f68c32/readtextures.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/2e95472f-3ecb-493c-835f-406021f68c32/readtextures.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/2e95472f-3ecb-493c-835f-406021f68c32/readtextures.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/2e95472f-3ecb-493c-835f-406021f68c32/readtextures.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<h2>Optimization 3: Reduce G-Buffer Resolution</h2><p>Next, I reduced the G-Buffer textures from float to half. I expected this to lower bandwidth usage, but to my surprise it made things worse:</p><ul>
<li>Texture Read Limiter: 46.79% → 56.9%</li>
<li>Texture Read Utilization: 30.03% → 32.31%</li>
</ul><p>Sometimes optimizations backfire, and this was one of those cases.</p>











































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/0d0ae44e-40bd-420f-b9c1-b173ea3215cb/ReducedG-BufferResolution.png" data-image-dimensions="3104x1828" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/0d0ae44e-40bd-420f-b9c1-b173ea3215cb/ReducedG-BufferResolution.png?format=1000w" width="3104" height="1828" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/0d0ae44e-40bd-420f-b9c1-b173ea3215cb/ReducedG-BufferResolution.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/0d0ae44e-40bd-420f-b9c1-b173ea3215cb/ReducedG-BufferResolution.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/0d0ae44e-40bd-420f-b9c1-b173ea3215cb/ReducedG-BufferResolution.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/0d0ae44e-40bd-420f-b9c1-b173ea3215cb/ReducedG-BufferResolution.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/0d0ae44e-40bd-420f-b9c1-b173ea3215cb/ReducedG-BufferResolution.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/0d0ae44e-40bd-420f-b9c1-b173ea3215cb/ReducedG-BufferResolution.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/0d0ae44e-40bd-420f-b9c1-b173ea3215cb/ReducedG-BufferResolution.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<h2>Optimization 4: Half-Precision Math in Lighting</h2><p>I then focused on ALU utilization by switching parts of my lighting calculations to use half-precision (half) math. Specifically:</p><ul>
<li>Diffuse contribution → half precision</li>
<li>Specular contribution → full precision (float)</li>
</ul><p>The results were underwhelming:</p><ul>
<li>ALU Limiter: 37.65% → 37.55%</li>
<li>ALU Utilization: 35.77% → 35.83%</li>
<li>F32 Utilization: 27.86% → 25.12%</li>
<li>F16 Utilization: 0% → 1.11%</li>
</ul><p>Half-precision math showed up in the counters, but didn’t change the overall bottleneck.</p>











































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/01482942-e538-41ff-b4a7-6ddea32b9346/usehalf-for-diffuse-float-spec.png" data-image-dimensions="3104x1828" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/01482942-e538-41ff-b4a7-6ddea32b9346/usehalf-for-diffuse-float-spec.png?format=1000w" width="3104" height="1828" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/01482942-e538-41ff-b4a7-6ddea32b9346/usehalf-for-diffuse-float-spec.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/01482942-e538-41ff-b4a7-6ddea32b9346/usehalf-for-diffuse-float-spec.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/01482942-e538-41ff-b4a7-6ddea32b9346/usehalf-for-diffuse-float-spec.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/01482942-e538-41ff-b4a7-6ddea32b9346/usehalf-for-diffuse-float-spec.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/01482942-e538-41ff-b4a7-6ddea32b9346/usehalf-for-diffuse-float-spec.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/01482942-e538-41ff-b4a7-6ddea32b9346/usehalf-for-diffuse-float-spec.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/01482942-e538-41ff-b4a7-6ddea32b9346/usehalf-for-diffuse-float-spec.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<h2>Final Results</h2><p>Here’s the overall improvement from all the optimizations:</p><p><em>Before → After</em></p><ul>
<li>GPU Time: 2.53 ms → 2.31 ms (~8.7% faster)</li>
<li>Texture Read Limiter: 57.81% → 50.93%</li>
<li>MMU Limiter: 32.19% → 23.41%</li>
<li>ALU Limiter: 37.65% → 37.55% (flat)</li>
<li>F32 Utilization: 27.86% → 25.12%</li>
<li>F16 Utilization: 0.00% → 1.11%</li>
<li>Integer &amp; Complex Limiter: 14.25% → 8.82%</li>
<li>Texture Read Utilization: 26.83% → 31.62%</li>
</ul><h2>What the Numbers Say</h2><ul>
<li>I shaved about 9% off the Light Pass—a real improvement, but not dramatic.</li>
<li>The biggest wins came from reducing memory-side pressure (MMU ↓, Texture Read Limiter ↓).</li>
<li>ALU stayed flat, which shows the pass is still memory/texture-bound, not math-bound.</li>
<li>Half-precision math registered, but didn’t help much since math wasn’t the bottleneck.</li>
<li>Removing unnecessary integer/complex math improved things locally, but again, the frame was dominated by texture fetch bandwidth.</li>
</ul><h2>Takeaway</h2><p>Optimizations don’t always yield big wins, but each attempt brings clarity. In this case, the profiler clearly shows that the Light Pass is memory/texture-bound. My next steps will focus directly on reducing texture fetch cost, rather than trimming ALU math.</p><p>Thanks for reading.</p>
<p><a href="https://www.haroldserrano.com/blog/optimizing-the-light-pass-in-my-engine">Permalink</a><p>]]></description><media:content type="image/png" url="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1755460964626-7ZYWV2LVDVZAREPG4M7D/light+pass+optimization.png?format=1500w" medium="image" isDefault="true" width="1280" height="720"><media:title type="plain">Optimizing My Engine’s Light Pass: Lessons from GPU Profiling</media:title></media:content></item><item><title>How SSAO Instantly Improved My Engine’s Visuals</title><category>Game Engine Development</category><dc:creator>Harold Serrano</dc:creator><pubDate>Tue, 05 Aug 2025 06:16:32 +0000</pubDate><link>https://www.haroldserrano.com/blog/how-ssao-instantly-improved-my-engines-visuals</link><guid isPermaLink="false">54851541e4b0fb60932ad015:54851687e4b016eb8428ff4b:6891a118016d5d300e5a0c33</guid><description><![CDATA[<p>In this video, you’ll see:</p><ul>
<li>Before/after comparisons of SSAO in action</li>
<li>A quick explanation of how SSAO works</li>
<li>Why it’s worth adding to your renderer</li>
<li>How I integrated it into my lighting pipeline</li>
</ul>
&nbsp;&nbsp;<p>If you're building your own engine or renderer, or just want to level up your graphics knowledge, this one's for you.</p><p>Enjoy.</p>]]></description><media:content type="image/png" url="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1754374580776-2UYZJX4XPZL3X3BZMU6Z/ssao-2.png?format=1500w" medium="image" isDefault="true" width="1280" height="720"><media:title type="plain">How SSAO Instantly Improved My Engine’s Visuals</media:title></media:content></item><item><title>Progress, Not Perfection: How I Work on My Game Engine Daily</title><category>Game Engine Development</category><dc:creator>Harold Serrano</dc:creator><pubDate>Tue, 29 Jul 2025 00:02:22 +0000</pubDate><link>https://www.haroldserrano.com/blog/thismindsetchangedeverything</link><guid isPermaLink="false">54851541e4b0fb60932ad015:54851687e4b016eb8428ff4b:6884d27260d1f561ab39f1fa</guid><description><![CDATA[<p>I took several months off from Youtube to focus entirely on improving the <a href="https://github.com/untoldengine/UntoldEngine">Untold Engine</a> renderer, and it has paid off.</p>

&nbsp;&nbsp;<p>Through sheer work and pushing myself everyday, I have managed to add several features to the renderer such as:</p>
<ul>
<li>Multiple light types: Spot and Area lights</li>
<li>Gizmo tools to translate, rotate and scale</li>
<li>Post-processing shaders such as: Depth of field, Chromatic Aberration, Bloom, Color Grading, White Balance, Vignette effects.</li>
<li>Gizmo tools to manipulate light meshes</li>
<li>Improved Editor's user experience</li>
</ul>
<p>Overall, the renderer feels more complete. While setting up your scene, you can manipulate the position, orientation and scale of each model through the use Gizmo tools. If curious, you can get a quick look at the different PBR textures attached to your model, and if desired, you can update them as well.<br>If desired, you can add any of the four types of lights into your scene: Directional, Point, Spot and Area light. And you can modify their direction by simply dragging the gizmo tool.</p>
<p>Once your scene is ready, you can add several Post-processing effects as mentioned above. Each effect's properties can be manipulated through the editor and you get visual feedback of the effect.</p>
<p>I feel very proud of the stage of the renderer. I fixed several bugs in the renderer and while fixing each bug; I learned a lot more than I expected. However, working on the renderer daily was hard. Between my full-time job and my beautiful family, I was able to spare an hour or so on the renderer. Every day, I had to force myself to wake up before my kids did, so I can focus on getting some work done, even when my energy level was close to zero. Somehow, I managed to get the renderer to its current state, and it is something I feel proud of.</p>
<p>There are several issues with the Renderer and that is OK, because everyday I wake up with the idea that I will make the engine a bit better than it was the day before, and I'm convinced that this mindset will take the engine to the next level. </p>]]></description><media:content type="image/png" url="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1753747201283-J2XCJ6OBOLNQ5VWWFCRC/1percentmindset-6.png?format=1500w" medium="image" isDefault="true" width="1280" height="720"><media:title type="plain">Progress, Not Perfection: How I Work on My Game Engine Daily</media:title></media:content></item><item><title>I Built An Editor For This One Feature!!!</title><category>Game Engine Development</category><dc:creator>Harold Serrano</dc:creator><pubDate>Sun, 27 Apr 2025 13:32:23 +0000</pubDate><link>https://www.haroldserrano.com/blog/this-test-could-make-or-break-my-engine</link><guid isPermaLink="false">54851541e4b0fb60932ad015:54851687e4b016eb8428ff4b:680e3130e5d4447049df9771</guid><description><![CDATA[<p>After two years of rewriting my game engine from scratch using ECS architecture, I finally arrived at a huge milestone: testing whether I can assign gameplay behavior to a character directly through the editor. On the surface, it might sound like a small step — but behind this test is months of work, late nights, countless bugs, and an entire editor system built from scratch.</p><p>Building the editor was one of the hardest parts of this journey. From the Scenegraph to the Inspector, the Asset Browser to mouse selection (Ray Picker) — every system needed to work together seamlessly. There were moments when I seriously considered scrapping the editor entirely. But deep down, I knew that if I wanted my engine to be flexible and developer-friendly, this feature had to exist.</p>
&nbsp;&nbsp;<p>In this devlog, I walk through the emotional build-up leading to the moment of truth: hitting Play and seeing if everything works. This test isn’t just about a feature — it’s about proving that the foundation I’ve been building can support the kind of dynamic, user-driven workflows I wanted. It's a small feature, but a huge step for my engine.</p>]]></description><media:content type="image/png" url="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1745766519004-2DKA59A7Q3XLFANKU6MC/will+it+fall+apart-3.png?format=1500w" medium="image" isDefault="true" width="1280" height="720"><media:title type="plain">I Built An Editor For This One Feature!!!</media:title></media:content></item><item><title>One Step Closer to a Full Game Engine Editor</title><category>Game Engine Development</category><dc:creator>Harold Serrano</dc:creator><pubDate>Tue, 15 Apr 2025 06:08:19 +0000</pubDate><link>https://www.haroldserrano.com/blog/one-step-closer-to-a-full-game-engine-editor</link><guid isPermaLink="false">54851541e4b0fb60932ad015:54851687e4b016eb8428ff4b:67fdf75d4e260c2b7187be6e</guid><description><![CDATA[<p>In this devlog, I’m taking you through the latest updates to the <a href="https://github.com/untoldengine/UntoldEngine">Untold Engine</a> editor—lighting, cameras, asset browsing, and image-based lighting are now fully integrated into the UI.</p>
<p>No more hacking things in through code—everything can be controlled visually. I’ll show you the problems I ran into, how I solved them, and what the engine looks like now in action.</p>
<p>This update brings the editor one step closer to becoming a fully usable game creation tool—and I’d love to hear your thoughts on where to take it next.</p>

&nbsp;&nbsp;<p>Thanks for watching.</p>]]></description><media:content type="image/png" url="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1744697228047-45VUYVKT66TCKGMPEQWY/engineupdateapril1.png?format=1500w" medium="image" isDefault="true" width="1280" height="720"><media:title type="plain">One Step Closer to a Full Game Engine Editor</media:title></media:content></item><item><title>My Engine's Editor Had One Big Problem!!!!</title><category>Game Engine Development</category><dc:creator>Harold Serrano</dc:creator><pubDate>Tue, 11 Mar 2025 06:06:44 +0000</pubDate><link>https://www.haroldserrano.com/blog/my-engines-editor-had-one-big-problem</link><guid isPermaLink="false">54851541e4b0fb60932ad015:54851687e4b016eb8428ff4b:67cfd25a004c11010b61bbd2</guid><description><![CDATA[<p>In this video, I share one big problem that my engine's editor had.</p>
&nbsp;&nbsp;<p>Thanks for watching.</p>]]></description><media:content type="image/png" url="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1741673199142-YWKYGCAP10B0IYP7ZLX0/Everything+Gone%21%21%21.png?format=1500w" medium="image" isDefault="true" width="1280" height="720"><media:title type="plain">My Engine's Editor Had One Big Problem!!!!</media:title></media:content></item><item><title>The Missing System in My Engine!!! </title><category>Game Engine Development</category><dc:creator>Harold Serrano</dc:creator><pubDate>Mon, 03 Mar 2025 05:46:40 +0000</pubDate><link>https://www.haroldserrano.com/blog/the-missing-system-in-my-engine</link><guid isPermaLink="false">54851541e4b0fb60932ad015:54851687e4b016eb8428ff4b:67c541b758a22d05f3d2a2db</guid><description><![CDATA[<p>In this video, I show you the Editor I'm developing for the Untold Engine. You can add entities and add components dynamically, thus making game development easier. It is still a work in progress.</p>
&nbsp;&nbsp;<p>Enjoy</p>]]></description><media:content type="image/png" url="https://images.squarespace-cdn.com/content/v1/54851541e4b0fb60932ad015/1740980786965-63KQNR71VJD849R3CJF2/editor+showcase.png?format=1500w" medium="image" isDefault="true" width="1280" height="720"><media:title type="plain">The Missing System in My Engine!!!</media:title></media:content></item></channel></rss>