<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
	<channel>
		<title><![CDATA[Esenthel Forum - Showcase]]></title>
		<link>https://esenthel.com/forum/</link>
		<description><![CDATA[Esenthel Forum - https://esenthel.com/forum]]></description>
		<pubDate>Mon, 27 Jul 2026 14:27:09 +0000</pubDate>
		<generator>MyBB</generator>
		<item>
			<title><![CDATA[Weather]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11839</link>
			<pubDate>Thu, 23 Jul 2026 16:23:42 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11839</guid>
			<description><![CDATA[Video:<br />
<br />
<a href="https://streamable.com/jzrk6s" target="_blank">https://streamable.com/jzrk6s</a><br />
<br />
<br />
<br />
<a href="https://ibb.co/SX12tqdW" target="_blank"><img src="https://i.ibb.co/qM4zNbWX/Screenshot-From-2026-07-23-12-01-15.png" border="0" alt="[Image: Screenshot-From-2026-07-23-12-01-15.png]" /></a><br />
<a href="https://ibb.co/LX64Z09d" target="_blank"><img src="https://i.ibb.co/8L2F7mPg/Screenshot-From-2026-07-23-12-01-39.png" border="0" alt="[Image: Screenshot-From-2026-07-23-12-01-39.png]" /></a><br />
<a href="https://ibb.co/XZkskqZk" target="_blank"><img src="https://i.ibb.co/Rp404qp4/Screenshot-From-2026-07-23-12-01-23.png" border="0" alt="[Image: Screenshot-From-2026-07-23-12-01-23.png]" /></a><br />
<a href="https://ibb.co/fVk77ZNs" target="_blank"><img src="https://i.ibb.co/V0g88bCs/Screenshot-From-2026-07-23-12-02-43.png" border="0" alt="[Image: Screenshot-From-2026-07-23-12-02-43.png]" /></a><br />
<a href="https://ibb.co/fYfphLvZ" target="_blank"><img src="https://i.ibb.co/XZ1shM86/Screenshot-From-2026-07-23-12-02-26.png" border="0" alt="[Image: Screenshot-From-2026-07-23-12-02-26.png]" /></a><br />
<a href="https://ibb.co/YBkDQQqL" target="_blank"><img src="https://i.ibb.co/HTzN44kH/Screenshot-From-2026-07-23-12-02-14.png" border="0" alt="[Image: Screenshot-From-2026-07-23-12-02-14.png]" /></a><br />
<a href="https://ibb.co/mVrhNDFz" target="_blank"><img src="https://i.ibb.co/hJx2Y718/Screenshot-From-2026-07-23-12-02-02.png" border="0" alt="[Image: Screenshot-From-2026-07-23-12-02-02.png]" /></a><br />
<br />
AI written summary:<br />
<br />
<span style="font-size: 16pt;"><span style="font-weight: bold;">New Rendering Pillar: Sky, Clouds, Fog &amp; Weather</span></span><br />
<br />
I've merged a full atmosphere/weather stack into my fork. It's five independent<br />
systems that share one physically-based sky, plus a unified wind bus that ties the<br />
whole scene together. Everything below is <span style="font-weight: bold;">opt-in and default-OFF</span> - if you<br />
enable none of it, your project renders exactly as before (<span style="font-style: italic;">bit-identical, verified<br />
against the stock Sky tutorial</span>). It's <span style="font-weight: bold;">Vulkan-first</span>: where a system's shaders<br />
or compute support aren't present, it falls back to the classic path automatically.<br />
<br />
<hr />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">The five systems at a glance</span></span><br />
<ul>
<li><span style="font-weight: bold;">Physical Sky Atmosphere</span> (Hillaire 2020) - <span style="font-style: italic;">Sky.atmosphericPhysical() / SkyAtmo</span></li>
<li><span style="font-weight: bold;">Dynamic Environment Capture</span> (sky-derived ambient + reflections)  <span style="font-style: italic;">D.envMapDynamic(true) / EnvCapture</span> - <span style="color: #CC0000;">Vulkan only</span></li>
<li><span style="font-weight: bold;">Volumetric Clouds V2</span> (Nubis-lineage GPU raymarch) - <span style="font-style: italic;">Clouds.v2.create()</span></li>
<li><span style="font-weight: bold;">Froxel Volumetric Fog</span> (Wronski/Hillaire) - <span style="font-style: italic;">D.volumetricFog(true) / VolFog</span></li>
<li><span style="font-weight: bold;">Weather + Unified Wind Bus</span> - <span style="font-style: italic;">Weather.enable() / Wind</span><br />
</li></ul>
<br />
...plus a <span style="font-weight: bold;">linear (start/end-distance) global fog</span> mode you can use on its own.<br />
<br />
<hr />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">What each system does</span></span><br />
<br />
<span style="font-weight: bold;">Physical Sky Atmosphere</span><br />
A Hillaire-style atmosphere with Rayleigh, Mie and ozone scattering, multiple<br />
scattering with ground bounce, a physical sun disc, and stars that fade out with sky<br />
luminance. It also drives <span style="font-weight: bold;">aerial perspective</span> - distant geometry picks up the<br />
correct atmospheric tint. Presentation knobs (sky radiance, saturation, sun-disc size,<br />
star fade) grade the result without rebuilding the scattering LUTs, so tweaking the<br />
look is cheap.<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Sky.atmosphericPhysical(true);</code></div></div>
<br />
<span style="font-weight: bold;">Dynamic Environment Capture</span><br />
Instead of a static cubemap, this captures one cube face per frame from the physical<br />
sky into an HDR cube with prefiltered mips, feeding ambient light, reflections and the<br />
water cube. Reflections and ambient warm-shift at dusk on their own. (Vulkan only;<br />
disabling it restores the static env map.)<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>D.envMapDynamic(true);</code></div></div>
<br />
<span style="font-weight: bold;">Volumetric Clouds V2</span><br />
A real GPU cloud raymarcher - Perlin-Worley + Worley noise, Henyey-Greenstein phase,<br />
powder and multi-scatter - with amortized temporal reconstruction and a deterministic<br />
per-pixel ray phase so the clouds stay stable instead of boiling. A clear sky costs<br />
almost nothing (CPU early-out); a full overcast deck is a few ms at 3/4 trace res.<br />
Coverage, type, density and wind are all driven per-frame with <span style="font-weight: bold;">zero shader<br />
rebuilds</span>.<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Clouds.v2.create();<br />
Clouds.v2.crispness(0.3); // trim thin wisps into crisper shapes</code></div></div>
<br />
<span style="font-weight: bold;">Froxel Volumetric Fog</span><br />
A camera-frustum froxel fog volume with height fog and sun in-scatter lit <span style="font-style: italic;">through<br />
the cascade shadow map</span> - so you get real light shafts / god rays, not a flat tint.<br />
Transparent surfaces sample the same volume at their own depth, so there are no fog<br />
halos around alpha geometry. Coexists with the classic analytic fog.<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>D.volumetricFog(true);</code></div></div>
<br />
<span style="font-weight: bold;">Weather + Unified Wind Bus</span><br />
Presets (<span style="font-style: italic;">CLEAR ... STORM ... SNOW / FOG / CUSTOM</span>) blend over timed transitions<br />
and drive cloud coverage/type/density, fog, and sun/ambient/sky as <span style="font-weight: bold;">multipliers on<br />
your authored values</span> - all restored exactly when you disable it. A single <span style="font-weight: bold;">Wind</span><br />
bus feeds grass, cloth, water, particles and cloud advection from one source. On top of<br />
that: camera-locked rain/snow, a global wetness pass (up-facing surfaces get more<br />
reflective), lightning flash + delayed thunder, and rain/wind audio.<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Weather.enable();<br />
Weather.preset(WEATHER_STORM);</code></div></div>
<br />
<hr />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">Tutorials - new "23 - Weather" category</span></span><br />
<br />
Five new tutorials under <span style="font-style: italic;">Tutorials/Source/23 - Weather/</span>. They're <span style="font-weight: bold;">not</span> gated<br />
on Vulkan the sky degrades to the classic gradient when its shaders are absent, so<br />
they build and run on every backend.<br />
<br />
<ol type="1">
<li><span style="font-weight: bold;">Tutorial_23_SkyAtmosphere</span> - the physical sky/atmosphere on its own: sun disc, stars, aerial perspective, and the grading knobs.</li>
<li><span style="font-weight: bold;">Tutorial_23_SkyAmbient</span> - dynamic environment capture: watch reflections and ambient shift as the sun moves through the day.</li>
<li><span style="font-weight: bold;">Tutorial_23_VolumetricClouds2</span> - the cloud raymarcher, with test scenes and a live crispness control.</li>
<li><span style="font-weight: bold;">Tutorial_23_FroxelFog</span> - volumetric fog and sun shafts / god rays.</li>
<li><span style="font-weight: bold;">Tutorial_23_Weather</span> - the whole thing together: toggle CLEAR vs STORM and watch clouds, fog, rain, wet ground and lightning respond in one scene.<br />
</li></ol>
<br />
Build any of them the usual way:<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>cmake --build cmake-build-debug --target Tutorial_23_Weather</code></div></div>
<br />
<hr />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">Notes</span></span><br />
<ul>
<li>Every system is <span style="font-weight: bold;">default-OFF</span> existing projects are untouched until you opt in.</li>
<li><span style="font-weight: bold;">Vulkan</span> gives the full effect; other backends fall back gracefully.</li>
<li>Weather is the only system that hooks core per-frame code (a wetness hook + a wind-update hook), and both short-circuit when Weather is disabled.<br />
</li></ul>
<br />
Full design + frame-composition write-ups are in the repo:<br />
<span style="font-style: italic;">docs/sky-fog-weather-design.md</span> and <span style="font-style: italic;">docs/clouds-v2-handoff.md</span>.<br />
<br />
In this fork:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></description>
			<content:encoded><![CDATA[Video:<br />
<br />
<a href="https://streamable.com/jzrk6s" target="_blank">https://streamable.com/jzrk6s</a><br />
<br />
<br />
<br />
<a href="https://ibb.co/SX12tqdW" target="_blank"><img src="https://i.ibb.co/qM4zNbWX/Screenshot-From-2026-07-23-12-01-15.png" border="0" alt="[Image: Screenshot-From-2026-07-23-12-01-15.png]" /></a><br />
<a href="https://ibb.co/LX64Z09d" target="_blank"><img src="https://i.ibb.co/8L2F7mPg/Screenshot-From-2026-07-23-12-01-39.png" border="0" alt="[Image: Screenshot-From-2026-07-23-12-01-39.png]" /></a><br />
<a href="https://ibb.co/XZkskqZk" target="_blank"><img src="https://i.ibb.co/Rp404qp4/Screenshot-From-2026-07-23-12-01-23.png" border="0" alt="[Image: Screenshot-From-2026-07-23-12-01-23.png]" /></a><br />
<a href="https://ibb.co/fVk77ZNs" target="_blank"><img src="https://i.ibb.co/V0g88bCs/Screenshot-From-2026-07-23-12-02-43.png" border="0" alt="[Image: Screenshot-From-2026-07-23-12-02-43.png]" /></a><br />
<a href="https://ibb.co/fYfphLvZ" target="_blank"><img src="https://i.ibb.co/XZ1shM86/Screenshot-From-2026-07-23-12-02-26.png" border="0" alt="[Image: Screenshot-From-2026-07-23-12-02-26.png]" /></a><br />
<a href="https://ibb.co/YBkDQQqL" target="_blank"><img src="https://i.ibb.co/HTzN44kH/Screenshot-From-2026-07-23-12-02-14.png" border="0" alt="[Image: Screenshot-From-2026-07-23-12-02-14.png]" /></a><br />
<a href="https://ibb.co/mVrhNDFz" target="_blank"><img src="https://i.ibb.co/hJx2Y718/Screenshot-From-2026-07-23-12-02-02.png" border="0" alt="[Image: Screenshot-From-2026-07-23-12-02-02.png]" /></a><br />
<br />
AI written summary:<br />
<br />
<span style="font-size: 16pt;"><span style="font-weight: bold;">New Rendering Pillar: Sky, Clouds, Fog &amp; Weather</span></span><br />
<br />
I've merged a full atmosphere/weather stack into my fork. It's five independent<br />
systems that share one physically-based sky, plus a unified wind bus that ties the<br />
whole scene together. Everything below is <span style="font-weight: bold;">opt-in and default-OFF</span> - if you<br />
enable none of it, your project renders exactly as before (<span style="font-style: italic;">bit-identical, verified<br />
against the stock Sky tutorial</span>). It's <span style="font-weight: bold;">Vulkan-first</span>: where a system's shaders<br />
or compute support aren't present, it falls back to the classic path automatically.<br />
<br />
<hr />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">The five systems at a glance</span></span><br />
<ul>
<li><span style="font-weight: bold;">Physical Sky Atmosphere</span> (Hillaire 2020) - <span style="font-style: italic;">Sky.atmosphericPhysical() / SkyAtmo</span></li>
<li><span style="font-weight: bold;">Dynamic Environment Capture</span> (sky-derived ambient + reflections)  <span style="font-style: italic;">D.envMapDynamic(true) / EnvCapture</span> - <span style="color: #CC0000;">Vulkan only</span></li>
<li><span style="font-weight: bold;">Volumetric Clouds V2</span> (Nubis-lineage GPU raymarch) - <span style="font-style: italic;">Clouds.v2.create()</span></li>
<li><span style="font-weight: bold;">Froxel Volumetric Fog</span> (Wronski/Hillaire) - <span style="font-style: italic;">D.volumetricFog(true) / VolFog</span></li>
<li><span style="font-weight: bold;">Weather + Unified Wind Bus</span> - <span style="font-style: italic;">Weather.enable() / Wind</span><br />
</li></ul>
<br />
...plus a <span style="font-weight: bold;">linear (start/end-distance) global fog</span> mode you can use on its own.<br />
<br />
<hr />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">What each system does</span></span><br />
<br />
<span style="font-weight: bold;">Physical Sky Atmosphere</span><br />
A Hillaire-style atmosphere with Rayleigh, Mie and ozone scattering, multiple<br />
scattering with ground bounce, a physical sun disc, and stars that fade out with sky<br />
luminance. It also drives <span style="font-weight: bold;">aerial perspective</span> - distant geometry picks up the<br />
correct atmospheric tint. Presentation knobs (sky radiance, saturation, sun-disc size,<br />
star fade) grade the result without rebuilding the scattering LUTs, so tweaking the<br />
look is cheap.<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Sky.atmosphericPhysical(true);</code></div></div>
<br />
<span style="font-weight: bold;">Dynamic Environment Capture</span><br />
Instead of a static cubemap, this captures one cube face per frame from the physical<br />
sky into an HDR cube with prefiltered mips, feeding ambient light, reflections and the<br />
water cube. Reflections and ambient warm-shift at dusk on their own. (Vulkan only;<br />
disabling it restores the static env map.)<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>D.envMapDynamic(true);</code></div></div>
<br />
<span style="font-weight: bold;">Volumetric Clouds V2</span><br />
A real GPU cloud raymarcher - Perlin-Worley + Worley noise, Henyey-Greenstein phase,<br />
powder and multi-scatter - with amortized temporal reconstruction and a deterministic<br />
per-pixel ray phase so the clouds stay stable instead of boiling. A clear sky costs<br />
almost nothing (CPU early-out); a full overcast deck is a few ms at 3/4 trace res.<br />
Coverage, type, density and wind are all driven per-frame with <span style="font-weight: bold;">zero shader<br />
rebuilds</span>.<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Clouds.v2.create();<br />
Clouds.v2.crispness(0.3); // trim thin wisps into crisper shapes</code></div></div>
<br />
<span style="font-weight: bold;">Froxel Volumetric Fog</span><br />
A camera-frustum froxel fog volume with height fog and sun in-scatter lit <span style="font-style: italic;">through<br />
the cascade shadow map</span> - so you get real light shafts / god rays, not a flat tint.<br />
Transparent surfaces sample the same volume at their own depth, so there are no fog<br />
halos around alpha geometry. Coexists with the classic analytic fog.<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>D.volumetricFog(true);</code></div></div>
<br />
<span style="font-weight: bold;">Weather + Unified Wind Bus</span><br />
Presets (<span style="font-style: italic;">CLEAR ... STORM ... SNOW / FOG / CUSTOM</span>) blend over timed transitions<br />
and drive cloud coverage/type/density, fog, and sun/ambient/sky as <span style="font-weight: bold;">multipliers on<br />
your authored values</span> - all restored exactly when you disable it. A single <span style="font-weight: bold;">Wind</span><br />
bus feeds grass, cloth, water, particles and cloud advection from one source. On top of<br />
that: camera-locked rain/snow, a global wetness pass (up-facing surfaces get more<br />
reflective), lightning flash + delayed thunder, and rain/wind audio.<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Weather.enable();<br />
Weather.preset(WEATHER_STORM);</code></div></div>
<br />
<hr />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">Tutorials - new "23 - Weather" category</span></span><br />
<br />
Five new tutorials under <span style="font-style: italic;">Tutorials/Source/23 - Weather/</span>. They're <span style="font-weight: bold;">not</span> gated<br />
on Vulkan the sky degrades to the classic gradient when its shaders are absent, so<br />
they build and run on every backend.<br />
<br />
<ol type="1">
<li><span style="font-weight: bold;">Tutorial_23_SkyAtmosphere</span> - the physical sky/atmosphere on its own: sun disc, stars, aerial perspective, and the grading knobs.</li>
<li><span style="font-weight: bold;">Tutorial_23_SkyAmbient</span> - dynamic environment capture: watch reflections and ambient shift as the sun moves through the day.</li>
<li><span style="font-weight: bold;">Tutorial_23_VolumetricClouds2</span> - the cloud raymarcher, with test scenes and a live crispness control.</li>
<li><span style="font-weight: bold;">Tutorial_23_FroxelFog</span> - volumetric fog and sun shafts / god rays.</li>
<li><span style="font-weight: bold;">Tutorial_23_Weather</span> - the whole thing together: toggle CLEAR vs STORM and watch clouds, fog, rain, wet ground and lightning respond in one scene.<br />
</li></ol>
<br />
Build any of them the usual way:<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>cmake --build cmake-build-debug --target Tutorial_23_Weather</code></div></div>
<br />
<hr />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">Notes</span></span><br />
<ul>
<li>Every system is <span style="font-weight: bold;">default-OFF</span> existing projects are untouched until you opt in.</li>
<li><span style="font-weight: bold;">Vulkan</span> gives the full effect; other backends fall back gracefully.</li>
<li>Weather is the only system that hooks core per-frame code (a wetness hook + a wind-update hook), and both short-circuit when Weather is disabled.<br />
</li></ul>
<br />
Full design + frame-composition write-ups are in the repo:<br />
<span style="font-style: italic;">docs/sky-fog-weather-design.md</span> and <span style="font-style: italic;">docs/clouds-v2-handoff.md</span>.<br />
<br />
In this fork:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Android Vulkan]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11836</link>
			<pubDate>Tue, 14 Jul 2026 16:23:26 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11836</guid>
			<description><![CDATA[<a href="https://postimg.cc/hQvRwTrJ" target="_blank"><img src="https://i.postimg.cc/d3mQh9Z9/android-world-1.png" border="0" alt="[Image: android-world-1.png]" /></a><br />
<br />
<a href="https://postimg.cc/N2FvVm44" target="_blank"><img src="https://i.postimg.cc/J0c7sQH9/android-world-moving.png" border="0" alt="[Image: android-world-moving.png]" /></a><br />
<br />
<br />
<br />
AI summary:<br />
<br />
The engine now builds and runs on Android arm64, using <span style="font-weight: bold;">the exact same Vulkan 1.3 backend as the PC path</span>. There is no GLES renderer, no cut-down mobile fork, and no separate shader set. The renderer that draws your desktop frame is the renderer that draws the phone's.<br />
<br />
It renders, takes touch input, handles rotation, survives being backgrounded, plays positional audio, loads mobile-compressed textures, and composites an HTML/CSS HUD over a streamed 3D world with a playable character.<br />
<br />
And rather than claim "it works on Android", I ran it on a device farm. <span style="font-weight: bold;">176 device/OS combinations</span> on real hardware. The full results — including everything that fails, and why — are in this post.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">What's in it</span></span><br />
<ul>
<li><span style="font-weight: bold;">Vulkan 1.3 + descriptor indexing (bindless)</span> as the baseline — the same requirement as PC. WSI via <span style="font-family: monospace;">vkCreateAndroidSurfaceKHR</span>.</li>
<li><span style="font-weight: bold;">Touch input</span>, including a virtual joystick (left thumb moves, right thumb turns).</li>
<li><span style="font-weight: bold;">Display rotation</span> and <span style="font-weight: bold;">resume from background</span>.</li>
<li><span style="font-weight: bold;">Positional audio</span> — openal-soft is now the Android sound provider, so the whole environmental audio pillar (EFX reverb, HRTF, occlusion) lights up on a phone.</li>
<li><span style="font-weight: bold;">ASTC / EAC textures</span> — proper mobile formats, with a headless publish step: <span style="font-family: monospace;">TitanEditor --publish=android</span>.</li>
<li><span style="font-weight: bold;">RmlUi</span> — the HTML/CSS UI stack cross-compiles for Android and is fully interactive by touch.</li>
<li><span style="font-weight: bold;">A real game demo</span> — streamed Game::World, textured terrain, a playable character, RmlUi HUD, in landscape.<br />
</li></ul>
<br />
The deliverable is a shared <span style="font-family: monospace;">libEsenthelProject.so</span> loaded by a NativeActivity subclass. Editor and tutorials are gated off for Android.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">The engine's render defaults are DESKTOP defaults</span></span><br />
<br />
This was the biggest perf lesson, and it will bite anyone porting: <span style="font-weight: bold;">the engine's defaults are desktop defaults, and on a phone they are ruinous.</span> Six shadow cascades at 2048², SSAO, and an FSR-style upscaler are all perfectly sensible on a GPU with a fan.<br />
<br />
Measured with GPU timestamps on an Adreno 740 at 2316x1080: <span style="font-weight: bold;">47 -&gt; 72 fps</span>, from three changes.<br />
<ul>
<li><span style="font-weight: bold;">Shadows</span> — 6 cascades @2048² down to 3 @1024². Light phase 6.8 -&gt; 3.9 ms.</li>
<li><span style="font-weight: bold;">Ambient</span> — SSAO down to AMBIENT_FLAT.</li>
<li><span style="font-weight: bold;">Density</span> — 75% render scale. Opaque 4.9 -&gt; 3.2 ms, light 3.9 -&gt; 2.4 ms.<br />
</li></ul>
<br />
<span style="font-weight: bold;">And one genuine trap.</span> The default density filter is <span style="font-family: monospace;">FILTER_EASU</span> (FSR-style upscaling), and it <span style="font-style: italic;">costs more than the pixels it saves</span>. Rendering at 75% with EASU (49 fps) was <span style="font-weight: bold;">slower</span> than rendering at 100% and not upscaling at all (55 fps). Pair any density change with <span style="font-family: monospace;">FILTER_LINEAR</span>, or you will conclude — wrongly — that resolution scaling does nothing.<br />
<br />
I did not trust that. It was one GPU.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">So I measured it on four</span></span><br />
<br />
"EASU is a trap" is a claim about one GPU's ALU-to-bandwidth balance. Generalising that into a shipping default without checking is exactly the kind of thing that quietly embarrasses you later. So: same world, same fixed camera, same frame counts, four render presets, swept <span style="font-style: italic;">inside a single run</span> on each device.<br />
<br />
<span style="font-weight: bold;">GPU milliseconds. "Shipped" is 75% + LINEAR. "+EASU" changes ONLY the filter, so the difference between those two columns is the cost of EASU and nothing else.</span><br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>GPU&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; device&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;shipped&nbsp;&nbsp; +EASU&nbsp;&nbsp; native(100%)&nbsp;&nbsp; EASU costs<br />
-----------------------------------------------------------------------------<br />
Adreno 740&nbsp;&nbsp;&nbsp;&nbsp;S23 Ultra&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 5.04&nbsp;&nbsp;&nbsp;&nbsp;11.32&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 8.95&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;+5.5 ms<br />
Adreno 750&nbsp;&nbsp;&nbsp;&nbsp;Galaxy S24&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;3.36&nbsp;&nbsp;&nbsp;&nbsp; 5.73&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 4.89&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;+2.5 ms<br />
Mali-G710&nbsp;&nbsp;&nbsp;&nbsp; Pixel 7 Pro&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 6.11&nbsp;&nbsp;&nbsp;&nbsp;12.53&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 8.67&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;+6.0 ms<br />
Mali-G68&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Galaxy A54&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 18.97&nbsp;&nbsp;&nbsp;&nbsp;32.95&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;29.79&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; +15.1 ms</code></div></div>
<br />
<span style="font-weight: bold;">The trap is universal, and far worse on Mali than on the Adreno where I found it.</span> On every GPU measured, upscaling and then running EASU costs more GPU time than simply rendering every pixel. A Mali-G68 pays <span style="font-weight: bold;">+15.1 ms</span> — on a device whose entire 60 fps budget is 16.7 ms. Generalising the Adreno figure would have understated the problem by 3x.<br />
<br />
The reassuring half: the shipped default (75% + LINEAR) is the fastest preset on all four GPUs. The setting was right. The evidence for it was thin, and now it isn't.<br />
<br />
<span style="font-weight: bold;">Read GPU milliseconds, not fps.</span> The Adreno 750 sits at ~70 fps under <span style="font-style: italic;">every</span> preset while its GPU time swings 3.4 -&gt; 6.4 ms. It is panel/CPU-capped, so fps there measures the ceiling, not the render cost. Benchmarking a phone by frame rate alone measures the screen.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">The device matrix — 176 combos on real hardware</span></span><br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Runs&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 94&nbsp;&nbsp;&nbsp;&nbsp;Vulkan 1.3, bindless, 0 validation errors<br />
Below baseline&nbsp;&nbsp; 80&nbsp;&nbsp;&nbsp;&nbsp;Vulkan 1.1 — not ONE exposes dynamic_rendering<br />
Fails&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 2&nbsp;&nbsp;&nbsp;&nbsp;Mali-G52 MC2 — no descriptor indexing<br />
Measured&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;176 / 199 catalogue combos</code></div></div>
<br />
<span style="font-weight: bold;"><span style="font-size: medium;">Phones it RUNS on — 0 validation errors</span></span><br />
<br />
"fps" is filled in only where I ran the perf sweep; it is a property of the GPU, not the phone. "BC" is whether the GPU can read the engine's BC7 textures natively — see further down, this turns out to matter more than the failure column.<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>GPU&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;fps&nbsp;&nbsp; BC&nbsp;&nbsp; Devices<br />
------------------------------------------------------------------------<br />
Mali-G715&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; —&nbsp;&nbsp;&nbsp;&nbsp;no&nbsp;&nbsp; Pixel 8, 8 Pro, 8a, 9, 9 Pro, 9 Pro XL,<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;9 Pro Fold, 9a, 10a<br />
Adreno 740&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;88&nbsp;&nbsp; yes&nbsp;&nbsp;Galaxy S23, S23+, S23 Ultra, Tab S9,<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Z Flip5, Z Fold5, Xperia 1 V<br />
Adreno 750&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;69&nbsp;&nbsp; yes&nbsp;&nbsp;Galaxy S24, S24+, S24 Ultra,<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Z Flip6, Z Fold6<br />
Mali-G710&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 53&nbsp;&nbsp; no&nbsp;&nbsp; Pixel 7, 7 Pro, 7a, Pixel Fold, Pixel Tablet<br />
Mali-G68&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;25&nbsp;&nbsp; no&nbsp;&nbsp; Galaxy A16, A26, A35, A54, Tab S9 FE<br />
Adreno 830&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;—&nbsp;&nbsp;&nbsp;&nbsp;yes&nbsp;&nbsp;Galaxy S25, S25+, S25 Edge, S25 Ultra,<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Z TriFold<br />
PowerVR DXT-48-1536&nbsp;&nbsp;&nbsp;&nbsp; —&nbsp;&nbsp;&nbsp;&nbsp;no&nbsp;&nbsp; Pixel 10, 10 Pro, 10 Pro XL, 10 Pro Fold<br />
Samsung Xclipse 940&nbsp;&nbsp;&nbsp;&nbsp; —&nbsp;&nbsp;&nbsp;&nbsp;no&nbsp;&nbsp; Galaxy S24, S24+, S24 FE&nbsp;&nbsp; (Exynos variants)<br />
Mali-G57 MC2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;—&nbsp;&nbsp;&nbsp;&nbsp;no&nbsp;&nbsp; Galaxy A15, XCover7, moto g (2025)<br />
Adreno 840&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;—&nbsp;&nbsp;&nbsp;&nbsp;yes&nbsp;&nbsp;Galaxy S26, S26+<br />
Adreno 710&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;—&nbsp;&nbsp;&nbsp;&nbsp;yes&nbsp;&nbsp;Galaxy A36<br />
Mali-G68 MC4&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;—&nbsp;&nbsp;&nbsp;&nbsp;no&nbsp;&nbsp; Galaxy A34, Lenovo Tab P12<br />
Mali-G57&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;—&nbsp;&nbsp;&nbsp;&nbsp;no&nbsp;&nbsp; moto g04, realme C53<br />
Samsung Xclipse 540&nbsp;&nbsp;&nbsp;&nbsp; —&nbsp;&nbsp;&nbsp;&nbsp;no&nbsp;&nbsp; Galaxy A56<br />
Adreno 735&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;—&nbsp;&nbsp;&nbsp;&nbsp;yes&nbsp;&nbsp;motorola razr plus 2024<br />
Mali-G720 Immortalis&nbsp;&nbsp;&nbsp;&nbsp;—&nbsp;&nbsp;&nbsp;&nbsp;no&nbsp;&nbsp; Galaxy Tab S10+<br />
Samsung Xclipse 530&nbsp;&nbsp;&nbsp;&nbsp; —&nbsp;&nbsp;&nbsp;&nbsp;no&nbsp;&nbsp; Galaxy A55<br />
Samsung Xclipse 920&nbsp;&nbsp;&nbsp;&nbsp; —&nbsp;&nbsp;&nbsp;&nbsp;no&nbsp;&nbsp; Galaxy S23 FE&nbsp;&nbsp;(Exynos variant)<br />
Mali-G78&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;—&nbsp;&nbsp;&nbsp;&nbsp;no&nbsp;&nbsp; Pixel 6</code></div></div>
<br />
Adreno, Mali, PowerVR and Samsung Xclipse all run it. Every current flagship line — Pixel 6 through 10, Galaxy S23 through S26, the foldables — plus a decent spread of mid-range.<br />
<br />
<span style="font-weight: bold;"><span style="font-size: medium;">Phones it FAILS on</span></span><br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Mali-G52 MC2&nbsp;&nbsp; Galaxy A06, moto g05&nbsp;&nbsp; driver 49.1.0<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; no descriptor indexing — 11 validation errors</code></div></div>
<br />
Two devices, one GPU class. The engine's materials are bindless, so this is a real product decision — a non-bindless fallback path — not a bug.<br />
<br />
<span style="font-weight: bold;"><span style="font-size: medium;">Phones BELOW the baseline (Vulkan 1.1 — cannot run it)</span></span><br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>GPU&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Devices<br />
------------------------------------------------------------------------<br />
Adreno 730&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Galaxy S22, S22+, S22 Ultra, S23 FE, Tab S8,<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Tab S8 Ultra, Z Flip4, Z Fold4, OnePlus 10T<br />
Adreno 619&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Galaxy A52, Tab A9+, Tab S7 FE, OnePlus Nord CE 2/3<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Lite, Xperia 10 V, moto g34, CPH2195<br />
Adreno 660&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Galaxy S21, S21+, S21 FE, S21 Ultra<br />
Adreno 650&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Galaxy Note20, Note20 Ultra, S20 FE, S20 Ultra, Tab S7<br />
Adreno 610&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Galaxy A05s, Tab A7, Xperia 10 II, moto g(30),<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;moto g(9) play, moto g play (2021 / 2024)<br />
Mali-G57 MC2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Galaxy A14, A15, A16, A79, moto g (2022)<br />
Mali-G78&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Galaxy S21 Ultra, Pixel 6, Pixel 6a<br />
PowerVR Rogue GE8320&nbsp;&nbsp;Galaxy A03s, A12, A21<br />
Adreno 642L&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Galaxy A52s, motorola edge 30<br />
Adreno 710&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Xperia 10 VI, moto g96<br />
Mali-G52 / G52 MC2&nbsp;&nbsp;&nbsp;&nbsp;Galaxy Tab A8, Galaxy A32, Motorola G20,<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;moto g stylus (2023)<br />
Mali-G68&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Galaxy A25, Galaxy A53<br />
Mali-G610 MC3&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; motorola edge 40 neo<br />
Mali-G77 MC9&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;motorola edge 40<br />
Mali-G72&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Galaxy XCover Pro<br />
PowerVR BXM-8-256&nbsp;&nbsp;&nbsp;&nbsp; moto g54<br />
Adreno 620 / 630&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Pixel 5, Pixel 3<br />
Adreno 506&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;AQUOS sense2 SH-01L</code></div></div>
<br />
Read that list carefully, because it is stranger than it looks: <span style="font-weight: bold;">several phones appear in BOTH tables.</span> Pixel 6. Galaxy A15, A16. Galaxy S23 FE, Galaxy S24. Same model name, opposite verdict.<br />
<br />
There are two separate reasons for that, and they are worth keeping apart.<br />
<br />
<span style="font-weight: bold;">1. Regional silicon.</span> Galaxy S23 FE and S24 ship a Snapdragon in some markets and an Exynos in others — Adreno 730 vs Xclipse 920, Adreno 750 vs Xclipse 940. Those are genuinely different GPUs behind one marketing name. Nothing surprising, but it does mean "does the S24 work?" has no single answer.<br />
<br />
<span style="font-weight: bold;">2. The driver. This is the real finding.</span><br />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">The baseline is a DRIVER threshold, not hardware</span></span><br />
<br />
Pixel 6, Galaxy A15/A16 and the Mali-G68 A-series are not regional variants. They are the <span style="font-style: italic;">same silicon</span>, passing or failing purely on which driver version the device shipped with:<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>GPU&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PASSES on driver&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;FAILS on driver<br />
--------------------------------------------------------------------<br />
Adreno 710&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;512.800.35 / 512.800.48&nbsp;&nbsp; 512.615.0 / 512.615.88<br />
Mali-G57 MC2&nbsp;&nbsp;&nbsp;&nbsp;38.1.0&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;32.1.0<br />
Mali-G68&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;38.1.0&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;32.1.0<br />
Mali-G78&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;38.1.0&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;32.1.0 / 38.0.0</code></div></div>
<br />
The threshold is <span style="font-weight: bold;">Mali ≥38</span> and <span style="font-weight: bold;">Adreno ≥512.800</span>. A Pixel 6 on Mali driver 38.1.0 runs the engine; a Pixel 6 on 32.1.0 cannot. Nothing about the phone changed but the driver it shipped with.<br />
<br />
Below the threshold the device does not expose Vulkan 1.3 — and crucially, <span style="font-weight: bold;">not one below-baseline device exposes <span style="font-family: monospace;">dynamic_rendering</span> even as an extension</span>. So supporting them means writing a second render path, not a shim.<br />
<br />
And it is not simply a matter of waiting for old phones to age out. I have Galaxy S22s running a current <span style="font-weight: bold;">Android 16</span> that still ship an Adreno 730 driver reporting Vulkan 1.1. The OS is new; the GPU driver is not.<br />
<br />
<span style="font-weight: bold;">The interesting finding is not in the failure column.</span> Look at the BC column in the first table: <span style="font-weight: bold;">60 of the 94 devices that DO run us have no BC texture support</span> — every Mali, every PowerVR, every Xclipse. The engine's own <span style="font-family: monospace;">Engine.pak</span> still ships BC7, so on <span style="font-weight: bold;">64% of our supported devices</span> every texture is silently expanded to RGBA at load. Several times the VRAM. Nothing looks wrong on screen. Only a capability probe can see it, and it is now the biggest open item on the Android side.<br />
<br />
Also worth naming: a mid-range Mali-G68 (Galaxy A54) runs the demo at <span style="font-weight: bold;">25 fps</span> even on the mobile preset. If that class of device is in scope, it needs its own settings tier.<br />
<br />
One caveat on all of the above: this is <span style="font-weight: bold;">the test farm's catalogue, not the market</span>. It over-represents Pixels and Samsungs and whatever Google chose to stock. Read it as a driver/GPU survey, not a market-share estimate.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">The farm's PASS column lies</span></span><br />
<br />
A warning for anyone doing this: a device-farm "pass" on a game-loop test means <span style="font-style: italic;">the activity finished</span>. Nothing more.<br />
<br />
An app that fails to load its data pak, exits immediately, and renders <span style="font-weight: bold;">absolutely nothing</span> also "passes". That happened here, and I believed it for a while — the pak sat at a path that scoped storage made unreadable, the engine logged "Can't load Pak", quit, drew nothing, and the farm reported <span style="font-weight: bold;">Passed</span> across the board.<br />
<br />
So every verdict in the tables above comes from the <span style="font-weight: bold;">device's own logcat</span> — a capability line the probe prints <span style="font-style: italic;">before</span> any bail-out, so that even hardware we refuse to run on still yields a full data row instead of an empty log. That is why the below-baseline devices have driver versions at all.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">Summary</span></span><br />
<ul>
<li><span style="font-weight: bold;">Same Vulkan backend as PC</span> — no GLES, no mobile fork, no separate shaders</li>
<li><span style="font-weight: bold;">94 device/OS combos run it clean</span> across Adreno, Mali, PowerVR and Xclipse — 0 validation errors</li>
<li><span style="font-weight: bold;">The baseline is a driver threshold, not hardware</span> — the same GPU passes on a newer driver</li>
<li><span style="font-weight: bold;">47 -&gt; 72 fps</span> from mobile render settings — and the EASU trap is now measured on four GPUs, not one</li>
<li><span style="font-weight: bold;">Touch, rotation, resume, audio, ASTC/EAC, RmlUi HUD</span>, playable character in a streamed world</li>
<li><span style="font-weight: bold;">Five latent engine bugs</span> found and fixed — all of which desktop had been hiding<br />
</li></ul>
<br />
Open: the <span style="font-family: monospace;">Engine.pak</span> BC7 transcode (64% of supported devices are silently decompressing textures), and a settings tier for mid-range Mali.<br />
<br />
<br />
More info about the device farm, it was Google Cloud's, all of the scripts and apps needed to run the device farm tests are included in the repo:<br />
<br />
<br />
The scripts<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>scripts/android_gpu_matrix.sh&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;one device per GPU architecture — does it render correctly?<br />
scripts/android_perf_matrix.sh&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 4 render presets per device (the EASU harness) — NEW<br />
scripts/android_survey_resume.sh&nbsp;&nbsp;&nbsp;&nbsp; capability survey, idempotent, launches async<br />
scripts/android_survey_collect.sh&nbsp;&nbsp;&nbsp;&nbsp;pulls the logcats, prints the report<br />
scripts/android_survey_report.py&nbsp;&nbsp;&nbsp;&nbsp; summary + per-device table + survey.json<br />
scripts/android_tutorial_sweep.sh&nbsp;&nbsp;&nbsp;&nbsp;37 tutorials on-device with the validation layer<br />
scripts/android_publish_pak.sh&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; the ASTC/EAC Project.pak<br />
scripts/android_thirdparty_build.sh&nbsp;&nbsp;one-time arm64 cross-build<br />
scripts/android_game_demo.sh&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; build + deploy the game demo<br />
scripts/android_fetch_validation_layer.sh</code></div></div>
<br />
The apps they ship to the devices<br />
<br />
AndroidProbe.cpp (capability probe), AndroidPerf.cpp (the perf sweep I added), AndroidSweep.cpp (tutorial sweep), AndroidGame.cpp (the demo) — all tracked, all with CMake targets.<br />
<br />
in this fork:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></description>
			<content:encoded><![CDATA[<a href="https://postimg.cc/hQvRwTrJ" target="_blank"><img src="https://i.postimg.cc/d3mQh9Z9/android-world-1.png" border="0" alt="[Image: android-world-1.png]" /></a><br />
<br />
<a href="https://postimg.cc/N2FvVm44" target="_blank"><img src="https://i.postimg.cc/J0c7sQH9/android-world-moving.png" border="0" alt="[Image: android-world-moving.png]" /></a><br />
<br />
<br />
<br />
AI summary:<br />
<br />
The engine now builds and runs on Android arm64, using <span style="font-weight: bold;">the exact same Vulkan 1.3 backend as the PC path</span>. There is no GLES renderer, no cut-down mobile fork, and no separate shader set. The renderer that draws your desktop frame is the renderer that draws the phone's.<br />
<br />
It renders, takes touch input, handles rotation, survives being backgrounded, plays positional audio, loads mobile-compressed textures, and composites an HTML/CSS HUD over a streamed 3D world with a playable character.<br />
<br />
And rather than claim "it works on Android", I ran it on a device farm. <span style="font-weight: bold;">176 device/OS combinations</span> on real hardware. The full results — including everything that fails, and why — are in this post.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">What's in it</span></span><br />
<ul>
<li><span style="font-weight: bold;">Vulkan 1.3 + descriptor indexing (bindless)</span> as the baseline — the same requirement as PC. WSI via <span style="font-family: monospace;">vkCreateAndroidSurfaceKHR</span>.</li>
<li><span style="font-weight: bold;">Touch input</span>, including a virtual joystick (left thumb moves, right thumb turns).</li>
<li><span style="font-weight: bold;">Display rotation</span> and <span style="font-weight: bold;">resume from background</span>.</li>
<li><span style="font-weight: bold;">Positional audio</span> — openal-soft is now the Android sound provider, so the whole environmental audio pillar (EFX reverb, HRTF, occlusion) lights up on a phone.</li>
<li><span style="font-weight: bold;">ASTC / EAC textures</span> — proper mobile formats, with a headless publish step: <span style="font-family: monospace;">TitanEditor --publish=android</span>.</li>
<li><span style="font-weight: bold;">RmlUi</span> — the HTML/CSS UI stack cross-compiles for Android and is fully interactive by touch.</li>
<li><span style="font-weight: bold;">A real game demo</span> — streamed Game::World, textured terrain, a playable character, RmlUi HUD, in landscape.<br />
</li></ul>
<br />
The deliverable is a shared <span style="font-family: monospace;">libEsenthelProject.so</span> loaded by a NativeActivity subclass. Editor and tutorials are gated off for Android.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">The engine's render defaults are DESKTOP defaults</span></span><br />
<br />
This was the biggest perf lesson, and it will bite anyone porting: <span style="font-weight: bold;">the engine's defaults are desktop defaults, and on a phone they are ruinous.</span> Six shadow cascades at 2048², SSAO, and an FSR-style upscaler are all perfectly sensible on a GPU with a fan.<br />
<br />
Measured with GPU timestamps on an Adreno 740 at 2316x1080: <span style="font-weight: bold;">47 -&gt; 72 fps</span>, from three changes.<br />
<ul>
<li><span style="font-weight: bold;">Shadows</span> — 6 cascades @2048² down to 3 @1024². Light phase 6.8 -&gt; 3.9 ms.</li>
<li><span style="font-weight: bold;">Ambient</span> — SSAO down to AMBIENT_FLAT.</li>
<li><span style="font-weight: bold;">Density</span> — 75% render scale. Opaque 4.9 -&gt; 3.2 ms, light 3.9 -&gt; 2.4 ms.<br />
</li></ul>
<br />
<span style="font-weight: bold;">And one genuine trap.</span> The default density filter is <span style="font-family: monospace;">FILTER_EASU</span> (FSR-style upscaling), and it <span style="font-style: italic;">costs more than the pixels it saves</span>. Rendering at 75% with EASU (49 fps) was <span style="font-weight: bold;">slower</span> than rendering at 100% and not upscaling at all (55 fps). Pair any density change with <span style="font-family: monospace;">FILTER_LINEAR</span>, or you will conclude — wrongly — that resolution scaling does nothing.<br />
<br />
I did not trust that. It was one GPU.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">So I measured it on four</span></span><br />
<br />
"EASU is a trap" is a claim about one GPU's ALU-to-bandwidth balance. Generalising that into a shipping default without checking is exactly the kind of thing that quietly embarrasses you later. So: same world, same fixed camera, same frame counts, four render presets, swept <span style="font-style: italic;">inside a single run</span> on each device.<br />
<br />
<span style="font-weight: bold;">GPU milliseconds. "Shipped" is 75% + LINEAR. "+EASU" changes ONLY the filter, so the difference between those two columns is the cost of EASU and nothing else.</span><br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>GPU&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; device&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;shipped&nbsp;&nbsp; +EASU&nbsp;&nbsp; native(100%)&nbsp;&nbsp; EASU costs<br />
-----------------------------------------------------------------------------<br />
Adreno 740&nbsp;&nbsp;&nbsp;&nbsp;S23 Ultra&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 5.04&nbsp;&nbsp;&nbsp;&nbsp;11.32&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 8.95&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;+5.5 ms<br />
Adreno 750&nbsp;&nbsp;&nbsp;&nbsp;Galaxy S24&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;3.36&nbsp;&nbsp;&nbsp;&nbsp; 5.73&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 4.89&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;+2.5 ms<br />
Mali-G710&nbsp;&nbsp;&nbsp;&nbsp; Pixel 7 Pro&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 6.11&nbsp;&nbsp;&nbsp;&nbsp;12.53&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 8.67&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;+6.0 ms<br />
Mali-G68&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Galaxy A54&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 18.97&nbsp;&nbsp;&nbsp;&nbsp;32.95&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;29.79&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; +15.1 ms</code></div></div>
<br />
<span style="font-weight: bold;">The trap is universal, and far worse on Mali than on the Adreno where I found it.</span> On every GPU measured, upscaling and then running EASU costs more GPU time than simply rendering every pixel. A Mali-G68 pays <span style="font-weight: bold;">+15.1 ms</span> — on a device whose entire 60 fps budget is 16.7 ms. Generalising the Adreno figure would have understated the problem by 3x.<br />
<br />
The reassuring half: the shipped default (75% + LINEAR) is the fastest preset on all four GPUs. The setting was right. The evidence for it was thin, and now it isn't.<br />
<br />
<span style="font-weight: bold;">Read GPU milliseconds, not fps.</span> The Adreno 750 sits at ~70 fps under <span style="font-style: italic;">every</span> preset while its GPU time swings 3.4 -&gt; 6.4 ms. It is panel/CPU-capped, so fps there measures the ceiling, not the render cost. Benchmarking a phone by frame rate alone measures the screen.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">The device matrix — 176 combos on real hardware</span></span><br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Runs&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 94&nbsp;&nbsp;&nbsp;&nbsp;Vulkan 1.3, bindless, 0 validation errors<br />
Below baseline&nbsp;&nbsp; 80&nbsp;&nbsp;&nbsp;&nbsp;Vulkan 1.1 — not ONE exposes dynamic_rendering<br />
Fails&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 2&nbsp;&nbsp;&nbsp;&nbsp;Mali-G52 MC2 — no descriptor indexing<br />
Measured&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;176 / 199 catalogue combos</code></div></div>
<br />
<span style="font-weight: bold;"><span style="font-size: medium;">Phones it RUNS on — 0 validation errors</span></span><br />
<br />
"fps" is filled in only where I ran the perf sweep; it is a property of the GPU, not the phone. "BC" is whether the GPU can read the engine's BC7 textures natively — see further down, this turns out to matter more than the failure column.<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>GPU&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;fps&nbsp;&nbsp; BC&nbsp;&nbsp; Devices<br />
------------------------------------------------------------------------<br />
Mali-G715&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; —&nbsp;&nbsp;&nbsp;&nbsp;no&nbsp;&nbsp; Pixel 8, 8 Pro, 8a, 9, 9 Pro, 9 Pro XL,<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;9 Pro Fold, 9a, 10a<br />
Adreno 740&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;88&nbsp;&nbsp; yes&nbsp;&nbsp;Galaxy S23, S23+, S23 Ultra, Tab S9,<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Z Flip5, Z Fold5, Xperia 1 V<br />
Adreno 750&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;69&nbsp;&nbsp; yes&nbsp;&nbsp;Galaxy S24, S24+, S24 Ultra,<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Z Flip6, Z Fold6<br />
Mali-G710&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 53&nbsp;&nbsp; no&nbsp;&nbsp; Pixel 7, 7 Pro, 7a, Pixel Fold, Pixel Tablet<br />
Mali-G68&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;25&nbsp;&nbsp; no&nbsp;&nbsp; Galaxy A16, A26, A35, A54, Tab S9 FE<br />
Adreno 830&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;—&nbsp;&nbsp;&nbsp;&nbsp;yes&nbsp;&nbsp;Galaxy S25, S25+, S25 Edge, S25 Ultra,<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Z TriFold<br />
PowerVR DXT-48-1536&nbsp;&nbsp;&nbsp;&nbsp; —&nbsp;&nbsp;&nbsp;&nbsp;no&nbsp;&nbsp; Pixel 10, 10 Pro, 10 Pro XL, 10 Pro Fold<br />
Samsung Xclipse 940&nbsp;&nbsp;&nbsp;&nbsp; —&nbsp;&nbsp;&nbsp;&nbsp;no&nbsp;&nbsp; Galaxy S24, S24+, S24 FE&nbsp;&nbsp; (Exynos variants)<br />
Mali-G57 MC2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;—&nbsp;&nbsp;&nbsp;&nbsp;no&nbsp;&nbsp; Galaxy A15, XCover7, moto g (2025)<br />
Adreno 840&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;—&nbsp;&nbsp;&nbsp;&nbsp;yes&nbsp;&nbsp;Galaxy S26, S26+<br />
Adreno 710&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;—&nbsp;&nbsp;&nbsp;&nbsp;yes&nbsp;&nbsp;Galaxy A36<br />
Mali-G68 MC4&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;—&nbsp;&nbsp;&nbsp;&nbsp;no&nbsp;&nbsp; Galaxy A34, Lenovo Tab P12<br />
Mali-G57&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;—&nbsp;&nbsp;&nbsp;&nbsp;no&nbsp;&nbsp; moto g04, realme C53<br />
Samsung Xclipse 540&nbsp;&nbsp;&nbsp;&nbsp; —&nbsp;&nbsp;&nbsp;&nbsp;no&nbsp;&nbsp; Galaxy A56<br />
Adreno 735&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;—&nbsp;&nbsp;&nbsp;&nbsp;yes&nbsp;&nbsp;motorola razr plus 2024<br />
Mali-G720 Immortalis&nbsp;&nbsp;&nbsp;&nbsp;—&nbsp;&nbsp;&nbsp;&nbsp;no&nbsp;&nbsp; Galaxy Tab S10+<br />
Samsung Xclipse 530&nbsp;&nbsp;&nbsp;&nbsp; —&nbsp;&nbsp;&nbsp;&nbsp;no&nbsp;&nbsp; Galaxy A55<br />
Samsung Xclipse 920&nbsp;&nbsp;&nbsp;&nbsp; —&nbsp;&nbsp;&nbsp;&nbsp;no&nbsp;&nbsp; Galaxy S23 FE&nbsp;&nbsp;(Exynos variant)<br />
Mali-G78&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;—&nbsp;&nbsp;&nbsp;&nbsp;no&nbsp;&nbsp; Pixel 6</code></div></div>
<br />
Adreno, Mali, PowerVR and Samsung Xclipse all run it. Every current flagship line — Pixel 6 through 10, Galaxy S23 through S26, the foldables — plus a decent spread of mid-range.<br />
<br />
<span style="font-weight: bold;"><span style="font-size: medium;">Phones it FAILS on</span></span><br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Mali-G52 MC2&nbsp;&nbsp; Galaxy A06, moto g05&nbsp;&nbsp; driver 49.1.0<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; no descriptor indexing — 11 validation errors</code></div></div>
<br />
Two devices, one GPU class. The engine's materials are bindless, so this is a real product decision — a non-bindless fallback path — not a bug.<br />
<br />
<span style="font-weight: bold;"><span style="font-size: medium;">Phones BELOW the baseline (Vulkan 1.1 — cannot run it)</span></span><br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>GPU&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Devices<br />
------------------------------------------------------------------------<br />
Adreno 730&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Galaxy S22, S22+, S22 Ultra, S23 FE, Tab S8,<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Tab S8 Ultra, Z Flip4, Z Fold4, OnePlus 10T<br />
Adreno 619&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Galaxy A52, Tab A9+, Tab S7 FE, OnePlus Nord CE 2/3<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Lite, Xperia 10 V, moto g34, CPH2195<br />
Adreno 660&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Galaxy S21, S21+, S21 FE, S21 Ultra<br />
Adreno 650&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Galaxy Note20, Note20 Ultra, S20 FE, S20 Ultra, Tab S7<br />
Adreno 610&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Galaxy A05s, Tab A7, Xperia 10 II, moto g(30),<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;moto g(9) play, moto g play (2021 / 2024)<br />
Mali-G57 MC2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Galaxy A14, A15, A16, A79, moto g (2022)<br />
Mali-G78&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Galaxy S21 Ultra, Pixel 6, Pixel 6a<br />
PowerVR Rogue GE8320&nbsp;&nbsp;Galaxy A03s, A12, A21<br />
Adreno 642L&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Galaxy A52s, motorola edge 30<br />
Adreno 710&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Xperia 10 VI, moto g96<br />
Mali-G52 / G52 MC2&nbsp;&nbsp;&nbsp;&nbsp;Galaxy Tab A8, Galaxy A32, Motorola G20,<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;moto g stylus (2023)<br />
Mali-G68&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Galaxy A25, Galaxy A53<br />
Mali-G610 MC3&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; motorola edge 40 neo<br />
Mali-G77 MC9&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;motorola edge 40<br />
Mali-G72&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Galaxy XCover Pro<br />
PowerVR BXM-8-256&nbsp;&nbsp;&nbsp;&nbsp; moto g54<br />
Adreno 620 / 630&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Pixel 5, Pixel 3<br />
Adreno 506&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;AQUOS sense2 SH-01L</code></div></div>
<br />
Read that list carefully, because it is stranger than it looks: <span style="font-weight: bold;">several phones appear in BOTH tables.</span> Pixel 6. Galaxy A15, A16. Galaxy S23 FE, Galaxy S24. Same model name, opposite verdict.<br />
<br />
There are two separate reasons for that, and they are worth keeping apart.<br />
<br />
<span style="font-weight: bold;">1. Regional silicon.</span> Galaxy S23 FE and S24 ship a Snapdragon in some markets and an Exynos in others — Adreno 730 vs Xclipse 920, Adreno 750 vs Xclipse 940. Those are genuinely different GPUs behind one marketing name. Nothing surprising, but it does mean "does the S24 work?" has no single answer.<br />
<br />
<span style="font-weight: bold;">2. The driver. This is the real finding.</span><br />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">The baseline is a DRIVER threshold, not hardware</span></span><br />
<br />
Pixel 6, Galaxy A15/A16 and the Mali-G68 A-series are not regional variants. They are the <span style="font-style: italic;">same silicon</span>, passing or failing purely on which driver version the device shipped with:<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>GPU&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PASSES on driver&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;FAILS on driver<br />
--------------------------------------------------------------------<br />
Adreno 710&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;512.800.35 / 512.800.48&nbsp;&nbsp; 512.615.0 / 512.615.88<br />
Mali-G57 MC2&nbsp;&nbsp;&nbsp;&nbsp;38.1.0&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;32.1.0<br />
Mali-G68&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;38.1.0&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;32.1.0<br />
Mali-G78&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;38.1.0&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;32.1.0 / 38.0.0</code></div></div>
<br />
The threshold is <span style="font-weight: bold;">Mali ≥38</span> and <span style="font-weight: bold;">Adreno ≥512.800</span>. A Pixel 6 on Mali driver 38.1.0 runs the engine; a Pixel 6 on 32.1.0 cannot. Nothing about the phone changed but the driver it shipped with.<br />
<br />
Below the threshold the device does not expose Vulkan 1.3 — and crucially, <span style="font-weight: bold;">not one below-baseline device exposes <span style="font-family: monospace;">dynamic_rendering</span> even as an extension</span>. So supporting them means writing a second render path, not a shim.<br />
<br />
And it is not simply a matter of waiting for old phones to age out. I have Galaxy S22s running a current <span style="font-weight: bold;">Android 16</span> that still ship an Adreno 730 driver reporting Vulkan 1.1. The OS is new; the GPU driver is not.<br />
<br />
<span style="font-weight: bold;">The interesting finding is not in the failure column.</span> Look at the BC column in the first table: <span style="font-weight: bold;">60 of the 94 devices that DO run us have no BC texture support</span> — every Mali, every PowerVR, every Xclipse. The engine's own <span style="font-family: monospace;">Engine.pak</span> still ships BC7, so on <span style="font-weight: bold;">64% of our supported devices</span> every texture is silently expanded to RGBA at load. Several times the VRAM. Nothing looks wrong on screen. Only a capability probe can see it, and it is now the biggest open item on the Android side.<br />
<br />
Also worth naming: a mid-range Mali-G68 (Galaxy A54) runs the demo at <span style="font-weight: bold;">25 fps</span> even on the mobile preset. If that class of device is in scope, it needs its own settings tier.<br />
<br />
One caveat on all of the above: this is <span style="font-weight: bold;">the test farm's catalogue, not the market</span>. It over-represents Pixels and Samsungs and whatever Google chose to stock. Read it as a driver/GPU survey, not a market-share estimate.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">The farm's PASS column lies</span></span><br />
<br />
A warning for anyone doing this: a device-farm "pass" on a game-loop test means <span style="font-style: italic;">the activity finished</span>. Nothing more.<br />
<br />
An app that fails to load its data pak, exits immediately, and renders <span style="font-weight: bold;">absolutely nothing</span> also "passes". That happened here, and I believed it for a while — the pak sat at a path that scoped storage made unreadable, the engine logged "Can't load Pak", quit, drew nothing, and the farm reported <span style="font-weight: bold;">Passed</span> across the board.<br />
<br />
So every verdict in the tables above comes from the <span style="font-weight: bold;">device's own logcat</span> — a capability line the probe prints <span style="font-style: italic;">before</span> any bail-out, so that even hardware we refuse to run on still yields a full data row instead of an empty log. That is why the below-baseline devices have driver versions at all.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">Summary</span></span><br />
<ul>
<li><span style="font-weight: bold;">Same Vulkan backend as PC</span> — no GLES, no mobile fork, no separate shaders</li>
<li><span style="font-weight: bold;">94 device/OS combos run it clean</span> across Adreno, Mali, PowerVR and Xclipse — 0 validation errors</li>
<li><span style="font-weight: bold;">The baseline is a driver threshold, not hardware</span> — the same GPU passes on a newer driver</li>
<li><span style="font-weight: bold;">47 -&gt; 72 fps</span> from mobile render settings — and the EASU trap is now measured on four GPUs, not one</li>
<li><span style="font-weight: bold;">Touch, rotation, resume, audio, ASTC/EAC, RmlUi HUD</span>, playable character in a streamed world</li>
<li><span style="font-weight: bold;">Five latent engine bugs</span> found and fixed — all of which desktop had been hiding<br />
</li></ul>
<br />
Open: the <span style="font-family: monospace;">Engine.pak</span> BC7 transcode (64% of supported devices are silently decompressing textures), and a settings tier for mid-range Mali.<br />
<br />
<br />
More info about the device farm, it was Google Cloud's, all of the scripts and apps needed to run the device farm tests are included in the repo:<br />
<br />
<br />
The scripts<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>scripts/android_gpu_matrix.sh&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;one device per GPU architecture — does it render correctly?<br />
scripts/android_perf_matrix.sh&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 4 render presets per device (the EASU harness) — NEW<br />
scripts/android_survey_resume.sh&nbsp;&nbsp;&nbsp;&nbsp; capability survey, idempotent, launches async<br />
scripts/android_survey_collect.sh&nbsp;&nbsp;&nbsp;&nbsp;pulls the logcats, prints the report<br />
scripts/android_survey_report.py&nbsp;&nbsp;&nbsp;&nbsp; summary + per-device table + survey.json<br />
scripts/android_tutorial_sweep.sh&nbsp;&nbsp;&nbsp;&nbsp;37 tutorials on-device with the validation layer<br />
scripts/android_publish_pak.sh&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; the ASTC/EAC Project.pak<br />
scripts/android_thirdparty_build.sh&nbsp;&nbsp;one-time arm64 cross-build<br />
scripts/android_game_demo.sh&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; build + deploy the game demo<br />
scripts/android_fetch_validation_layer.sh</code></div></div>
<br />
The apps they ship to the devices<br />
<br />
AndroidProbe.cpp (capability probe), AndroidPerf.cpp (the perf sweep I added), AndroidSweep.cpp (tutorial sweep), AndroidGame.cpp (the demo) — all tracked, all with CMake targets.<br />
<br />
in this fork:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[bindless materials, GPU-driven submission, environmental audio, VRAM management]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11835</link>
			<pubDate>Tue, 14 Jul 2026 15:41:03 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11835</guid>
			<description><![CDATA[No visible things to show, because the biggest feature add was an improved audio system, and the now the engine fork uses OpenAL on all platforms. Major architectural/perf improvements to the Vulkan renderer.<br />
<br />
AI written summary:<br />
<br />
It is less photogenic, but this is the part that actually raises the engine's ceiling: <span style="font-weight: bold;">bindless material descriptors</span>, <span style="font-weight: bold;">GPU-driven draw submission</span>, a <span style="font-weight: bold;">texture residency manager</span>, <span style="font-weight: bold;">compute skinning</span>, <span style="font-weight: bold;">static shadow caching</span>, a <span style="font-weight: bold;">9.4x faster shader build</span>, and a complete <span style="font-weight: bold;">environmental audio stack</span>.<br />
<br />
Everything below is opt-in and Vulkan-only unless noted. All numbers measured on an AMD Radeon RX 6650 XT.<br />
<br />
Two of these were the direct result of an architecture audit: the engine was a DX11-shaped renderer on a modern Vulkan core, missing three standard pillars — bindless descriptors, GPU-driven submission, and parallel command recording. The first two are now in. The third turned out not to be needed, and I will explain why, because that is more interesting than shipping it would have been.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">1. Bindless material textures</span></span><br />
<br />
Previously every material switch re-allocated and rewrote a descriptor set. With many unique materials on screen this dominates: descriptor churn, and pool pressure up to 16384 sets per frame.<br />
<br />
Material textures now live in a <span style="font-weight: bold;">global set-1 texture table</span> (descriptor indexing, UPDATE_AFTER_BIND, 65536 slots, fence-gated slot recycling), indexed by a small per-material constant. A macro swap in the shader header re-routes every Col/Nrm/Ext/Det/Lum sample, so no shader needed hand-editing — the regenerated pak's set-0 layouts simply contain no material textures at all.<br />
<br />
That last detail matters: the churn is gone <span style="font-style: italic;">structurally</span>. There is no runtime flag to forget to enable.<br />
<br />
<span style="font-weight: bold;">Measured, 512 unique materials:</span><ul>
<li>533 -&gt; 22 descriptor sets per frame</li>
<li>1.95 -&gt; 1.29 ms</li>
<li>+51% fps<br />
</li></ul>
Pixel-identical against the committed baseline; clean validation soak.<br />
<br />
Demo: <span style="font-style: italic;">Tutorial_17_MaterialStress</span><br />
<br />
Bindless is also the precondition for everything in section 2.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">2. GPU-driven rendering: compute culling + indirect draw</span></span><br />
<br />
The engine culled per object on the CPU, per light, with no occlusion culling at all, and recorded every draw on one thread. multiDrawIndirect was enabled but never actually issued.<br />
<br />
There is now a persistent GPU-instanced layer: register N instances once, and a compute kernel culls and compacts them into draw chunks that feed <span style="font-family: monospace;">vkCmdDrawIndexedIndirectCount</span>. Zero CPU per-instance work, using the mesh's existing shaders — <span style="font-weight: bold;">no new shaders and no pak regeneration</span>.<br />
<br />
What is in it:<ul>
<li><span style="font-weight: bold;">Frustum culling on GPU</span> — an exact port of the engine's own sphere-vs-frustum test, so results match the CPU path.</li>
<li><span style="font-weight: bold;">Hi-Z occlusion culling</span>, two-phase. The layer occludes <span style="font-style: italic;">itself</span>: standing inside a dense field with no external occluders, 13,074 -&gt; 67 survivors (99.5% culled), and disocclusions are caught in the same frame.</li>
<li><span style="font-weight: bold;">Per-cascade shadow culling</span> — each shadow cascade re-culls on the GPU and indirect-draws survivors.</li>
<li><span style="font-weight: bold;">Automatic world migration</span> — Game::Static and Game::Kinematic objects move onto the layer as they stream in, and off as they stream out. Meshes with LODs become per-LOD banded layers whose distance bands are an exact port of the CPU LOD selection, so the GPU path draws the same LOD the CPU path would, in every view, at every distance.<br />
</li></ul>
<br />
<span style="font-weight: bold;">Measured (262k instances):</span><ul>
<li>Frustum only: 4.24 -&gt; 1.85 ms (2.3x)</li>
<li>Full stack: 8.8 -&gt; 1.54 ms (<span style="font-weight: bold;">5.7x</span>)</li>
<li>CPU command recording: 26.6 -&gt; 1.5 ms<br />
</li></ul>
Pixel-identical A/Bs — zero false culls.<br />
<br />
Demos: <span style="font-style: italic;">Tutorial_17_GpuCull</span>, and <span style="font-style: italic;">Tutorial_14_GpuWorld</span> (a real streamed world with thousands of statics flying in and out of range).<br />
<br />
<span style="font-weight: bold;">An engine-wide bug this uncovered:</span> under CSM load the per-draw uniform ring silently wrapped mid-frame (65k casters x 4 cascades is roughly 10x the ring budget), corrupting already-bound slots. That was a latent bug for anyone with heavy shadow scenes, not just the new path. The ring now grows on overflow.<br />
<br />
<span style="font-weight: bold;">And the honest part.</span> The win scales with density. At ~29k cheap statics with shadows on, the CPU path is still ahead (6.8 vs 7.8 ms) — the per-cascade dispatch rounds are a fixed cost that needs volume to amortise. This is a scalability feature, not a free win in every scene, which is why it stays opt-in.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">3. The pillar I did not ship</span></span><br />
<br />
The third missing pillar was parallel command recording — record the G-buffer and each shadow cascade on separate threads into secondary command buffers. It is a standard modern design and it was on the roadmap.<br />
<br />
I measured before writing it. The GPU-driven layer had already taken single-threaded CPU recording from 26.6 ms to 1.5 ms. What remains is serial pipeline overhead, not parallelisable draw loops — so a week-scale threading refactor (per-thread secondary CBs, per-context pools, a thread-safe uniform ring) would buy under 1 ms on frames that are GPU-bound anyway.<br />
<br />
So it is closed, not done, and I would rather say that plainly than ship a feature to tick a box. Same outcome for a per-frame arena allocator: an LD_PRELOAD counting shim showed steady-state allocation is under 10 mallocs per frame, so there was nothing to win.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">4. Environmental audio — the immersion layer</span></span><br />
<br />
This is the change I am happiest with: the engine had solid audio plumbing (OpenAL Soft, Opus/FLAC, real voice virtualization with priority management) and <span style="font-weight: bold;">no immersion layer whatsoever</span>. No reverb. No HRTF. No occlusion. Every space — cathedral, cupboard, open field — sounded identical and anechoic.<br />
<br />
OpenAL Soft ships EFX and HRTF for free. The engine simply never asked for them.<br />
<br />
Now shipped, <span style="font-weight: bold;">with zero new dependencies</span>:<br />
<ul>
<li><span style="font-weight: bold;">Reverb (EFX)</span> — <span style="font-family: monospace;">SND_REVERB</span>, a 26-preset EAX vocabulary. Live 3D sources attach a reverb send automatically; 2D/UI/music stay dry by construction.</li>
<li><span style="font-weight: bold;">Reverb zones</span> — Ball/Box volumes, innermost wins, with a classic two-slot equal-power crossfade. Walking through a doorway fades between spaces smoothly, and retargeting mid-fade stays smooth.</li>
<li><span style="font-weight: bold;">Occlusion and obstruction</span> — physics-raycast, using the Wwise-style split: a wall between you and the source muffles the dry path (obstruction), and full occlusion escalates into the reverb sends too. Shoulder-offset rays, round-robin throttled, smoothed.</li>
<li><span style="font-weight: bold;">HRTF + doppler</span> — proper binaural positioning on headphones.</li>
<li><span style="font-weight: bold;">Decoded-PCM sample bank</span> — cached PCM served through the existing reader, so looping, seeking, muffling and virtualization all keep working while per-play decode cost drops to zero.<br />
</li></ul>
<br />
Demo: <span style="font-style: italic;">Tutorial_09_EnvironmentalAudio</span> — a stone room, a doorway, and an open field, with <span style="font-weight: bold;">R</span>/<span style="font-weight: bold;">O</span>/<span style="font-weight: bold;">H</span>/<span style="font-weight: bold;">B</span> toggling reverb, occlusion, HRTF and the sample bank so you can hear each one switch in and out.<br />
<br />
<span style="font-weight: bold;">OpenAL across platforms.</span> The environmental stack is OpenAL-only, and Windows historically shipped XAudio/DirectSound. Rather than port the whole thing to XAudio2 XAPO effects, Windows now moves to openal-soft as well — the headers and Win64 import library were already vendored in-repo, so it is a define flip plus the DLL. One sound provider, one environmental stack, everywhere. (Landed; still wants validation on a Windows box, and there is a one-line rollback if it misbehaves.)<br />
<br />
Design doc: <span style="font-family: monospace;">docs/audio-design.md</span><br />
<br />
<span style="font-weight: bold;">One thing I deliberately did not build:</span> ray-traced audio occlusion against the RT acceleration structure. It sounds great in a feature list, and I have the TLAS sitting right there. But collision proxies are <span style="font-style: italic;">acoustically better</span> occluders than render triangles — sound wavelengths are metre-scale and diffract around the detail geometry that a TLAS is full of — the ray counts involved do not need a GPU, and a GPU readback adds latency to a system that must react instantly. It was a marketing bullet, not audible value, so it is struck. Material-based transmission and portal propagation are the real follow-ons.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">5. Texture residency / VRAM budget</span></span><br />
<br />
Every referenced texture was promoted to full resolution and kept there — no residency, no budget, no eviction. Your working set was "every texture the world has ever touched, at full res", which is a hard ceiling for open worlds.<br />
<br />
<span style="font-family: monospace;">EE_TEX_BUDGET=&lt;MB&gt;</span> now enforces an LRU byte budget. Material textures timestamp themselves on bind (the bindless registration hook gives this for free), and a throttled manager demotes the least-recently-used textures one mip step under pressure, promoting them back once there is headroom, with 15% hysteresis so it cannot oscillate.<br />
<br />
The neat part: demote and promote are the <span style="font-style: italic;">same operation</span> — a reload through the engine's existing streaming loader with a shrink override. Small pak read, background stream, the standard hot swap. Deferred image destruction plus bindless view re-validation make live swaps safe by construction.<br />
<br />
Verified: budget off, or a huge budget, is pixel-identical to stock; a 1 MB budget demotes to fit exactly; raising the budget mid-run recovers to <span style="font-weight: bold;">pixel-perfect</span> full quality.<br />
<br />
<span style="font-weight: bold;">A bug worth knowing about if you write background systems:</span> <span style="font-family: monospace;">Time.frame()</span> freezes when the app is not drawing. Anything clocking off it silently stops in an unfocused or minimized window. The manager ticks from the update path instead, so streaming survives alt-tab.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">6. Compute skinning (the raster half)</span></span><br />
<br />
Last post covered GPU-skinned characters for ray tracing. That was half the story — the other half is that skinning was VS-only, so the same linear-blend skin was recomputed in <span style="font-style: italic;">every</span> pass: early-Z, opaque, and once per shadow cascade.<br />
<br />
With <span style="font-family: monospace;">EE_VK_GPU_SKIN=1</span>, a character deforms <span style="font-weight: bold;">once per frame</span> into a real vertex buffer, and every pass afterwards — G-buffer, mirrors, all shadow views, and the RT acceleration structure — draws that same deformed buffer. One deform serves raster and ray tracing.<br />
<br />
It also gets <span style="font-weight: bold;">exact per-limb motion vectors</span>: the kernel skins by the previous frame's pose into a second position channel, so TAA sees a limb's true motion instead of just the camera's.<br />
<br />
Honest number: about -2.4% GPU on an 800-character idle crowd. Vertex skinning is a smaller slice of the frame than you would guess — CPU animation update dominates that scene. The real value here is correctness (RT and motion vectors), not the frame time.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">7. Static shadow caching for local lights</span></span><br />
<br />
<span style="font-family: monospace;">D.localShadowCache(max_lights)</span>. Every shadowed light previously re-rasterised all its geometry every frame. Now each light keeps a cached static depth atlas and only re-renders dynamic objects per frame; lights with no dynamics bind the cache directly with zero copies. It is camera-motion-proof by construction, because it is all in light space.<br />
<br />
<span style="font-weight: bold;">Honest perf:</span> -22% at 512px shadow maps, roughly break-even at 1024, and it <span style="font-style: italic;">loses</span> at 2048 in a scene of cheap instanced barrels — copying a 96 MB depth atlas per light beats re-rendering geometry that was nearly free to begin with. The win condition is scenes where per-light static shadow rendering is genuinely expensive. It is opt-in for exactly that reason.<br />
<br />
(Chasing an apparent culling leak here turned out to be my own demo: point-light range grows with the square root of power, so those lights were ~90 m across and legitimately touched everything. Engine culling was correct. Emptiness is now decided by one cheap sphere query, so empty lights are truly free.)<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">8. Shader build: 2h11m -&gt; 13m56s</span></span><br />
<br />
Boolean shader dimensions (dither, gamma, alpha test, detail, material blend, UV scale, and Forward's nine light dims) were being baked into full permutations, so the pak universe — and the cold build — grew combinatorially.<br />
<br />
They are now <span style="font-weight: bold;">specialization constants</span>, resolved at pipeline creation. Two compiler-side changes turn a smaller pak into saved time: specialization values are excluded from the shader cache key, and identical compiles are deduplicated within a run.<br />
<ul>
<li>Cold Vulkan shader regen: <span style="font-weight: bold;">2h11m -&gt; 13m56s (9.4x)</span></li>
<li>Forward: 39,080 -&gt; 10,664 compiles</li>
<li>Engine.pak: -27 MB (Forward 76.8 -&gt; 37.1 MB, Deferred 29.8 -&gt; 14.4 MB, Blend Light 21.0 -&gt; 9.1 MB)</li>
<li>Output bit-identical; stock content pixel-identical<br />
</li></ul>
<br />
Part of that came from noticing the forward renderer is compiled out of the runtime entirely — so roughly 17 of the 33 remaining minutes were spent building shaders nothing could ever load. Now gated (and reversible), which also removed 108 MB of unloadable paks from the repo.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">9. Async compute, and a batch of small wins</span></span><br />
<br />
<span style="font-family: monospace;">EE_VK_ASYNC_COMPUTE=1</span> routes the GI ray-traced moments pass onto the dedicated compute queue — it reads the acceleration structure and writes an atlas consumed next frame, so it is genuinely async-clean. Two timelines (a shared one violates execution-order monotonicity), per-slot fences, concurrent sharing for cross-queue resources. About 0.09 ms/frame off the graphics queue, zero validation errors, pixel-parity.<br />
<br />
Also landed, each small and each measured:<ul>
<li><span style="font-weight: bold;">Static vertex/index buffers no longer take a dedicated allocation</span> — they suballocate through VMA. Every mesh used to burn one <span style="font-family: monospace;">vkAllocateMemory</span>, heading for the ~4096 allocation ceiling in a mesh-heavy world.</li>
<li><span style="font-weight: bold;">GI capture dispatches batched</span> — one 3D dispatch per pass instead of one micro-dispatch per probe with a full barrier between each.</li>
<li><span style="font-weight: bold;">Immutable samplers</span> — the five never-recreated samplers are baked into the layouts. The other four are settings-mutable, so baking them would dangle every layout on a settings change; they stay written.</li>
<li><span style="font-weight: bold;">Dynamic vertex ring 32 KB -&gt; 2 MB</span> — kills most mid-frame buffer orphaning.</li>
<li><span style="font-weight: bold;">PhysX TGS solver</span> — opt-in via <span style="font-family: monospace;">EE_PHYSX_TGS</span>, cheaper stability.<br />
</li></ul>
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">Correctness</span></span><br />
<br />
The whole stack went through an adversarial multi-agent review before merge — reviewers instructed to <span style="font-style: italic;">refute</span> each finding against the code rather than confirm it. It found 9 real defects: a use-after-free in the sound bank, two more memory bugs, a GPU-cull shadow-gate hole, a spec violation, and four feature-breaking logic errors — including one where the compute-skinning motion vectors <span style="font-style: italic;">never actually worked</span> and had been silently inert for standard meshes.<br />
<br />
Every one of them was behind an opt-in lever, so the default path never regressed. All fixed. A forced synchronization-validation soak reports zero hazards.<br />
<br />
I mention it because it is the honest counterweight to a features list: this many opt-in systems will hide broken paths unless something goes looking for them on purpose.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">Summary</span></span><br />
<ul>
<li><span style="font-weight: bold;">Bindless materials</span> — 533 -&gt; 22 descriptor sets/frame, +51% fps at 512 materials</li>
<li><span style="font-weight: bold;">GPU-driven rendering</span> — compute cull + Hi-Z + indirect draw, 8.8 -&gt; 1.54 ms at 262k instances</li>
<li><span style="font-weight: bold;">Environmental audio</span> — EFX reverb, zones, occlusion, HRTF, PCM bank; openal-soft on every platform</li>
<li><span style="font-weight: bold;">Texture residency</span> — <span style="font-family: monospace;">EE_TEX_BUDGET</span>, LRU mip demote/promote, pixel-perfect recovery</li>
<li><span style="font-weight: bold;">Compute skinning</span> — deform once per frame, exact per-limb motion vectors</li>
<li><span style="font-weight: bold;">Static shadow cache</span> — <span style="font-family: monospace;">D.localShadowCache</span></li>
<li><span style="font-weight: bold;">Spec constants</span> — cold shader build 9.4x faster, pak -27 MB</li>
<li><span style="font-weight: bold;">Async compute</span> — GI moments on the dedicated queue<br />
</li></ul>
<br />
Design docs are in the repo: <span style="font-family: monospace;">docs/audio-design.md</span>, <span style="font-family: monospace;">docs/clustered-froxel-lights-design.md</span>, <span style="font-family: monospace;">docs/rt-shadows-design.md</span>, <span style="font-family: monospace;">docs/rt-reflections-design.md</span>.<br />
<br />
<br />
In this engine fork:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></description>
			<content:encoded><![CDATA[No visible things to show, because the biggest feature add was an improved audio system, and the now the engine fork uses OpenAL on all platforms. Major architectural/perf improvements to the Vulkan renderer.<br />
<br />
AI written summary:<br />
<br />
It is less photogenic, but this is the part that actually raises the engine's ceiling: <span style="font-weight: bold;">bindless material descriptors</span>, <span style="font-weight: bold;">GPU-driven draw submission</span>, a <span style="font-weight: bold;">texture residency manager</span>, <span style="font-weight: bold;">compute skinning</span>, <span style="font-weight: bold;">static shadow caching</span>, a <span style="font-weight: bold;">9.4x faster shader build</span>, and a complete <span style="font-weight: bold;">environmental audio stack</span>.<br />
<br />
Everything below is opt-in and Vulkan-only unless noted. All numbers measured on an AMD Radeon RX 6650 XT.<br />
<br />
Two of these were the direct result of an architecture audit: the engine was a DX11-shaped renderer on a modern Vulkan core, missing three standard pillars — bindless descriptors, GPU-driven submission, and parallel command recording. The first two are now in. The third turned out not to be needed, and I will explain why, because that is more interesting than shipping it would have been.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">1. Bindless material textures</span></span><br />
<br />
Previously every material switch re-allocated and rewrote a descriptor set. With many unique materials on screen this dominates: descriptor churn, and pool pressure up to 16384 sets per frame.<br />
<br />
Material textures now live in a <span style="font-weight: bold;">global set-1 texture table</span> (descriptor indexing, UPDATE_AFTER_BIND, 65536 slots, fence-gated slot recycling), indexed by a small per-material constant. A macro swap in the shader header re-routes every Col/Nrm/Ext/Det/Lum sample, so no shader needed hand-editing — the regenerated pak's set-0 layouts simply contain no material textures at all.<br />
<br />
That last detail matters: the churn is gone <span style="font-style: italic;">structurally</span>. There is no runtime flag to forget to enable.<br />
<br />
<span style="font-weight: bold;">Measured, 512 unique materials:</span><ul>
<li>533 -&gt; 22 descriptor sets per frame</li>
<li>1.95 -&gt; 1.29 ms</li>
<li>+51% fps<br />
</li></ul>
Pixel-identical against the committed baseline; clean validation soak.<br />
<br />
Demo: <span style="font-style: italic;">Tutorial_17_MaterialStress</span><br />
<br />
Bindless is also the precondition for everything in section 2.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">2. GPU-driven rendering: compute culling + indirect draw</span></span><br />
<br />
The engine culled per object on the CPU, per light, with no occlusion culling at all, and recorded every draw on one thread. multiDrawIndirect was enabled but never actually issued.<br />
<br />
There is now a persistent GPU-instanced layer: register N instances once, and a compute kernel culls and compacts them into draw chunks that feed <span style="font-family: monospace;">vkCmdDrawIndexedIndirectCount</span>. Zero CPU per-instance work, using the mesh's existing shaders — <span style="font-weight: bold;">no new shaders and no pak regeneration</span>.<br />
<br />
What is in it:<ul>
<li><span style="font-weight: bold;">Frustum culling on GPU</span> — an exact port of the engine's own sphere-vs-frustum test, so results match the CPU path.</li>
<li><span style="font-weight: bold;">Hi-Z occlusion culling</span>, two-phase. The layer occludes <span style="font-style: italic;">itself</span>: standing inside a dense field with no external occluders, 13,074 -&gt; 67 survivors (99.5% culled), and disocclusions are caught in the same frame.</li>
<li><span style="font-weight: bold;">Per-cascade shadow culling</span> — each shadow cascade re-culls on the GPU and indirect-draws survivors.</li>
<li><span style="font-weight: bold;">Automatic world migration</span> — Game::Static and Game::Kinematic objects move onto the layer as they stream in, and off as they stream out. Meshes with LODs become per-LOD banded layers whose distance bands are an exact port of the CPU LOD selection, so the GPU path draws the same LOD the CPU path would, in every view, at every distance.<br />
</li></ul>
<br />
<span style="font-weight: bold;">Measured (262k instances):</span><ul>
<li>Frustum only: 4.24 -&gt; 1.85 ms (2.3x)</li>
<li>Full stack: 8.8 -&gt; 1.54 ms (<span style="font-weight: bold;">5.7x</span>)</li>
<li>CPU command recording: 26.6 -&gt; 1.5 ms<br />
</li></ul>
Pixel-identical A/Bs — zero false culls.<br />
<br />
Demos: <span style="font-style: italic;">Tutorial_17_GpuCull</span>, and <span style="font-style: italic;">Tutorial_14_GpuWorld</span> (a real streamed world with thousands of statics flying in and out of range).<br />
<br />
<span style="font-weight: bold;">An engine-wide bug this uncovered:</span> under CSM load the per-draw uniform ring silently wrapped mid-frame (65k casters x 4 cascades is roughly 10x the ring budget), corrupting already-bound slots. That was a latent bug for anyone with heavy shadow scenes, not just the new path. The ring now grows on overflow.<br />
<br />
<span style="font-weight: bold;">And the honest part.</span> The win scales with density. At ~29k cheap statics with shadows on, the CPU path is still ahead (6.8 vs 7.8 ms) — the per-cascade dispatch rounds are a fixed cost that needs volume to amortise. This is a scalability feature, not a free win in every scene, which is why it stays opt-in.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">3. The pillar I did not ship</span></span><br />
<br />
The third missing pillar was parallel command recording — record the G-buffer and each shadow cascade on separate threads into secondary command buffers. It is a standard modern design and it was on the roadmap.<br />
<br />
I measured before writing it. The GPU-driven layer had already taken single-threaded CPU recording from 26.6 ms to 1.5 ms. What remains is serial pipeline overhead, not parallelisable draw loops — so a week-scale threading refactor (per-thread secondary CBs, per-context pools, a thread-safe uniform ring) would buy under 1 ms on frames that are GPU-bound anyway.<br />
<br />
So it is closed, not done, and I would rather say that plainly than ship a feature to tick a box. Same outcome for a per-frame arena allocator: an LD_PRELOAD counting shim showed steady-state allocation is under 10 mallocs per frame, so there was nothing to win.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">4. Environmental audio — the immersion layer</span></span><br />
<br />
This is the change I am happiest with: the engine had solid audio plumbing (OpenAL Soft, Opus/FLAC, real voice virtualization with priority management) and <span style="font-weight: bold;">no immersion layer whatsoever</span>. No reverb. No HRTF. No occlusion. Every space — cathedral, cupboard, open field — sounded identical and anechoic.<br />
<br />
OpenAL Soft ships EFX and HRTF for free. The engine simply never asked for them.<br />
<br />
Now shipped, <span style="font-weight: bold;">with zero new dependencies</span>:<br />
<ul>
<li><span style="font-weight: bold;">Reverb (EFX)</span> — <span style="font-family: monospace;">SND_REVERB</span>, a 26-preset EAX vocabulary. Live 3D sources attach a reverb send automatically; 2D/UI/music stay dry by construction.</li>
<li><span style="font-weight: bold;">Reverb zones</span> — Ball/Box volumes, innermost wins, with a classic two-slot equal-power crossfade. Walking through a doorway fades between spaces smoothly, and retargeting mid-fade stays smooth.</li>
<li><span style="font-weight: bold;">Occlusion and obstruction</span> — physics-raycast, using the Wwise-style split: a wall between you and the source muffles the dry path (obstruction), and full occlusion escalates into the reverb sends too. Shoulder-offset rays, round-robin throttled, smoothed.</li>
<li><span style="font-weight: bold;">HRTF + doppler</span> — proper binaural positioning on headphones.</li>
<li><span style="font-weight: bold;">Decoded-PCM sample bank</span> — cached PCM served through the existing reader, so looping, seeking, muffling and virtualization all keep working while per-play decode cost drops to zero.<br />
</li></ul>
<br />
Demo: <span style="font-style: italic;">Tutorial_09_EnvironmentalAudio</span> — a stone room, a doorway, and an open field, with <span style="font-weight: bold;">R</span>/<span style="font-weight: bold;">O</span>/<span style="font-weight: bold;">H</span>/<span style="font-weight: bold;">B</span> toggling reverb, occlusion, HRTF and the sample bank so you can hear each one switch in and out.<br />
<br />
<span style="font-weight: bold;">OpenAL across platforms.</span> The environmental stack is OpenAL-only, and Windows historically shipped XAudio/DirectSound. Rather than port the whole thing to XAudio2 XAPO effects, Windows now moves to openal-soft as well — the headers and Win64 import library were already vendored in-repo, so it is a define flip plus the DLL. One sound provider, one environmental stack, everywhere. (Landed; still wants validation on a Windows box, and there is a one-line rollback if it misbehaves.)<br />
<br />
Design doc: <span style="font-family: monospace;">docs/audio-design.md</span><br />
<br />
<span style="font-weight: bold;">One thing I deliberately did not build:</span> ray-traced audio occlusion against the RT acceleration structure. It sounds great in a feature list, and I have the TLAS sitting right there. But collision proxies are <span style="font-style: italic;">acoustically better</span> occluders than render triangles — sound wavelengths are metre-scale and diffract around the detail geometry that a TLAS is full of — the ray counts involved do not need a GPU, and a GPU readback adds latency to a system that must react instantly. It was a marketing bullet, not audible value, so it is struck. Material-based transmission and portal propagation are the real follow-ons.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">5. Texture residency / VRAM budget</span></span><br />
<br />
Every referenced texture was promoted to full resolution and kept there — no residency, no budget, no eviction. Your working set was "every texture the world has ever touched, at full res", which is a hard ceiling for open worlds.<br />
<br />
<span style="font-family: monospace;">EE_TEX_BUDGET=&lt;MB&gt;</span> now enforces an LRU byte budget. Material textures timestamp themselves on bind (the bindless registration hook gives this for free), and a throttled manager demotes the least-recently-used textures one mip step under pressure, promoting them back once there is headroom, with 15% hysteresis so it cannot oscillate.<br />
<br />
The neat part: demote and promote are the <span style="font-style: italic;">same operation</span> — a reload through the engine's existing streaming loader with a shrink override. Small pak read, background stream, the standard hot swap. Deferred image destruction plus bindless view re-validation make live swaps safe by construction.<br />
<br />
Verified: budget off, or a huge budget, is pixel-identical to stock; a 1 MB budget demotes to fit exactly; raising the budget mid-run recovers to <span style="font-weight: bold;">pixel-perfect</span> full quality.<br />
<br />
<span style="font-weight: bold;">A bug worth knowing about if you write background systems:</span> <span style="font-family: monospace;">Time.frame()</span> freezes when the app is not drawing. Anything clocking off it silently stops in an unfocused or minimized window. The manager ticks from the update path instead, so streaming survives alt-tab.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">6. Compute skinning (the raster half)</span></span><br />
<br />
Last post covered GPU-skinned characters for ray tracing. That was half the story — the other half is that skinning was VS-only, so the same linear-blend skin was recomputed in <span style="font-style: italic;">every</span> pass: early-Z, opaque, and once per shadow cascade.<br />
<br />
With <span style="font-family: monospace;">EE_VK_GPU_SKIN=1</span>, a character deforms <span style="font-weight: bold;">once per frame</span> into a real vertex buffer, and every pass afterwards — G-buffer, mirrors, all shadow views, and the RT acceleration structure — draws that same deformed buffer. One deform serves raster and ray tracing.<br />
<br />
It also gets <span style="font-weight: bold;">exact per-limb motion vectors</span>: the kernel skins by the previous frame's pose into a second position channel, so TAA sees a limb's true motion instead of just the camera's.<br />
<br />
Honest number: about -2.4% GPU on an 800-character idle crowd. Vertex skinning is a smaller slice of the frame than you would guess — CPU animation update dominates that scene. The real value here is correctness (RT and motion vectors), not the frame time.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">7. Static shadow caching for local lights</span></span><br />
<br />
<span style="font-family: monospace;">D.localShadowCache(max_lights)</span>. Every shadowed light previously re-rasterised all its geometry every frame. Now each light keeps a cached static depth atlas and only re-renders dynamic objects per frame; lights with no dynamics bind the cache directly with zero copies. It is camera-motion-proof by construction, because it is all in light space.<br />
<br />
<span style="font-weight: bold;">Honest perf:</span> -22% at 512px shadow maps, roughly break-even at 1024, and it <span style="font-style: italic;">loses</span> at 2048 in a scene of cheap instanced barrels — copying a 96 MB depth atlas per light beats re-rendering geometry that was nearly free to begin with. The win condition is scenes where per-light static shadow rendering is genuinely expensive. It is opt-in for exactly that reason.<br />
<br />
(Chasing an apparent culling leak here turned out to be my own demo: point-light range grows with the square root of power, so those lights were ~90 m across and legitimately touched everything. Engine culling was correct. Emptiness is now decided by one cheap sphere query, so empty lights are truly free.)<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">8. Shader build: 2h11m -&gt; 13m56s</span></span><br />
<br />
Boolean shader dimensions (dither, gamma, alpha test, detail, material blend, UV scale, and Forward's nine light dims) were being baked into full permutations, so the pak universe — and the cold build — grew combinatorially.<br />
<br />
They are now <span style="font-weight: bold;">specialization constants</span>, resolved at pipeline creation. Two compiler-side changes turn a smaller pak into saved time: specialization values are excluded from the shader cache key, and identical compiles are deduplicated within a run.<br />
<ul>
<li>Cold Vulkan shader regen: <span style="font-weight: bold;">2h11m -&gt; 13m56s (9.4x)</span></li>
<li>Forward: 39,080 -&gt; 10,664 compiles</li>
<li>Engine.pak: -27 MB (Forward 76.8 -&gt; 37.1 MB, Deferred 29.8 -&gt; 14.4 MB, Blend Light 21.0 -&gt; 9.1 MB)</li>
<li>Output bit-identical; stock content pixel-identical<br />
</li></ul>
<br />
Part of that came from noticing the forward renderer is compiled out of the runtime entirely — so roughly 17 of the 33 remaining minutes were spent building shaders nothing could ever load. Now gated (and reversible), which also removed 108 MB of unloadable paks from the repo.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">9. Async compute, and a batch of small wins</span></span><br />
<br />
<span style="font-family: monospace;">EE_VK_ASYNC_COMPUTE=1</span> routes the GI ray-traced moments pass onto the dedicated compute queue — it reads the acceleration structure and writes an atlas consumed next frame, so it is genuinely async-clean. Two timelines (a shared one violates execution-order monotonicity), per-slot fences, concurrent sharing for cross-queue resources. About 0.09 ms/frame off the graphics queue, zero validation errors, pixel-parity.<br />
<br />
Also landed, each small and each measured:<ul>
<li><span style="font-weight: bold;">Static vertex/index buffers no longer take a dedicated allocation</span> — they suballocate through VMA. Every mesh used to burn one <span style="font-family: monospace;">vkAllocateMemory</span>, heading for the ~4096 allocation ceiling in a mesh-heavy world.</li>
<li><span style="font-weight: bold;">GI capture dispatches batched</span> — one 3D dispatch per pass instead of one micro-dispatch per probe with a full barrier between each.</li>
<li><span style="font-weight: bold;">Immutable samplers</span> — the five never-recreated samplers are baked into the layouts. The other four are settings-mutable, so baking them would dangle every layout on a settings change; they stay written.</li>
<li><span style="font-weight: bold;">Dynamic vertex ring 32 KB -&gt; 2 MB</span> — kills most mid-frame buffer orphaning.</li>
<li><span style="font-weight: bold;">PhysX TGS solver</span> — opt-in via <span style="font-family: monospace;">EE_PHYSX_TGS</span>, cheaper stability.<br />
</li></ul>
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">Correctness</span></span><br />
<br />
The whole stack went through an adversarial multi-agent review before merge — reviewers instructed to <span style="font-style: italic;">refute</span> each finding against the code rather than confirm it. It found 9 real defects: a use-after-free in the sound bank, two more memory bugs, a GPU-cull shadow-gate hole, a spec violation, and four feature-breaking logic errors — including one where the compute-skinning motion vectors <span style="font-style: italic;">never actually worked</span> and had been silently inert for standard meshes.<br />
<br />
Every one of them was behind an opt-in lever, so the default path never regressed. All fixed. A forced synchronization-validation soak reports zero hazards.<br />
<br />
I mention it because it is the honest counterweight to a features list: this many opt-in systems will hide broken paths unless something goes looking for them on purpose.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">Summary</span></span><br />
<ul>
<li><span style="font-weight: bold;">Bindless materials</span> — 533 -&gt; 22 descriptor sets/frame, +51% fps at 512 materials</li>
<li><span style="font-weight: bold;">GPU-driven rendering</span> — compute cull + Hi-Z + indirect draw, 8.8 -&gt; 1.54 ms at 262k instances</li>
<li><span style="font-weight: bold;">Environmental audio</span> — EFX reverb, zones, occlusion, HRTF, PCM bank; openal-soft on every platform</li>
<li><span style="font-weight: bold;">Texture residency</span> — <span style="font-family: monospace;">EE_TEX_BUDGET</span>, LRU mip demote/promote, pixel-perfect recovery</li>
<li><span style="font-weight: bold;">Compute skinning</span> — deform once per frame, exact per-limb motion vectors</li>
<li><span style="font-weight: bold;">Static shadow cache</span> — <span style="font-family: monospace;">D.localShadowCache</span></li>
<li><span style="font-weight: bold;">Spec constants</span> — cold shader build 9.4x faster, pak -27 MB</li>
<li><span style="font-weight: bold;">Async compute</span> — GI moments on the dedicated queue<br />
</li></ul>
<br />
Design docs are in the repo: <span style="font-family: monospace;">docs/audio-design.md</span>, <span style="font-family: monospace;">docs/clustered-froxel-lights-design.md</span>, <span style="font-family: monospace;">docs/rt-shadows-design.md</span>, <span style="font-family: monospace;">docs/rt-reflections-design.md</span>.<br />
<br />
<br />
In this engine fork:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Nanite-like Meshlets]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11834</link>
			<pubDate>Tue, 14 Jul 2026 14:38:34 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11834</guid>
			<description><![CDATA[Videos:<br />
Using:<br />
<a href="https://www.fab.com/listings/454001c6-075c-411c-8682-28c098596e2e" target="_blank">https://www.fab.com/listings/454001c6-07...c098596e2e</a><br />
<a href="https://streamable.com/6djncn" target="_blank">https://streamable.com/6djncn</a><br />
<br />
Using:<br />
<a href="https://www.fab.com/listings/af3e64be-29ce-4fe0-a8bc-0924987162e8" target="_blank">https://www.fab.com/listings/af3e64be-29...24987162e8</a><br />
<a href="https://streamable.com/1xsc3m" target="_blank">https://streamable.com/1xsc3m</a><br />
<br />
Using an engine generated million tri mesh (so megascans don't have to be included in engine repo):<br />
<a href="https://streamable.com/xnr8kv" target="_blank">https://streamable.com/xnr8kv</a><br />
<br />
<br />
Screenshots:<br />
<a href="https://postimg.cc/ft0qGkNM" target="_blank"><img src="https://i.postimg.cc/RFPrnnpJ/Screenshot-From-2026-07-13-22-17-29.png" border="0" alt="[Image: Screenshot-From-2026-07-13-22-17-29.png]" /></a><br />
<br />
<a href="https://postimg.cc/Kkgp64y6" target="_blank"><img src="https://i.postimg.cc/4d8Chh2x/Screenshot-From-2026-07-13-22-17-39.png" border="0" alt="[Image: Screenshot-From-2026-07-13-22-17-39.png]" /></a><br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
AI written summary:<br />
<br />
Film-quality source meshes — photogrammetry scans, ZBrush sculpts, Megascans "Raw" assets — can't be drawn directly. A million-triangle rock is mostly <span style="font-weight: bold;">sub-pixel triangles</span>, where the hardware rasterizer wastes most of its work (2x2 quad granularity + triangle setup), and where hand-authored LODs pop.<br />
<br />
This branch adds <span style="font-weight: bold;">virtualized geometry</span>: a mesh is baked into <span style="font-weight: bold;">clusters</span> (&lt;=64 vertices / &lt;=124 triangles, each with a bounding sphere + normal cone), the GPU picks a per-view cut through a <span style="font-weight: bold;">continuous-LOD DAG</span> every frame, and the number of triangles you draw tracks the <span style="font-weight: bold;">screen area</span> the object covers — never how many triangles the artist shipped.<br />
<br />
<span style="font-weight: bold;">Vulkan only</span> (needs <span style="font-style: italic;">VK_EXT_mesh_shader</span>), and <span style="font-weight: bold;">opt-in</span>: with it off, nothing in the classic path changes.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">How it works</span></span><br />
<br />
<ol type="1">
<li><span style="font-weight: bold;">Bake</span> — <span style="font-style: italic;">MeshRender::buildMeshletDag()</span>. Cluster the mesh, then repeatedly group adjacent clusters, simplify each group <span style="font-weight: bold;">with its outer boundary locked</span> (so neighbours stay crack-free), and re-clusterize. It's a DAG, not a chain. 1M triangles -&gt; ~22k clusters, 14-19 levels, about <span style="font-weight: bold;">1.8 seconds</span>. Bake once, save the mesh, done.</li>
<li><span style="font-weight: bold;">Task shader</span> — one thread per (instance, cluster): frustum cull + normal-cone backface cull + <span style="font-weight: bold;">error-based LOD cut</span> + two-phase Hi-Z occlusion. One dispatch covers <span style="font-style: italic;">every instance</span> of a mesh, so 400 rocks is one dispatch, not 400.</li>
<li><span style="font-weight: bold;">Mesh shader</span> — emits the surviving cluster's triangles, and can <span style="font-weight: bold;">skin its own &lt;=64 vertices</span> (no pre-skin pass, no per-instance deformed vertex buffer — a crowd is still one dispatch).</li>
<li><span style="font-weight: bold;">Visibility buffer</span> — rasterize (cluster | triangle) into one 32-bit target, then resolve materials in a <span style="font-weight: bold;">single full-screen pass</span>: exactly one shade per visible pixel.</li>
<li><span style="font-weight: bold;">Compute rasterizer</span> — clusters whose screen footprint is tiny skip the hardware raster entirely; a 64-bit atomic <span style="font-weight: bold;">is</span> the depth test. At scan density that's most of the geometry.</li>
<li><span style="font-weight: bold;">Hierarchical pre-pass</span> — cull the DAG's node hierarchy first so the task stage only ever sees survivors.<br />
</li></ol>
<br />
<span style="font-weight: bold;">Why the LOD cut is crack-free:</span> a cluster is used while its own error projects below the pixel budget and its parent group's error does not. Because the group's boundary was locked during simplification, two neighbouring clusters sitting at <span style="font-style: italic;">different</span> LOD levels still share an identical boundary. No stitching, no skirts, no popping.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">Turning it on (and off)</span></span><br />
<br />
Everything lives on the global <span style="font-style: italic;">GpuMeshlet</span> object. <span style="font-weight: bold;">Defaults are the ship defaults</span> — the whole system is inert until you set <span style="font-style: italic;">enabled</span>:<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>GpuMeshlet.enabled = true;&nbsp;&nbsp; // master switch&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;DEFAULT: false (fully inert)</code></div></div>
<br />
<span style="font-weight: bold;">Culling</span><br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>GpuMeshlet.cull&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= true;&nbsp;&nbsp; // per-cluster frustum cull&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;DEFAULT: true<br />
GpuMeshlet.cone_cull = true;&nbsp;&nbsp; // per-cluster normal-cone backface&nbsp;&nbsp;&nbsp;&nbsp;DEFAULT: true<br />
GpuMeshlet.hiz&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = true;&nbsp;&nbsp; // two-phase Hi-Z occlusion&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;DEFAULT: true<br />
GpuMeshlet.precull&nbsp;&nbsp; = false;&nbsp;&nbsp;// hierarchical node pre-pass&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;DEFAULT: false<br />
GpuMeshlet.tri_cull&nbsp;&nbsp;= false;&nbsp;&nbsp;// per-triangle cull&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; DEFAULT: false (measured SLOWER; the<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //&nbsp;&nbsp; compute rasterizer already owns that case)</code></div></div>
<span style="font-weight: bold;">precull</span> is off by default because it carries a fixed per-mesh dispatch cost that doesn't pay for itself on light scenes. On heavy content it is the single biggest win available — see the numbers below. If your scene is scan-density, turn it on.<br />
<br />
<span style="font-weight: bold;">LOD</span><br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>GpuMeshlet.lod&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= true;&nbsp;&nbsp; // the continuous-LOD DAG&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; DEFAULT: true<br />
GpuMeshlet.lod_error&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= 1.0f;&nbsp;&nbsp; // screen-space error budget, in PIXELS<br />
GpuMeshlet.shadow_lod_error = 4.0f;&nbsp;&nbsp; // the budget used for SHADOW views</code></div></div>
<span style="font-weight: bold;">shadow_lod_error</span> matters more than it looks. A shadow map cannot resolve sub-pixel triangles, so rasterizing them into one is pure waste. Shadow views take a coarser cut — but the cut is still evaluated from the <span style="font-weight: bold;">main camera</span>, so shadow geometry can never disagree with what you can see. Set it to 0 to use the main view's budget (the old behaviour).<br />
<br />
<span style="font-weight: bold;">Rasterization</span><br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>GpuMeshlet.visbuffer = false;&nbsp;&nbsp;// visibility buffer + deferred material resolve<br />
GpuMeshlet.swraster&nbsp;&nbsp;= false;&nbsp;&nbsp;// compute rasterizer for tiny clusters (needs visbuffer)<br />
GpuMeshlet.sw_thresh = 16.0f;&nbsp;&nbsp;// cluster screen size (px) below which compute raster takes over</code></div></div>
<br />
<span style="font-weight: bold;">Shadows / secondary views</span><br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>GpuMeshlet.shadow&nbsp;&nbsp;&nbsp;&nbsp;= true;&nbsp;&nbsp; // meshlets cast shadows&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; DEFAULT: true<br />
GpuMeshlet.secondary = true;&nbsp;&nbsp; // meshlets appear in mirrors / water / GI captures</code></div></div>
<br />
<span style="font-weight: bold;">Streaming</span> (cluster residency — for assets too big to keep resident)<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>GpuMeshlet.stream&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= false;&nbsp;&nbsp;// residency management&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; DEFAULT: false (bit-exact when off)<br />
GpuMeshlet.stream_disk = false;&nbsp;&nbsp;// spill to disk<br />
GpuMeshlet.stream_pak&nbsp;&nbsp;= false;&nbsp;&nbsp;// stream straight out of the .pak<br />
GpuMeshlet.stream_pool = 1024;&nbsp;&nbsp; // resident cluster budget</code></div></div>
<br />
<span style="font-weight: bold;">World integration</span><br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>GpuMeshlet.world = false;&nbsp;&nbsp; // Game::Static objects auto-migrate to the meshlet path</code></div></div>
Or drive it yourself: <span style="font-style: italic;">GpuMeshlet.add(render, matrix, material)</span> per instance, or <span style="font-style: italic;">worldAdd(mesh, matrix)</span> which returns <span style="font-weight: bold;">false</span> if the per-frame unique-mesh budget (128 meshes) is exhausted — in which case you draw it classically. Overflow <span style="font-weight: bold;">degrades</span>, it never silently drops geometry.<br />
<br />
<span style="font-weight: bold;">Debug palette</span> (visibility-buffer path only — it recolors the <span style="font-style: italic;">real</span> cluster/triangle IDs, not a proxy mesh)<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>GpuMeshlet.debug&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= 0;&nbsp;&nbsp;// 0 off, 1 triangle, 2 cluster, 3 triangle screen SIZE, 4 instance<br />
GpuMeshlet.debug_wipe = 0;&nbsp;&nbsp;// split-screen X in pixels (0 = whole screen)</code></div></div>
<br />
<span style="font-weight: bold;">Stats:</span> <span style="font-style: italic;">GpuMeshlet.drawnClusters()</span>, <span style="font-style: italic;">GpuMeshlet.precullClusters()</span>.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">Virtual Shadow Maps (the companion feature)</span></span><br />
<br />
Cascaded shadow maps re-render every caster every frame at a resolution that's wrong almost everywhere. VSM treats the sun's shadow as a <span style="font-weight: bold;">virtual texture</span>: a 128x128 grid of pages per clipmap level over a 4096^2 physical atlas, where only the pages the screen actually asks for are rendered — and they are <span style="font-weight: bold;">cached across frames</span>.<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>VsmShadows(true);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// enable (or EE_VSM=1)<br />
VsmInvalidate(Ball);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // tell the cache a world volume changed<br />
VsmStats(resident, rendered);&nbsp;&nbsp;// rendered == 0 means fully cached<br />
VsmPendingPages();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &gt;0 == still refilling</code></div></div>
<ul>
<li>It's delivered as a full-screen sun-visibility <span style="font-weight: bold;">mask</span>, into the same socket the ray-traced sun shadows use — so it needed <span style="font-weight: bold;">zero shader-pak changes</span>.</li>
<li>Clipmap level is chosen by <span style="font-weight: bold;">texel density</span> (~1 atlas texel per screen pixel), not distance — which is also what bounds the page count.</li>
<li>Pages are rendered by the engine's own traversal, so a page gets <span style="font-weight: bold;">every</span> caster: terrain, classic objects, and meshlets alike.</li>
<li>The <span style="font-weight: bold;">cascades only render while the cache is refilling</span>. In the steady state VSM carries the sun shadow alone and the entire cascade render + resolve is skipped.</li>
<li>Movement is tracked automatically for both meshlet and classic casters (skinned ones are bounded by their <span style="font-weight: bold;">bones</span> — a character's matrix can sit still while the pose moves). Spawn / despawn / streaming are handled too. <span style="font-style: italic;">VsmInvalidate()</span> is only for geometry that changes <span style="font-weight: bold;">without moving</span> (a terrain edit, a swapped mesh).<br />
</li></ul>
<br />
Env knobs: <span style="font-style: italic;">EE_VSM=1</span>, <span style="font-style: italic;">EE_VSM_LEVELS</span> (4), <span style="font-style: italic;">EE_VSM_CSM</span> (auto / 0=never / 1=always), <span style="font-style: italic;">EE_VSM_PAGE_BUDGET</span> (24), <span style="font-style: italic;">EE_VSM_COLD_BUDGET</span> (128), <span style="font-style: italic;">EE_VSM_CLASSIC=0</span> (disable the classic-caster scan), <span style="font-style: italic;">EE_VSM_DEBUG</span> (1-9), <span style="font-style: italic;">EE_VSM_LOG=1</span>.<br />
<br />
<span style="font-weight: bold;">Measured</span> (400 instances, GPU_TIMER_LIGHT): cascades <span style="font-weight: bold;">3.01 ms</span> -&gt; VSM <span style="font-weight: bold;">2.71 ms</span>, sharper and cached. A moving caster re-renders <span style="font-weight: bold;">12 pages, not 567</span>.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">Tutorials</span></span><br />
<br />
Five, all under <span style="font-style: italic;">Tutorials/Source/14 - Game Basics/</span>. They run on engine/procedural assets — no downloads needed.<br />
<ul>
<li><span style="font-weight: bold;">Tutorial_14_Meshlets</span> — the teaching one. A classic-drawn sphere next to a meshlet-drawn sphere as an <span style="font-weight: bold;">A/B control</span>, with every toggle live: <span style="font-style: italic;">[C]</span> frustum, <span style="font-style: italic;">[N]</span> cone, <span style="font-style: italic;">[H]</span> Hi-Z, <span style="font-style: italic;">[W]</span> an occluding wall, <span style="font-style: italic;">[S]</span> shadows, <span style="font-style: italic;">[L]</span> the LOD DAG, <span style="font-style: italic;">[-/=]</span> the error budget, <span style="font-style: italic;">[G]</span> instance count, <span style="font-style: italic;">[T]</span> false-color clusters. Watch the drawn-cluster count collapse as you put the wall up.</li>
<li><span style="font-weight: bold;">Tutorial_14_MeshletWorld</span> — the <span style="font-style: italic;">Game::Static</span> world path (<span style="font-style: italic;">GpuMeshlet.world</span>) plus VSM. This is what a real game scene looks like.</li>
<li><span style="font-weight: bold;">Tutorial_14_SkinnedMeshlets</span> — mesh-shader skinning. A crowd of animated characters, still one dispatch, crack-free under animation.</li>
<li><span style="font-weight: bold;">Tutorial_14_HeroMeshlets</span> — hero density: a procedurally displaced <span style="font-weight: bold;">1M-triangle</span> rock, instanced into the distance so one frame spans the whole LOD range.</li>
<li><span style="font-weight: bold;">Tutorial_14_MeshletShowcase</span> — the presentation demo. Split-screen <span style="font-weight: bold;">photoreal | false-color</span> (the canonical shot), a fly-in, a 200-instance field, and the debug palette. Defaults to its own procedural 1M-triangle rock, and will <span style="font-weight: bold;">import a real scan</span> if you point it at one:<br />
</li></ul>
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code># one scan (OBJ or FBX; 8K PBR maps are found automatically next to the mesh)<br />
EE_SHOWCASE_MESH=/path/Rock_Raw.fbx&nbsp;&nbsp;EE_SHOWCASE_YAW=180&nbsp;&nbsp;./Tutorial_14_MeshletShowcase<br />
<br />
# a whole Megascans kit: every sub-folder's scan becomes its OWN asset<br />
EE_SHOWCASE_DIR=/path/quarry&nbsp;&nbsp;EE_SHOWCASE_COUNT=45&nbsp;&nbsp;EE_MESHLET_PRECULL=1&nbsp;&nbsp;./Tutorial_14_MeshletShowcase</code></div></div>
<br />
Each tutorial exposes its knobs as env vars for A/B'ing without a rebuild — <span style="font-style: italic;">EE_MESHLET_VIS</span>, <span style="font-style: italic;">EE_MESHLET_SW</span>, <span style="font-style: italic;">EE_MESHLET_LOD_ERR</span>, <span style="font-style: italic;">EE_MESHLET_PRECULL</span>, <span style="font-style: italic;">EE_MESHLET_SHADOW_ERR</span>, <span style="font-style: italic;">EE_MESHLET_TRI_CULL</span>, <span style="font-style: italic;">EE_MESHLET_STREAM</span>, and so on. (These are <span style="font-weight: bold;">demo-side</span> conveniences; the engine API is the <span style="font-style: italic;">GpuMeshlet</span> flags above.)<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">Numbers</span></span><br />
<br />
Measured on an AMD RX 6650 XT at 3072x1296, on real Megascans scans:<br />
<ul>
<li><span style="font-weight: bold;">Stone wall scan</span> — 1,000,000 tris (28k clusters): draws 71k (5.7%), <span style="font-weight: bold;">2.7 ms</span> opaque.</li>
<li><span style="font-weight: bold;">Mine tunnel scan</span> — 7,996,040 tris (194k clusters, 23 DAG levels): draws 144k (<span style="font-weight: bold;">1.8%</span>), <span style="font-weight: bold;">2.3 ms</span> opaque.</li>
<li><span style="font-weight: bold;">Slate quarry</span> — <span style="font-weight: bold;">45 distinct</span> million-triangle scans = 48,977,263 triangles of <span style="font-style: italic;">unique</span> geometry, 200 instances = <span style="font-weight: bold;">217,936,240 scene triangles</span>: draws 363k (<span style="font-weight: bold;">0.167%</span>), <span style="font-weight: bold;">12.9 ms</span> opaque.<br />
</li></ul>
<br />
And what <span style="font-style: italic;">precull</span> does to that quarry scene (16 unique scans, 200 instances, ~212M scene triangles):<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>gpuOpaque&nbsp;&nbsp;&nbsp;&nbsp;gpuLight&nbsp;&nbsp;&nbsp;&nbsp;fps<br />
&nbsp;&nbsp;precull OFF&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;34.9 ms&nbsp;&nbsp;&nbsp;&nbsp; 115.0 ms&nbsp;&nbsp;&nbsp;&nbsp;6.6<br />
&nbsp;&nbsp;precull ON&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;9.6 ms&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;25.5 ms&nbsp;&nbsp; 28.1</code></div></div>
Pixel-identical output. If you're running scan-density content, <span style="font-weight: bold;">turn precull on</span>.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">Caveats — read these</span></span><br />
<ul>
<li><span style="font-weight: bold;">Vulkan only.</span> Needs <span style="font-style: italic;">VK_EXT_mesh_shader</span>. There is no DX12 or GL path yet; the classic renderer is untouched and still handles everything else.</li>
<li><span style="font-weight: bold;">Opt-in.</span> Default-off, top to bottom. Nothing changes until <span style="font-style: italic;">GpuMeshlet.enabled = true</span>.</li>
<li><span style="font-weight: bold;">128 unique meshes per frame.</span> Past that, <span style="font-style: italic;">worldAdd()</span> returns false and the mesh keeps its classic draw — it degrades, it doesn't drop.</li>
<li><span style="font-weight: bold;">precull is default-off</span> and <span style="font-weight: bold;">tri_cull is default-off</span> (the latter measured slower — the compute rasterizer already covers its best case).</li>
<li>Materials work on every path, including normal mapping with <span style="font-weight: bold;">zero stored tangents</span> (the resolve derives the tangent frame analytically) and alpha-tested cutouts. Non-opaque techniques fall back to the classic path.<br />
</li></ul>
<br />
Full write-up: <span style="font-style: italic;">docs/meshlets-design.md</span> and <span style="font-style: italic;">docs/vsm-design.md</span>. The README has expandable sections for both.<br />
<br />
<br />
In this fork:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></description>
			<content:encoded><![CDATA[Videos:<br />
Using:<br />
<a href="https://www.fab.com/listings/454001c6-075c-411c-8682-28c098596e2e" target="_blank">https://www.fab.com/listings/454001c6-07...c098596e2e</a><br />
<a href="https://streamable.com/6djncn" target="_blank">https://streamable.com/6djncn</a><br />
<br />
Using:<br />
<a href="https://www.fab.com/listings/af3e64be-29ce-4fe0-a8bc-0924987162e8" target="_blank">https://www.fab.com/listings/af3e64be-29...24987162e8</a><br />
<a href="https://streamable.com/1xsc3m" target="_blank">https://streamable.com/1xsc3m</a><br />
<br />
Using an engine generated million tri mesh (so megascans don't have to be included in engine repo):<br />
<a href="https://streamable.com/xnr8kv" target="_blank">https://streamable.com/xnr8kv</a><br />
<br />
<br />
Screenshots:<br />
<a href="https://postimg.cc/ft0qGkNM" target="_blank"><img src="https://i.postimg.cc/RFPrnnpJ/Screenshot-From-2026-07-13-22-17-29.png" border="0" alt="[Image: Screenshot-From-2026-07-13-22-17-29.png]" /></a><br />
<br />
<a href="https://postimg.cc/Kkgp64y6" target="_blank"><img src="https://i.postimg.cc/4d8Chh2x/Screenshot-From-2026-07-13-22-17-39.png" border="0" alt="[Image: Screenshot-From-2026-07-13-22-17-39.png]" /></a><br />
<br />
<br />
<br />
<br />
<br />
<br />
<br />
AI written summary:<br />
<br />
Film-quality source meshes — photogrammetry scans, ZBrush sculpts, Megascans "Raw" assets — can't be drawn directly. A million-triangle rock is mostly <span style="font-weight: bold;">sub-pixel triangles</span>, where the hardware rasterizer wastes most of its work (2x2 quad granularity + triangle setup), and where hand-authored LODs pop.<br />
<br />
This branch adds <span style="font-weight: bold;">virtualized geometry</span>: a mesh is baked into <span style="font-weight: bold;">clusters</span> (&lt;=64 vertices / &lt;=124 triangles, each with a bounding sphere + normal cone), the GPU picks a per-view cut through a <span style="font-weight: bold;">continuous-LOD DAG</span> every frame, and the number of triangles you draw tracks the <span style="font-weight: bold;">screen area</span> the object covers — never how many triangles the artist shipped.<br />
<br />
<span style="font-weight: bold;">Vulkan only</span> (needs <span style="font-style: italic;">VK_EXT_mesh_shader</span>), and <span style="font-weight: bold;">opt-in</span>: with it off, nothing in the classic path changes.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">How it works</span></span><br />
<br />
<ol type="1">
<li><span style="font-weight: bold;">Bake</span> — <span style="font-style: italic;">MeshRender::buildMeshletDag()</span>. Cluster the mesh, then repeatedly group adjacent clusters, simplify each group <span style="font-weight: bold;">with its outer boundary locked</span> (so neighbours stay crack-free), and re-clusterize. It's a DAG, not a chain. 1M triangles -&gt; ~22k clusters, 14-19 levels, about <span style="font-weight: bold;">1.8 seconds</span>. Bake once, save the mesh, done.</li>
<li><span style="font-weight: bold;">Task shader</span> — one thread per (instance, cluster): frustum cull + normal-cone backface cull + <span style="font-weight: bold;">error-based LOD cut</span> + two-phase Hi-Z occlusion. One dispatch covers <span style="font-style: italic;">every instance</span> of a mesh, so 400 rocks is one dispatch, not 400.</li>
<li><span style="font-weight: bold;">Mesh shader</span> — emits the surviving cluster's triangles, and can <span style="font-weight: bold;">skin its own &lt;=64 vertices</span> (no pre-skin pass, no per-instance deformed vertex buffer — a crowd is still one dispatch).</li>
<li><span style="font-weight: bold;">Visibility buffer</span> — rasterize (cluster | triangle) into one 32-bit target, then resolve materials in a <span style="font-weight: bold;">single full-screen pass</span>: exactly one shade per visible pixel.</li>
<li><span style="font-weight: bold;">Compute rasterizer</span> — clusters whose screen footprint is tiny skip the hardware raster entirely; a 64-bit atomic <span style="font-weight: bold;">is</span> the depth test. At scan density that's most of the geometry.</li>
<li><span style="font-weight: bold;">Hierarchical pre-pass</span> — cull the DAG's node hierarchy first so the task stage only ever sees survivors.<br />
</li></ol>
<br />
<span style="font-weight: bold;">Why the LOD cut is crack-free:</span> a cluster is used while its own error projects below the pixel budget and its parent group's error does not. Because the group's boundary was locked during simplification, two neighbouring clusters sitting at <span style="font-style: italic;">different</span> LOD levels still share an identical boundary. No stitching, no skirts, no popping.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">Turning it on (and off)</span></span><br />
<br />
Everything lives on the global <span style="font-style: italic;">GpuMeshlet</span> object. <span style="font-weight: bold;">Defaults are the ship defaults</span> — the whole system is inert until you set <span style="font-style: italic;">enabled</span>:<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>GpuMeshlet.enabled = true;&nbsp;&nbsp; // master switch&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;DEFAULT: false (fully inert)</code></div></div>
<br />
<span style="font-weight: bold;">Culling</span><br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>GpuMeshlet.cull&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= true;&nbsp;&nbsp; // per-cluster frustum cull&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;DEFAULT: true<br />
GpuMeshlet.cone_cull = true;&nbsp;&nbsp; // per-cluster normal-cone backface&nbsp;&nbsp;&nbsp;&nbsp;DEFAULT: true<br />
GpuMeshlet.hiz&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = true;&nbsp;&nbsp; // two-phase Hi-Z occlusion&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;DEFAULT: true<br />
GpuMeshlet.precull&nbsp;&nbsp; = false;&nbsp;&nbsp;// hierarchical node pre-pass&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;DEFAULT: false<br />
GpuMeshlet.tri_cull&nbsp;&nbsp;= false;&nbsp;&nbsp;// per-triangle cull&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; DEFAULT: false (measured SLOWER; the<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //&nbsp;&nbsp; compute rasterizer already owns that case)</code></div></div>
<span style="font-weight: bold;">precull</span> is off by default because it carries a fixed per-mesh dispatch cost that doesn't pay for itself on light scenes. On heavy content it is the single biggest win available — see the numbers below. If your scene is scan-density, turn it on.<br />
<br />
<span style="font-weight: bold;">LOD</span><br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>GpuMeshlet.lod&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= true;&nbsp;&nbsp; // the continuous-LOD DAG&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; DEFAULT: true<br />
GpuMeshlet.lod_error&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= 1.0f;&nbsp;&nbsp; // screen-space error budget, in PIXELS<br />
GpuMeshlet.shadow_lod_error = 4.0f;&nbsp;&nbsp; // the budget used for SHADOW views</code></div></div>
<span style="font-weight: bold;">shadow_lod_error</span> matters more than it looks. A shadow map cannot resolve sub-pixel triangles, so rasterizing them into one is pure waste. Shadow views take a coarser cut — but the cut is still evaluated from the <span style="font-weight: bold;">main camera</span>, so shadow geometry can never disagree with what you can see. Set it to 0 to use the main view's budget (the old behaviour).<br />
<br />
<span style="font-weight: bold;">Rasterization</span><br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>GpuMeshlet.visbuffer = false;&nbsp;&nbsp;// visibility buffer + deferred material resolve<br />
GpuMeshlet.swraster&nbsp;&nbsp;= false;&nbsp;&nbsp;// compute rasterizer for tiny clusters (needs visbuffer)<br />
GpuMeshlet.sw_thresh = 16.0f;&nbsp;&nbsp;// cluster screen size (px) below which compute raster takes over</code></div></div>
<br />
<span style="font-weight: bold;">Shadows / secondary views</span><br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>GpuMeshlet.shadow&nbsp;&nbsp;&nbsp;&nbsp;= true;&nbsp;&nbsp; // meshlets cast shadows&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; DEFAULT: true<br />
GpuMeshlet.secondary = true;&nbsp;&nbsp; // meshlets appear in mirrors / water / GI captures</code></div></div>
<br />
<span style="font-weight: bold;">Streaming</span> (cluster residency — for assets too big to keep resident)<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>GpuMeshlet.stream&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= false;&nbsp;&nbsp;// residency management&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; DEFAULT: false (bit-exact when off)<br />
GpuMeshlet.stream_disk = false;&nbsp;&nbsp;// spill to disk<br />
GpuMeshlet.stream_pak&nbsp;&nbsp;= false;&nbsp;&nbsp;// stream straight out of the .pak<br />
GpuMeshlet.stream_pool = 1024;&nbsp;&nbsp; // resident cluster budget</code></div></div>
<br />
<span style="font-weight: bold;">World integration</span><br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>GpuMeshlet.world = false;&nbsp;&nbsp; // Game::Static objects auto-migrate to the meshlet path</code></div></div>
Or drive it yourself: <span style="font-style: italic;">GpuMeshlet.add(render, matrix, material)</span> per instance, or <span style="font-style: italic;">worldAdd(mesh, matrix)</span> which returns <span style="font-weight: bold;">false</span> if the per-frame unique-mesh budget (128 meshes) is exhausted — in which case you draw it classically. Overflow <span style="font-weight: bold;">degrades</span>, it never silently drops geometry.<br />
<br />
<span style="font-weight: bold;">Debug palette</span> (visibility-buffer path only — it recolors the <span style="font-style: italic;">real</span> cluster/triangle IDs, not a proxy mesh)<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>GpuMeshlet.debug&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= 0;&nbsp;&nbsp;// 0 off, 1 triangle, 2 cluster, 3 triangle screen SIZE, 4 instance<br />
GpuMeshlet.debug_wipe = 0;&nbsp;&nbsp;// split-screen X in pixels (0 = whole screen)</code></div></div>
<br />
<span style="font-weight: bold;">Stats:</span> <span style="font-style: italic;">GpuMeshlet.drawnClusters()</span>, <span style="font-style: italic;">GpuMeshlet.precullClusters()</span>.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">Virtual Shadow Maps (the companion feature)</span></span><br />
<br />
Cascaded shadow maps re-render every caster every frame at a resolution that's wrong almost everywhere. VSM treats the sun's shadow as a <span style="font-weight: bold;">virtual texture</span>: a 128x128 grid of pages per clipmap level over a 4096^2 physical atlas, where only the pages the screen actually asks for are rendered — and they are <span style="font-weight: bold;">cached across frames</span>.<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>VsmShadows(true);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// enable (or EE_VSM=1)<br />
VsmInvalidate(Ball);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // tell the cache a world volume changed<br />
VsmStats(resident, rendered);&nbsp;&nbsp;// rendered == 0 means fully cached<br />
VsmPendingPages();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &gt;0 == still refilling</code></div></div>
<ul>
<li>It's delivered as a full-screen sun-visibility <span style="font-weight: bold;">mask</span>, into the same socket the ray-traced sun shadows use — so it needed <span style="font-weight: bold;">zero shader-pak changes</span>.</li>
<li>Clipmap level is chosen by <span style="font-weight: bold;">texel density</span> (~1 atlas texel per screen pixel), not distance — which is also what bounds the page count.</li>
<li>Pages are rendered by the engine's own traversal, so a page gets <span style="font-weight: bold;">every</span> caster: terrain, classic objects, and meshlets alike.</li>
<li>The <span style="font-weight: bold;">cascades only render while the cache is refilling</span>. In the steady state VSM carries the sun shadow alone and the entire cascade render + resolve is skipped.</li>
<li>Movement is tracked automatically for both meshlet and classic casters (skinned ones are bounded by their <span style="font-weight: bold;">bones</span> — a character's matrix can sit still while the pose moves). Spawn / despawn / streaming are handled too. <span style="font-style: italic;">VsmInvalidate()</span> is only for geometry that changes <span style="font-weight: bold;">without moving</span> (a terrain edit, a swapped mesh).<br />
</li></ul>
<br />
Env knobs: <span style="font-style: italic;">EE_VSM=1</span>, <span style="font-style: italic;">EE_VSM_LEVELS</span> (4), <span style="font-style: italic;">EE_VSM_CSM</span> (auto / 0=never / 1=always), <span style="font-style: italic;">EE_VSM_PAGE_BUDGET</span> (24), <span style="font-style: italic;">EE_VSM_COLD_BUDGET</span> (128), <span style="font-style: italic;">EE_VSM_CLASSIC=0</span> (disable the classic-caster scan), <span style="font-style: italic;">EE_VSM_DEBUG</span> (1-9), <span style="font-style: italic;">EE_VSM_LOG=1</span>.<br />
<br />
<span style="font-weight: bold;">Measured</span> (400 instances, GPU_TIMER_LIGHT): cascades <span style="font-weight: bold;">3.01 ms</span> -&gt; VSM <span style="font-weight: bold;">2.71 ms</span>, sharper and cached. A moving caster re-renders <span style="font-weight: bold;">12 pages, not 567</span>.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">Tutorials</span></span><br />
<br />
Five, all under <span style="font-style: italic;">Tutorials/Source/14 - Game Basics/</span>. They run on engine/procedural assets — no downloads needed.<br />
<ul>
<li><span style="font-weight: bold;">Tutorial_14_Meshlets</span> — the teaching one. A classic-drawn sphere next to a meshlet-drawn sphere as an <span style="font-weight: bold;">A/B control</span>, with every toggle live: <span style="font-style: italic;">[C]</span> frustum, <span style="font-style: italic;">[N]</span> cone, <span style="font-style: italic;">[H]</span> Hi-Z, <span style="font-style: italic;">[W]</span> an occluding wall, <span style="font-style: italic;">[S]</span> shadows, <span style="font-style: italic;">[L]</span> the LOD DAG, <span style="font-style: italic;">[-/=]</span> the error budget, <span style="font-style: italic;">[G]</span> instance count, <span style="font-style: italic;">[T]</span> false-color clusters. Watch the drawn-cluster count collapse as you put the wall up.</li>
<li><span style="font-weight: bold;">Tutorial_14_MeshletWorld</span> — the <span style="font-style: italic;">Game::Static</span> world path (<span style="font-style: italic;">GpuMeshlet.world</span>) plus VSM. This is what a real game scene looks like.</li>
<li><span style="font-weight: bold;">Tutorial_14_SkinnedMeshlets</span> — mesh-shader skinning. A crowd of animated characters, still one dispatch, crack-free under animation.</li>
<li><span style="font-weight: bold;">Tutorial_14_HeroMeshlets</span> — hero density: a procedurally displaced <span style="font-weight: bold;">1M-triangle</span> rock, instanced into the distance so one frame spans the whole LOD range.</li>
<li><span style="font-weight: bold;">Tutorial_14_MeshletShowcase</span> — the presentation demo. Split-screen <span style="font-weight: bold;">photoreal | false-color</span> (the canonical shot), a fly-in, a 200-instance field, and the debug palette. Defaults to its own procedural 1M-triangle rock, and will <span style="font-weight: bold;">import a real scan</span> if you point it at one:<br />
</li></ul>
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code># one scan (OBJ or FBX; 8K PBR maps are found automatically next to the mesh)<br />
EE_SHOWCASE_MESH=/path/Rock_Raw.fbx&nbsp;&nbsp;EE_SHOWCASE_YAW=180&nbsp;&nbsp;./Tutorial_14_MeshletShowcase<br />
<br />
# a whole Megascans kit: every sub-folder's scan becomes its OWN asset<br />
EE_SHOWCASE_DIR=/path/quarry&nbsp;&nbsp;EE_SHOWCASE_COUNT=45&nbsp;&nbsp;EE_MESHLET_PRECULL=1&nbsp;&nbsp;./Tutorial_14_MeshletShowcase</code></div></div>
<br />
Each tutorial exposes its knobs as env vars for A/B'ing without a rebuild — <span style="font-style: italic;">EE_MESHLET_VIS</span>, <span style="font-style: italic;">EE_MESHLET_SW</span>, <span style="font-style: italic;">EE_MESHLET_LOD_ERR</span>, <span style="font-style: italic;">EE_MESHLET_PRECULL</span>, <span style="font-style: italic;">EE_MESHLET_SHADOW_ERR</span>, <span style="font-style: italic;">EE_MESHLET_TRI_CULL</span>, <span style="font-style: italic;">EE_MESHLET_STREAM</span>, and so on. (These are <span style="font-weight: bold;">demo-side</span> conveniences; the engine API is the <span style="font-style: italic;">GpuMeshlet</span> flags above.)<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">Numbers</span></span><br />
<br />
Measured on an AMD RX 6650 XT at 3072x1296, on real Megascans scans:<br />
<ul>
<li><span style="font-weight: bold;">Stone wall scan</span> — 1,000,000 tris (28k clusters): draws 71k (5.7%), <span style="font-weight: bold;">2.7 ms</span> opaque.</li>
<li><span style="font-weight: bold;">Mine tunnel scan</span> — 7,996,040 tris (194k clusters, 23 DAG levels): draws 144k (<span style="font-weight: bold;">1.8%</span>), <span style="font-weight: bold;">2.3 ms</span> opaque.</li>
<li><span style="font-weight: bold;">Slate quarry</span> — <span style="font-weight: bold;">45 distinct</span> million-triangle scans = 48,977,263 triangles of <span style="font-style: italic;">unique</span> geometry, 200 instances = <span style="font-weight: bold;">217,936,240 scene triangles</span>: draws 363k (<span style="font-weight: bold;">0.167%</span>), <span style="font-weight: bold;">12.9 ms</span> opaque.<br />
</li></ul>
<br />
And what <span style="font-style: italic;">precull</span> does to that quarry scene (16 unique scans, 200 instances, ~212M scene triangles):<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>gpuOpaque&nbsp;&nbsp;&nbsp;&nbsp;gpuLight&nbsp;&nbsp;&nbsp;&nbsp;fps<br />
&nbsp;&nbsp;precull OFF&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;34.9 ms&nbsp;&nbsp;&nbsp;&nbsp; 115.0 ms&nbsp;&nbsp;&nbsp;&nbsp;6.6<br />
&nbsp;&nbsp;precull ON&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;9.6 ms&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;25.5 ms&nbsp;&nbsp; 28.1</code></div></div>
Pixel-identical output. If you're running scan-density content, <span style="font-weight: bold;">turn precull on</span>.<br />
<br />
<hr />
<br />
<span style="font-weight: bold;"><span style="font-size: large;">Caveats — read these</span></span><br />
<ul>
<li><span style="font-weight: bold;">Vulkan only.</span> Needs <span style="font-style: italic;">VK_EXT_mesh_shader</span>. There is no DX12 or GL path yet; the classic renderer is untouched and still handles everything else.</li>
<li><span style="font-weight: bold;">Opt-in.</span> Default-off, top to bottom. Nothing changes until <span style="font-style: italic;">GpuMeshlet.enabled = true</span>.</li>
<li><span style="font-weight: bold;">128 unique meshes per frame.</span> Past that, <span style="font-style: italic;">worldAdd()</span> returns false and the mesh keeps its classic draw — it degrades, it doesn't drop.</li>
<li><span style="font-weight: bold;">precull is default-off</span> and <span style="font-weight: bold;">tri_cull is default-off</span> (the latter measured slower — the compute rasterizer already covers its best case).</li>
<li>Materials work on every path, including normal mapping with <span style="font-weight: bold;">zero stored tangents</span> (the resolve derives the tangent frame analytically) and alpha-tested cutouts. Non-opaque techniques fall back to the classic path.<br />
</li></ul>
<br />
Full write-up: <span style="font-style: italic;">docs/meshlets-design.md</span> and <span style="font-style: italic;">docs/vsm-design.md</span>. The README has expandable sections for both.<br />
<br />
<br />
In this fork:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[RT sun shadows, RT specular reflections, Clustered/froxel lighting]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11833</link>
			<pubDate>Fri, 10 Jul 2026 02:39:06 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11833</guid>
			<description><![CDATA[RTGI+RT SHADOWS+RT Reflections<br />
<br />
<a href="https://postimg.cc/pmjg6tFT" target="_blank"><img src="https://i.postimg.cc/bwVpdyYQ/Screenshot-From-2026-07-09-22-28-11.png" border="0" alt="[Image: Screenshot-From-2026-07-09-22-28-11.png]" /></a><br />
<br />
<br />
No GI, CSM Shadows,<br />
<br />
<a href="https://postimg.cc/RWtjDzHS" target="_blank"><img src="https://i.postimg.cc/W1YTtNpk/Screenshot-From-2026-07-09-22-28-42.png" border="0" alt="[Image: Screenshot-From-2026-07-09-22-28-42.png]" /></a><br />
<br />
<br />
AI written summary:<br />
<br />
I've been extending the engine's Vulkan renderer with three new lighting features:<br />
<ul> []Ray-traced sun shadows []Ray-traced specular reflections</li>
<li>Clustered/froxel lighting </li></ul>
<br />
All three are opt-in and disabled by default. They are also pure runtime toggles, so no shader pak regeneration is needed to use them.<br />
<br />
The two ray-tracing features share a single scene acceleration structure, meaning the cost of “going RT” is paid once.<br />
<br />
Everything below was measured on an <span style="font-weight: bold;">AMD Radeon RX 6650 XT</span>.<br />
<br />
<hr />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">The Shared Foundation: One Scene TLAS, Zero Registration</span></span><br />
<br />
Both RT features use the ray-query acceleration structure originally introduced for dynamic GI.<br />
<br />
The best part is that nothing needs to be manually registered. Enable:<br />
<br />
GIRayTrace.auto_scene = true;<br />
The renderer then feeds every opaque mesh it draws into the acceleration structure automatically. Streamed world areas enter the RT scene as they load, with no additional application code.<br />
<br />
The RT world is the retained union of everything the camera has seen. Looking away from an object does not remove it from shadow, reflection, or GI rays. Meshes leave the acceleration structure only when they are actually destroyed.<br />
<br />
Acceleration-structure rebuilds are recorded directly into the frame command buffer, so there is no per-rebuild GPU stall.<br />
<br />
<span style="font-weight: bold;">Animated characters are supported too.</span><br />
<br />
GIRayTrace.addSkinnedOccluder(mesh, skel);<br />
This GPU-skins the character into its current world pose every frame and builds a per-frame BLAS. The character then:<br />
<ul> []Casts a correctly posed ray-traced shadow []Appears fully textured in ray-traced reflections</li>
<li>Costs only a fraction of a millisecond per character </li></ul>
<br />
One deliberate design choice is that dynamic characters receive GI but do not contribute to it.<br />
<br />
A deforming body inside a sparsely updated probe field causes the surrounding indirect lighting to strobe. To avoid this, the GI probes trace against a second, static-only TLAS.<br />
<br />
This is standard DDGI practice, and it eliminated a very visible “the lighting on my character keeps jumping” artifact.<br />
<br />
<hr />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Ray-Traced Sun Shadows</span></span><br />
<br />
For each pixel, the renderer traces one or more rays toward the sun against the scene TLAS:<br />
<ul>
<li>[]Hit = shadowed []Miss = illuminated </li></ul>
<br />
The detail that makes this a drop-in replacement is that the engine's existing sun-application stage already consumes a pre-resolved full-screen visibility mask.<br />
<br />
The RT pass simply writes that mask. The light shaders remain completely unchanged.<br />
<br />
When RT shadows are enabled, the renderer skips the entire conventional shadow-map pipeline:<br />
<ul> []CSM cascade rasterization []Cascade resolve</li>
<li>Shadow-mask blur </li></ul>
<br />
<span style="font-weight: bold;">Advantages over shadow maps</span><br />
<ul>
<li><span style="font-weight: bold;">No aliasing, cascade seams, or peter-panning.</span><br />
<br />
A thin floating bar can cast a clean, visibly detached shadow that a shadow map cannot reproduce reliably at any practical cascade resolution.</li>
<li><span style="font-weight: bold;">True contact-hardening soft shadows.</span><br />
<br />
Multiple rays are cone-sampled across the sun's disc using a Fibonacci spiral. The pattern is rotated per pixel and temporally jittered so TAA converges the penumbra.<br />
<br />
The result is a shadow that remains sharp at the contact point and becomes progressively softer as the distance between the occluder and receiver increases.</li>
<li><span style="font-weight: bold;">Cost comparable to CSM.</span><br />
<br />
Because RT shadows skip cascade rasterization entirely, the net lighting-phase cost can land near parity with conventional shadow maps in typical scenes.<br />
<br />
An eight-ray, half-resolution soft-shadow pass measured approximately <span style="font-weight: bold;">0.89 ms</span>, roughly the same cost as the CSM path it replaced. </li></ul>
<br />
Full resolution is the default. Half resolution can undersample thin, moving occluders such as animated characters.<br />
<br />
Ray count is the primary performance-versus-quality control.<br />
<br />
RtShadows(true);<br />
<br />
RtShadowQuality(0.03f, 4);<br />
The first quality parameter controls sun-disc softness. A softness value of zero produces hard shadows. The second parameter controls the number of rays per pixel.<br />
<br />
<span style="font-weight: bold;">Demo: Tutorial_14_RTShadows</span><br />
<ul> []<span style="font-weight: bold;">R</span> switches live between ray-traced shadows and shadow maps []<span style="font-weight: bold;">- / =</span> adjusts shadow softness</li>
<li>An on-screen HUD displays GPU cost </li></ul>
<br />
The initial implementation supports the sun only.<br />
<br />
<hr />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Ray-Traced Specular Reflections</span></span><br />
<br />
The reflection pass traces one:<br />
<br />
reflect(eye, normal)<br />
ray per pixel against the same scene TLAS.<br />
<br />
Reflection hits are fully lit and textured using:<br />
<ul> []Direct sunlight []A secondary shadow ray []Sky ambient lighting []GI probe irradiance evaluated at the hit point</li>
<li>The hit material's actual albedo texture </li></ul>
<br />
The result is written using the engine's existing SSR-buffer format.<br />
<br />
That allows the existing lit-color composite stage to fold the ray-traced result into the scene with no changes to the combine shader. The normal roughness-aware reflection fade is retained.<br />
<br />
<span style="font-weight: bold;">Where it beats SSR</span><br />
<br />
Screen-space reflections can only reflect geometry that is already visible on screen.<br />
<br />
The RT path can reflect:<br />
<ul>
<li>[]Content behind the camera []Objects around corners []Off-screen world geometry []The sky </li></ul>
<br />
It also avoids the characteristic smearing and disappearing reflections that occur when the camera moves or reflected geometry leaves the screen.<br />
<br />
At a demo resolution of <span style="font-weight: bold;">3072 × 1296</span>, the measured light-phase cost was approximately <span style="font-weight: bold;">0.17–0.24 ms</span>.<br />
<br />
That was inexpensive enough that I did not add a half-resolution mode.<br />
<br />
RtReflections(true);<br />
<span style="font-weight: bold;">Demo: Tutorial_14_RTReflections</span><br />
<br />
The scene contains a mirror floor and chrome ball. Press <span style="font-weight: bold;">R</span> to switch between ray-traced reflections and the traditional SSR-plus-cubemap path for a direct comparison.<br />
<br />
<hr />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Combined Ray-Tracing Showcase</span></span><br />
<br />
<span style="font-weight: bold;">Tutorial_14_RTShowcase</span> runs all three ray-traced systems together:<br />
<ul> []Dynamic GI []Sun shadows</li>
<li>Specular reflections </li></ul>
<br />
All three systems share the same automatically constructed TLAS.<br />
<br />
The demo runs in a streamed world containing an animated character. Each feature can be toggled independently:<br />
<ul> []<span style="font-weight: bold;">G</span> — dynamic GI []<span style="font-weight: bold;">H</span> — ray-traced shadows</li>
<li><span style="font-weight: bold;">R</span> — ray-traced reflections </li></ul>
<br />
<hr />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Clustered / Froxel Lighting</span></span><br />
<br />
Clustered lighting addresses a different renderer bottleneck.<br />
<br />
The classic deferred per-light path renders each light with a separate draw call. When many point lights overlap, the renderer may read the G-buffer and blend lighting into the same pixel once for every affecting light.<br />
<br />
The clustered path targets unshadowed point lights such as:<br />
<ul>
<li>[]Torches []Projectiles []Muzzle flashes []Other scenes containing large numbers of dynamic lights </li></ul>
<br />
A compute pass bins lights into a froxel grid containing:<br />
<ul>
<li>[]32 × 18 screen-space tiles []24 exponential depth slices </li></ul>
<br />
A full-screen pass then shades all lights affecting a pixel in one operation using the exact same BRDF as the classic deferred-light path.<br />
<br />
The clustered output was verified for pixel parity against the per-light path.<br />
<br />
<span style="font-weight: bold;">Measured GPU_TIMER_LIGHT results with overlapping, unshadowed point lights:</span><br />
<ul> []64 lights: approximately <span style="font-weight: bold;">5% faster</span> []128 lights: approximately <span style="font-weight: bold;">13% faster</span></li>
<li>256 lights: approximately <span style="font-weight: bold;">19% faster</span> </li></ul>
<br />
The performance advantage grows with light count.<br />
<br />
The honest framing is that the existing per-light renderer is already depth-bounded. It is not a naive full-screen-per-light baseline.<br />
<br />
Clustered lighting is therefore a scalability feature rather than a free performance win in every scene, which is why it remains opt-in.<br />
<br />
Cases that are not handled by the clustered path automatically fall back to the classic renderer:<br />
<ul> []Shadowed lights []The sun []Water rendering []MSAA []Clear-coat materials []Mirrors</li>
<li>Sub-rectangle views </li></ul>
<br />
This keeps rendering parity from being placed at risk.<br />
<br />
ClusteredLights(true);<br />
<span style="font-weight: bold;">Demo: Tutorial_14_ClusteredLights</span><br />
<br />
The demo presents a twilight field containing 256 overlapping lights.<br />
<ul> []<span style="font-weight: bold;">C</span> switches between clustered and per-light rendering []<span style="font-weight: bold;">Up / Down</span> changes the light count, up to 512</li>
<li>The light-phase GPU timer is displayed on screen </li></ul>
<br />
<hr />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Additional Notes</span></span><br />
<ul>
<li>All three systems are currently Vulkan-only.</li>
<li>The two ray-tracing features require ray-query-capable hardware.</li>
<li>On other rendering backends, every API call is a safe no-op. The tutorials still run, but the renderer remains on the classic paths.</li>
<li>The complete stack recently went through an adversarial, multi-pass code review and a synchronization-validation soak.</li>
<li>Synchronization validation reported zero hazards.</li>
<li>Sub-render re-entrancy, resource lifetime across streamed worlds, and thread safety of the retained RT scene received particular attention. </li></ul>
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Design Documents</span></span><br />
<br />
The repository contains detailed design documents for each system:<br />
<ul>
<li>[]docs/rt-shadows-design.md []docs/rt-reflections-design.md []docs/rt-skinned-occluders-design.md []docs/clustered-froxel-lights-design.md </li></ul>
<br />
In this repo:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></description>
			<content:encoded><![CDATA[RTGI+RT SHADOWS+RT Reflections<br />
<br />
<a href="https://postimg.cc/pmjg6tFT" target="_blank"><img src="https://i.postimg.cc/bwVpdyYQ/Screenshot-From-2026-07-09-22-28-11.png" border="0" alt="[Image: Screenshot-From-2026-07-09-22-28-11.png]" /></a><br />
<br />
<br />
No GI, CSM Shadows,<br />
<br />
<a href="https://postimg.cc/RWtjDzHS" target="_blank"><img src="https://i.postimg.cc/W1YTtNpk/Screenshot-From-2026-07-09-22-28-42.png" border="0" alt="[Image: Screenshot-From-2026-07-09-22-28-42.png]" /></a><br />
<br />
<br />
AI written summary:<br />
<br />
I've been extending the engine's Vulkan renderer with three new lighting features:<br />
<ul> []Ray-traced sun shadows []Ray-traced specular reflections</li>
<li>Clustered/froxel lighting </li></ul>
<br />
All three are opt-in and disabled by default. They are also pure runtime toggles, so no shader pak regeneration is needed to use them.<br />
<br />
The two ray-tracing features share a single scene acceleration structure, meaning the cost of “going RT” is paid once.<br />
<br />
Everything below was measured on an <span style="font-weight: bold;">AMD Radeon RX 6650 XT</span>.<br />
<br />
<hr />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">The Shared Foundation: One Scene TLAS, Zero Registration</span></span><br />
<br />
Both RT features use the ray-query acceleration structure originally introduced for dynamic GI.<br />
<br />
The best part is that nothing needs to be manually registered. Enable:<br />
<br />
GIRayTrace.auto_scene = true;<br />
The renderer then feeds every opaque mesh it draws into the acceleration structure automatically. Streamed world areas enter the RT scene as they load, with no additional application code.<br />
<br />
The RT world is the retained union of everything the camera has seen. Looking away from an object does not remove it from shadow, reflection, or GI rays. Meshes leave the acceleration structure only when they are actually destroyed.<br />
<br />
Acceleration-structure rebuilds are recorded directly into the frame command buffer, so there is no per-rebuild GPU stall.<br />
<br />
<span style="font-weight: bold;">Animated characters are supported too.</span><br />
<br />
GIRayTrace.addSkinnedOccluder(mesh, skel);<br />
This GPU-skins the character into its current world pose every frame and builds a per-frame BLAS. The character then:<br />
<ul> []Casts a correctly posed ray-traced shadow []Appears fully textured in ray-traced reflections</li>
<li>Costs only a fraction of a millisecond per character </li></ul>
<br />
One deliberate design choice is that dynamic characters receive GI but do not contribute to it.<br />
<br />
A deforming body inside a sparsely updated probe field causes the surrounding indirect lighting to strobe. To avoid this, the GI probes trace against a second, static-only TLAS.<br />
<br />
This is standard DDGI practice, and it eliminated a very visible “the lighting on my character keeps jumping” artifact.<br />
<br />
<hr />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Ray-Traced Sun Shadows</span></span><br />
<br />
For each pixel, the renderer traces one or more rays toward the sun against the scene TLAS:<br />
<ul>
<li>[]Hit = shadowed []Miss = illuminated </li></ul>
<br />
The detail that makes this a drop-in replacement is that the engine's existing sun-application stage already consumes a pre-resolved full-screen visibility mask.<br />
<br />
The RT pass simply writes that mask. The light shaders remain completely unchanged.<br />
<br />
When RT shadows are enabled, the renderer skips the entire conventional shadow-map pipeline:<br />
<ul> []CSM cascade rasterization []Cascade resolve</li>
<li>Shadow-mask blur </li></ul>
<br />
<span style="font-weight: bold;">Advantages over shadow maps</span><br />
<ul>
<li><span style="font-weight: bold;">No aliasing, cascade seams, or peter-panning.</span><br />
<br />
A thin floating bar can cast a clean, visibly detached shadow that a shadow map cannot reproduce reliably at any practical cascade resolution.</li>
<li><span style="font-weight: bold;">True contact-hardening soft shadows.</span><br />
<br />
Multiple rays are cone-sampled across the sun's disc using a Fibonacci spiral. The pattern is rotated per pixel and temporally jittered so TAA converges the penumbra.<br />
<br />
The result is a shadow that remains sharp at the contact point and becomes progressively softer as the distance between the occluder and receiver increases.</li>
<li><span style="font-weight: bold;">Cost comparable to CSM.</span><br />
<br />
Because RT shadows skip cascade rasterization entirely, the net lighting-phase cost can land near parity with conventional shadow maps in typical scenes.<br />
<br />
An eight-ray, half-resolution soft-shadow pass measured approximately <span style="font-weight: bold;">0.89 ms</span>, roughly the same cost as the CSM path it replaced. </li></ul>
<br />
Full resolution is the default. Half resolution can undersample thin, moving occluders such as animated characters.<br />
<br />
Ray count is the primary performance-versus-quality control.<br />
<br />
RtShadows(true);<br />
<br />
RtShadowQuality(0.03f, 4);<br />
The first quality parameter controls sun-disc softness. A softness value of zero produces hard shadows. The second parameter controls the number of rays per pixel.<br />
<br />
<span style="font-weight: bold;">Demo: Tutorial_14_RTShadows</span><br />
<ul> []<span style="font-weight: bold;">R</span> switches live between ray-traced shadows and shadow maps []<span style="font-weight: bold;">- / =</span> adjusts shadow softness</li>
<li>An on-screen HUD displays GPU cost </li></ul>
<br />
The initial implementation supports the sun only.<br />
<br />
<hr />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Ray-Traced Specular Reflections</span></span><br />
<br />
The reflection pass traces one:<br />
<br />
reflect(eye, normal)<br />
ray per pixel against the same scene TLAS.<br />
<br />
Reflection hits are fully lit and textured using:<br />
<ul> []Direct sunlight []A secondary shadow ray []Sky ambient lighting []GI probe irradiance evaluated at the hit point</li>
<li>The hit material's actual albedo texture </li></ul>
<br />
The result is written using the engine's existing SSR-buffer format.<br />
<br />
That allows the existing lit-color composite stage to fold the ray-traced result into the scene with no changes to the combine shader. The normal roughness-aware reflection fade is retained.<br />
<br />
<span style="font-weight: bold;">Where it beats SSR</span><br />
<br />
Screen-space reflections can only reflect geometry that is already visible on screen.<br />
<br />
The RT path can reflect:<br />
<ul>
<li>[]Content behind the camera []Objects around corners []Off-screen world geometry []The sky </li></ul>
<br />
It also avoids the characteristic smearing and disappearing reflections that occur when the camera moves or reflected geometry leaves the screen.<br />
<br />
At a demo resolution of <span style="font-weight: bold;">3072 × 1296</span>, the measured light-phase cost was approximately <span style="font-weight: bold;">0.17–0.24 ms</span>.<br />
<br />
That was inexpensive enough that I did not add a half-resolution mode.<br />
<br />
RtReflections(true);<br />
<span style="font-weight: bold;">Demo: Tutorial_14_RTReflections</span><br />
<br />
The scene contains a mirror floor and chrome ball. Press <span style="font-weight: bold;">R</span> to switch between ray-traced reflections and the traditional SSR-plus-cubemap path for a direct comparison.<br />
<br />
<hr />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Combined Ray-Tracing Showcase</span></span><br />
<br />
<span style="font-weight: bold;">Tutorial_14_RTShowcase</span> runs all three ray-traced systems together:<br />
<ul> []Dynamic GI []Sun shadows</li>
<li>Specular reflections </li></ul>
<br />
All three systems share the same automatically constructed TLAS.<br />
<br />
The demo runs in a streamed world containing an animated character. Each feature can be toggled independently:<br />
<ul> []<span style="font-weight: bold;">G</span> — dynamic GI []<span style="font-weight: bold;">H</span> — ray-traced shadows</li>
<li><span style="font-weight: bold;">R</span> — ray-traced reflections </li></ul>
<br />
<hr />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Clustered / Froxel Lighting</span></span><br />
<br />
Clustered lighting addresses a different renderer bottleneck.<br />
<br />
The classic deferred per-light path renders each light with a separate draw call. When many point lights overlap, the renderer may read the G-buffer and blend lighting into the same pixel once for every affecting light.<br />
<br />
The clustered path targets unshadowed point lights such as:<br />
<ul>
<li>[]Torches []Projectiles []Muzzle flashes []Other scenes containing large numbers of dynamic lights </li></ul>
<br />
A compute pass bins lights into a froxel grid containing:<br />
<ul>
<li>[]32 × 18 screen-space tiles []24 exponential depth slices </li></ul>
<br />
A full-screen pass then shades all lights affecting a pixel in one operation using the exact same BRDF as the classic deferred-light path.<br />
<br />
The clustered output was verified for pixel parity against the per-light path.<br />
<br />
<span style="font-weight: bold;">Measured GPU_TIMER_LIGHT results with overlapping, unshadowed point lights:</span><br />
<ul> []64 lights: approximately <span style="font-weight: bold;">5% faster</span> []128 lights: approximately <span style="font-weight: bold;">13% faster</span></li>
<li>256 lights: approximately <span style="font-weight: bold;">19% faster</span> </li></ul>
<br />
The performance advantage grows with light count.<br />
<br />
The honest framing is that the existing per-light renderer is already depth-bounded. It is not a naive full-screen-per-light baseline.<br />
<br />
Clustered lighting is therefore a scalability feature rather than a free performance win in every scene, which is why it remains opt-in.<br />
<br />
Cases that are not handled by the clustered path automatically fall back to the classic renderer:<br />
<ul> []Shadowed lights []The sun []Water rendering []MSAA []Clear-coat materials []Mirrors</li>
<li>Sub-rectangle views </li></ul>
<br />
This keeps rendering parity from being placed at risk.<br />
<br />
ClusteredLights(true);<br />
<span style="font-weight: bold;">Demo: Tutorial_14_ClusteredLights</span><br />
<br />
The demo presents a twilight field containing 256 overlapping lights.<br />
<ul> []<span style="font-weight: bold;">C</span> switches between clustered and per-light rendering []<span style="font-weight: bold;">Up / Down</span> changes the light count, up to 512</li>
<li>The light-phase GPU timer is displayed on screen </li></ul>
<br />
<hr />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Additional Notes</span></span><br />
<ul>
<li>All three systems are currently Vulkan-only.</li>
<li>The two ray-tracing features require ray-query-capable hardware.</li>
<li>On other rendering backends, every API call is a safe no-op. The tutorials still run, but the renderer remains on the classic paths.</li>
<li>The complete stack recently went through an adversarial, multi-pass code review and a synchronization-validation soak.</li>
<li>Synchronization validation reported zero hazards.</li>
<li>Sub-render re-entrancy, resource lifetime across streamed worlds, and thread safety of the retained RT scene received particular attention. </li></ul>
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Design Documents</span></span><br />
<br />
The repository contains detailed design documents for each system:<br />
<ul>
<li>[]docs/rt-shadows-design.md []docs/rt-reflections-design.md []docs/rt-skinned-occluders-design.md []docs/clustered-froxel-lights-design.md </li></ul>
<br />
In this repo:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Global Illumination (GI)]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11832</link>
			<pubDate>Wed, 08 Jul 2026 12:44:32 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11832</guid>
			<description><![CDATA[It's a subtle lighting feature, but in dark scenes it can make a difference. <br />
Intensity of GI must be increased to really see it especially if there is an ambient light source. <br />
RT hardware accelerated GI has much better performance than pure software (rasterized) GI.<br />
<br />
GI ON, intensity cranked so effect is obvious:<br />
<a href="https://postimg.cc/hJFQ5BH1" target="_blank"><img src="https://i.postimg.cc/HLn0jWzR/Screenshot-From-2026-07-08-08-19-29.png" border="0" alt="[Image: Screenshot-From-2026-07-08-08-19-29.png]" /></a><br />
<br />
<br />
GI OFF:<br />
<a href="https://postimg.cc/Ty8546Zq" target="_blank"><img src="https://i.postimg.cc/PqJbNfyc/Screenshot-From-2026-07-08-08-19-36.png" border="0" alt="[Image: Screenshot-From-2026-07-08-08-19-36.png]" /></a><br />
<br />
<br />
GI ON , yellow arrow is sun direction:<br />
<a href="https://postimg.cc/gw9LTWf8" target="_blank"><img src="https://i.postimg.cc/nLrKMV2R/Screenshot-From-2026-07-08-08-20-39.png" border="0" alt="[Image: Screenshot-From-2026-07-08-08-20-39.png]" /></a><br />
<br />
GI OFF:<br />
<a href="https://postimg.cc/rDRtjmYm" target="_blank"><img src="https://i.postimg.cc/RZXQMJ7c/Screenshot-From-2026-07-08-08-20-51.png" border="0" alt="[Image: Screenshot-From-2026-07-08-08-20-51.png]" /></a><br />
<br />
Left Side GI ON / Right Side GI OFF split screen:<br />
<a href="https://postimg.cc/4nJKHw6c" target="_blank"><img src="https://i.postimg.cc/MZB14FX9/Screenshot-From-2026-07-08-08-38-31.png" border="0" alt="[Image: Screenshot-From-2026-07-08-08-38-31.png]" /></a><br />
<br />
AI Generated Summary:<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Fully Dynamic Global Illumination for Esenthel — No Baking, No Authoring</span></span><br />
<br />
I added a complete real-time diffuse Global Illumination system to the engine’s deferred renderer.<br />
<br />
The headline: <span style="font-weight: bold;">zero authoring</span> — no baked lightmaps, no hand-placed probes, and no precompute step.<br />
<br />
You flip one switch and neutral surfaces start picking up colored bounce light from the walls, terrain, and objects around them. Time-of-day is fully dynamic. The system is Vulkan-only because it needs the compute path, and it is off by default. Existing scenes render byte-for-byte identically until an app explicitly opts in.<br />
<br />
There are two interchangeable producers that fill the lighting probes:<br />
<ul>
<li>[]<span style="font-weight: bold;">Rasterized cube capture</span> — works on any GPU and is always available as the fallback.<br />
[]<span style="font-weight: bold;">Hardware ray-traced capture</span> — optional ray-query path for GPUs that support it.<br />
</li></ul>
<br />
<span style="font-size: large;"><span style="font-weight: bold;">How it works</span></span><br />
<ul>
<li><span style="font-weight: bold;">DDGI-style probe grid</span><br />
<br />
A camera-centered 3D grid of irradiance probes follows the camera. The deferred light pass swaps the old flat ambient term for trilinearly interpolated probe irradiance.</li>
<li><span style="font-weight: bold;">Toroidal scrolling</span><br />
<br />
As the camera moves, the probe volume scrolls toroidally, addressed by integer cell index, instead of re-capturing everything. This prevents a re-capture storm when moving through the world.<br />
<br />
This also makes it floating-origin and large-world safe because the GI volume effectively rebases for free.</li>
<li><span style="font-weight: bold;">Cascades</span><br />
<br />
Range comes from nested cascades at geometrically increasing spacing. Three cascades cover roughly 24 m to 216 m, with a cross-cascade seam blend so there is no hard visible ring at the cascade boundaries.</li>
<li><span style="font-weight: bold;">DDGI-standard atlases</span><br />
<br />
Each probe captures visibility, either rasterized into a small cube or ray-traced, then compute integrates the result into octahedral irradiance and Chebyshev depth-moment atlases.<br />
<br />
The depth moments suppress light leaking through thin walls, and a surface-normal bias helps as well.</li>
<li><span style="font-weight: bold;">Decoupled capture vs. relight</span><br />
<br />
Geometry capture and lighting relight are separated. Both are hard-capped per frame.<br />
<br />
The expensive geometry capture is time-sliced and budgeted, so the cost stays bounded. A static scene captures nothing, so it is free when you are not moving. Irradiance converges through temporal blending.</li>
<li><span style="font-weight: bold;">Free time-of-day relight</span><br />
<br />
An optional producer bakes a lighting-independent cube once and relights it each frame with per-bounce sun shadowing. Moving the sun costs no re-capture.</li>
<li><span style="font-weight: bold;">Analytic re-seeding</span><br />
<br />
Scrolled-in probe slots are analytically re-seeded, which kills stale-bounce ghosting during fast movement.</li>
<li><span style="font-weight: bold;">Half-resolution evaluation</span><br />
<br />
Half-res evaluation plus depth-aware bilateral upscale keeps the light-pass cost down.</li>
<li><span style="font-weight: bold;">Optional SSGI</span><br />
<br />
A screen-space near-field bounce layer can be enabled on top of the probe GI for close-range contact GI.<br />
</li></ul>
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Hardware ray-traced path — opt-in, additive</span></span><br />
<br />
When ray-query hardware is present and a scene acceleration structure is registered, probe capture can be ray-traced instead of rasterized. This gives correct per-direction occlusion and bounce with no cube-face seams.<br />
<ul>
<li><span style="font-weight: bold;">RT occlusion moments</span><br />
<br />
GI.raytrace replaces the depth-cube producer with ray-traced occlusion moments. The light pass is unchanged.</li>
<li><span style="font-weight: bold;">RT bounce radiance</span><br />
<br />
GI.raytrace_radiance traces the diffuse bounce itself using cosine-weighted hemisphere gathering with Fibonacci-spiral rays.<br />
<br />
It supports textured and average-color albedo. Terrain with no UVs samples the texture’s mean color. It uses the correct hit-surface normal, NdotL, sun shadow rays, radiosity-style multi-bounce, and a per-frame sky-only cube so miss rays sample the real rendered sky.<br />
<br />
It reaches pixel parity with the rasterized producer at the same intensity on both Cornell and world scenes.</li>
<li><span style="font-weight: bold;">Zero-registration mode</span><br />
<br />
GIRayTrace.auto_scene = true lets the renderer feed every opaque mesh it draws into the acceleration structure automatically.<br />
<br />
Streamed world areas enter the TLAS as they draw and drop out as they stream away, with no app code required. Rebuilds are recorded into the frame command buffer, so there is no GPU stall.<br />
</li></ul>
<br />
The system is fully hardware-gated and TLAS-gated. If RT is off, unsupported, or unregistered, it falls back to rasterized capture. The software DDGI path is untouched.<br />
<br />
Verified working on both AMD RADV and NVIDIA.<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Demos</span></span><br />
<br />
Vulkan build, under Tutorials/Source/14 - Game Basics/<br />
<br />
<span style="font-weight: bold;">Tutorial_14_DynamicGI</span><br />
The basics: a neutral scene picking up colored bounce light, with live time-of-day. This is the color-bleed proof.<br />
<br />
<span style="font-weight: bold;">Tutorial_14_WorldGI</span><br />
GI over a real streamed Game::World with terrain and props, plus point-light and time controls.<br />
<br />
<span style="font-weight: bold;">Tutorial_14_LargeWorldGI</span><br />
GI running under the large-world floating origin. This proves it survives rebases without re-warming.<br />
<br />
<span style="font-weight: bold;">Tutorial_14_GIShowcase</span><br />
The “all the knobs” demo. A Cornell room with split-screen A/B.<br />
<br />
Controls include:<br />
<ul>
<li>[]<span style="font-weight: bold;">V</span> — split-screen A/B<br />
[]<span style="font-weight: bold;">- / =</span> — intensity<br />
[]<span style="font-weight: bold;">1 / 2 / 3</span> — cascade count<br />
[]<span style="font-weight: bold;">R</span> — relight<br />
[]<span style="font-weight: bold;">B</span> — Chebyshev<br />
[]<span style="font-weight: bold;">A</span> — TAA<br />
[]<span style="font-weight: bold;">N</span> — SSGI<br />
[]<span style="font-weight: bold;">Y</span> — RT moments<br />
[]<span style="font-weight: bold;">U</span> — RT radiance<br />
[]Sun, light, and wall toggles<br />
</li></ul>
<br />
<span style="font-weight: bold;">Tutorial_14_RTWorldGI</span><br />
The payoff: ray-traced GI over a playable streamed world via the zero-registration auto-scene hook.<br />
<br />
Controls include:<br />
<ul>
<li>[]<span style="font-weight: bold;">WASD</span> — move<br />
[]<span style="font-weight: bold;">G</span> — GI on/off, showing near-black ambient vs. bounced sunlight<br />
[]<span style="font-weight: bold;">U</span> — RT radiance vs. rasterized capture<br />
[]<span style="font-weight: bold;">Y</span> — RT moments<br />
[]<span style="font-weight: bold;">T</span> — time-of-day<br />
[]<span style="font-weight: bold;">- / =</span> — intensity<br />
</li></ul>
<br />
<span style="font-size: large;"><span style="font-weight: bold;">How it is turned off and on</span></span><br />
<br />
Everything defaults off at the engine level. These are the relevant defaults on the global GI object in Engine/H/Graphics/GI.h, plus GIRayTrace:<br />
<br />
<span style="font-weight: bold;">GI.enabled</span><br />
Default: false<br />
Turns on the whole system. Off means legacy flat ambient.<br />
<br />
<span style="font-weight: bold;">GIRayTrace.auto_scene</span><br />
Default: false<br />
Turns on zero-registration RT scene feeding.<br />
<br />
<span style="font-weight: bold;">GI.raytrace</span><br />
Default: false<br />
Turns on ray-traced occlusion moments.<br />
<br />
<span style="font-weight: bold;">GI.raytrace_radiance</span><br />
Default: false<br />
Turns on ray-traced bounce radiance.<br />
<br />
<span style="font-weight: bold;">GI.relight</span><br />
Default: false<br />
Turns on the free-time-of-day relight producer.<br />
<br />
<span style="font-weight: bold;">GI.ssgi</span><br />
Default: false<br />
Turns on screen-space near-field bounce.<br />
<br />
<span style="font-weight: bold;">GI.capture_shadows</span><br />
Default: false<br />
Turns on sun-shadowed lit-cube capture.<br />
<br />
<span style="font-weight: bold;">GI.chebyshev</span><br />
Default: true<br />
Turns on leak suppression when GI is enabled.<br />
<br />
<span style="font-weight: bold;">GI.half_res</span><br />
Default: true<br />
Turns on half-res evaluation and upscale.<br />
<br />
<span style="font-weight: bold;">GI.auto_light</span><br />
Default: true<br />
Derives sun and sky lighting from the live scene.<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Canonical app opt-in path</span></span><br />
<br />
An app opts in from InitPre or Init:<br />
<br />
GI.enabled = true;<br />
GI.intensity = 2.0f;<br />
GIRayTrace.auto_scene = true;<br />
GI.raytrace_radiance = true;<br />
<br />
GI.enabled is the master switch. Nothing renders GI without it.<br />
<br />
GI.intensity controls the indirect multiplier, where 1 is physical.<br />
<br />
GIRayTrace.auto_scene is optional and lets the renderer register the scene for ray tracing automatically.<br />
<br />
GI.raytrace_radiance is optional and enables ray-traced bounce radiance. It falls back if unsupported.<br />
<br />
There are also EE_GI environment-variable overrides, such as:<br />
<br />
EE_GI=1<br />
EE_GI_RAYTRACE_RADIANCE=1<br />
EE_GI_INTENSITY=...<br />
<br />
Those exist mainly for headless A/B testing. They are not the intended shipping path.<br />
<br />
Beyond the app switch, GI is gated on backend support. GI.active() requires the Vulkan compute path. The RT path is additionally gated on ray-query hardware.<br />
<br />
So on a non-Vulkan backend, or a GPU without ray-query support, the relevant paths simply no-op or fall back.<br />
<br />
Net result: if you do not opt in, nothing about your scene changes.<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Build and run one demo</span></span><br />
<br />
cmake --build cmake-build-release-vulkan --target Tutorial_14_GIShowcase -j &#36;(nproc)<br />
<br />
EE_NO_MSG_BOX=1 ./Tutorials/Tutorial_14_GIShowcase<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Documentation</span></span><br />
<br />
Full design notes and knob reference are in:<br />
<ul>
<li>[]The README’s Dynamic Global Illumination section<br />
[]docs/gi-raytracing-design.md<br />
</li></ul>
<br />
Available in this engine fork:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></description>
			<content:encoded><![CDATA[It's a subtle lighting feature, but in dark scenes it can make a difference. <br />
Intensity of GI must be increased to really see it especially if there is an ambient light source. <br />
RT hardware accelerated GI has much better performance than pure software (rasterized) GI.<br />
<br />
GI ON, intensity cranked so effect is obvious:<br />
<a href="https://postimg.cc/hJFQ5BH1" target="_blank"><img src="https://i.postimg.cc/HLn0jWzR/Screenshot-From-2026-07-08-08-19-29.png" border="0" alt="[Image: Screenshot-From-2026-07-08-08-19-29.png]" /></a><br />
<br />
<br />
GI OFF:<br />
<a href="https://postimg.cc/Ty8546Zq" target="_blank"><img src="https://i.postimg.cc/PqJbNfyc/Screenshot-From-2026-07-08-08-19-36.png" border="0" alt="[Image: Screenshot-From-2026-07-08-08-19-36.png]" /></a><br />
<br />
<br />
GI ON , yellow arrow is sun direction:<br />
<a href="https://postimg.cc/gw9LTWf8" target="_blank"><img src="https://i.postimg.cc/nLrKMV2R/Screenshot-From-2026-07-08-08-20-39.png" border="0" alt="[Image: Screenshot-From-2026-07-08-08-20-39.png]" /></a><br />
<br />
GI OFF:<br />
<a href="https://postimg.cc/rDRtjmYm" target="_blank"><img src="https://i.postimg.cc/RZXQMJ7c/Screenshot-From-2026-07-08-08-20-51.png" border="0" alt="[Image: Screenshot-From-2026-07-08-08-20-51.png]" /></a><br />
<br />
Left Side GI ON / Right Side GI OFF split screen:<br />
<a href="https://postimg.cc/4nJKHw6c" target="_blank"><img src="https://i.postimg.cc/MZB14FX9/Screenshot-From-2026-07-08-08-38-31.png" border="0" alt="[Image: Screenshot-From-2026-07-08-08-38-31.png]" /></a><br />
<br />
AI Generated Summary:<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Fully Dynamic Global Illumination for Esenthel — No Baking, No Authoring</span></span><br />
<br />
I added a complete real-time diffuse Global Illumination system to the engine’s deferred renderer.<br />
<br />
The headline: <span style="font-weight: bold;">zero authoring</span> — no baked lightmaps, no hand-placed probes, and no precompute step.<br />
<br />
You flip one switch and neutral surfaces start picking up colored bounce light from the walls, terrain, and objects around them. Time-of-day is fully dynamic. The system is Vulkan-only because it needs the compute path, and it is off by default. Existing scenes render byte-for-byte identically until an app explicitly opts in.<br />
<br />
There are two interchangeable producers that fill the lighting probes:<br />
<ul>
<li>[]<span style="font-weight: bold;">Rasterized cube capture</span> — works on any GPU and is always available as the fallback.<br />
[]<span style="font-weight: bold;">Hardware ray-traced capture</span> — optional ray-query path for GPUs that support it.<br />
</li></ul>
<br />
<span style="font-size: large;"><span style="font-weight: bold;">How it works</span></span><br />
<ul>
<li><span style="font-weight: bold;">DDGI-style probe grid</span><br />
<br />
A camera-centered 3D grid of irradiance probes follows the camera. The deferred light pass swaps the old flat ambient term for trilinearly interpolated probe irradiance.</li>
<li><span style="font-weight: bold;">Toroidal scrolling</span><br />
<br />
As the camera moves, the probe volume scrolls toroidally, addressed by integer cell index, instead of re-capturing everything. This prevents a re-capture storm when moving through the world.<br />
<br />
This also makes it floating-origin and large-world safe because the GI volume effectively rebases for free.</li>
<li><span style="font-weight: bold;">Cascades</span><br />
<br />
Range comes from nested cascades at geometrically increasing spacing. Three cascades cover roughly 24 m to 216 m, with a cross-cascade seam blend so there is no hard visible ring at the cascade boundaries.</li>
<li><span style="font-weight: bold;">DDGI-standard atlases</span><br />
<br />
Each probe captures visibility, either rasterized into a small cube or ray-traced, then compute integrates the result into octahedral irradiance and Chebyshev depth-moment atlases.<br />
<br />
The depth moments suppress light leaking through thin walls, and a surface-normal bias helps as well.</li>
<li><span style="font-weight: bold;">Decoupled capture vs. relight</span><br />
<br />
Geometry capture and lighting relight are separated. Both are hard-capped per frame.<br />
<br />
The expensive geometry capture is time-sliced and budgeted, so the cost stays bounded. A static scene captures nothing, so it is free when you are not moving. Irradiance converges through temporal blending.</li>
<li><span style="font-weight: bold;">Free time-of-day relight</span><br />
<br />
An optional producer bakes a lighting-independent cube once and relights it each frame with per-bounce sun shadowing. Moving the sun costs no re-capture.</li>
<li><span style="font-weight: bold;">Analytic re-seeding</span><br />
<br />
Scrolled-in probe slots are analytically re-seeded, which kills stale-bounce ghosting during fast movement.</li>
<li><span style="font-weight: bold;">Half-resolution evaluation</span><br />
<br />
Half-res evaluation plus depth-aware bilateral upscale keeps the light-pass cost down.</li>
<li><span style="font-weight: bold;">Optional SSGI</span><br />
<br />
A screen-space near-field bounce layer can be enabled on top of the probe GI for close-range contact GI.<br />
</li></ul>
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Hardware ray-traced path — opt-in, additive</span></span><br />
<br />
When ray-query hardware is present and a scene acceleration structure is registered, probe capture can be ray-traced instead of rasterized. This gives correct per-direction occlusion and bounce with no cube-face seams.<br />
<ul>
<li><span style="font-weight: bold;">RT occlusion moments</span><br />
<br />
GI.raytrace replaces the depth-cube producer with ray-traced occlusion moments. The light pass is unchanged.</li>
<li><span style="font-weight: bold;">RT bounce radiance</span><br />
<br />
GI.raytrace_radiance traces the diffuse bounce itself using cosine-weighted hemisphere gathering with Fibonacci-spiral rays.<br />
<br />
It supports textured and average-color albedo. Terrain with no UVs samples the texture’s mean color. It uses the correct hit-surface normal, NdotL, sun shadow rays, radiosity-style multi-bounce, and a per-frame sky-only cube so miss rays sample the real rendered sky.<br />
<br />
It reaches pixel parity with the rasterized producer at the same intensity on both Cornell and world scenes.</li>
<li><span style="font-weight: bold;">Zero-registration mode</span><br />
<br />
GIRayTrace.auto_scene = true lets the renderer feed every opaque mesh it draws into the acceleration structure automatically.<br />
<br />
Streamed world areas enter the TLAS as they draw and drop out as they stream away, with no app code required. Rebuilds are recorded into the frame command buffer, so there is no GPU stall.<br />
</li></ul>
<br />
The system is fully hardware-gated and TLAS-gated. If RT is off, unsupported, or unregistered, it falls back to rasterized capture. The software DDGI path is untouched.<br />
<br />
Verified working on both AMD RADV and NVIDIA.<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Demos</span></span><br />
<br />
Vulkan build, under Tutorials/Source/14 - Game Basics/<br />
<br />
<span style="font-weight: bold;">Tutorial_14_DynamicGI</span><br />
The basics: a neutral scene picking up colored bounce light, with live time-of-day. This is the color-bleed proof.<br />
<br />
<span style="font-weight: bold;">Tutorial_14_WorldGI</span><br />
GI over a real streamed Game::World with terrain and props, plus point-light and time controls.<br />
<br />
<span style="font-weight: bold;">Tutorial_14_LargeWorldGI</span><br />
GI running under the large-world floating origin. This proves it survives rebases without re-warming.<br />
<br />
<span style="font-weight: bold;">Tutorial_14_GIShowcase</span><br />
The “all the knobs” demo. A Cornell room with split-screen A/B.<br />
<br />
Controls include:<br />
<ul>
<li>[]<span style="font-weight: bold;">V</span> — split-screen A/B<br />
[]<span style="font-weight: bold;">- / =</span> — intensity<br />
[]<span style="font-weight: bold;">1 / 2 / 3</span> — cascade count<br />
[]<span style="font-weight: bold;">R</span> — relight<br />
[]<span style="font-weight: bold;">B</span> — Chebyshev<br />
[]<span style="font-weight: bold;">A</span> — TAA<br />
[]<span style="font-weight: bold;">N</span> — SSGI<br />
[]<span style="font-weight: bold;">Y</span> — RT moments<br />
[]<span style="font-weight: bold;">U</span> — RT radiance<br />
[]Sun, light, and wall toggles<br />
</li></ul>
<br />
<span style="font-weight: bold;">Tutorial_14_RTWorldGI</span><br />
The payoff: ray-traced GI over a playable streamed world via the zero-registration auto-scene hook.<br />
<br />
Controls include:<br />
<ul>
<li>[]<span style="font-weight: bold;">WASD</span> — move<br />
[]<span style="font-weight: bold;">G</span> — GI on/off, showing near-black ambient vs. bounced sunlight<br />
[]<span style="font-weight: bold;">U</span> — RT radiance vs. rasterized capture<br />
[]<span style="font-weight: bold;">Y</span> — RT moments<br />
[]<span style="font-weight: bold;">T</span> — time-of-day<br />
[]<span style="font-weight: bold;">- / =</span> — intensity<br />
</li></ul>
<br />
<span style="font-size: large;"><span style="font-weight: bold;">How it is turned off and on</span></span><br />
<br />
Everything defaults off at the engine level. These are the relevant defaults on the global GI object in Engine/H/Graphics/GI.h, plus GIRayTrace:<br />
<br />
<span style="font-weight: bold;">GI.enabled</span><br />
Default: false<br />
Turns on the whole system. Off means legacy flat ambient.<br />
<br />
<span style="font-weight: bold;">GIRayTrace.auto_scene</span><br />
Default: false<br />
Turns on zero-registration RT scene feeding.<br />
<br />
<span style="font-weight: bold;">GI.raytrace</span><br />
Default: false<br />
Turns on ray-traced occlusion moments.<br />
<br />
<span style="font-weight: bold;">GI.raytrace_radiance</span><br />
Default: false<br />
Turns on ray-traced bounce radiance.<br />
<br />
<span style="font-weight: bold;">GI.relight</span><br />
Default: false<br />
Turns on the free-time-of-day relight producer.<br />
<br />
<span style="font-weight: bold;">GI.ssgi</span><br />
Default: false<br />
Turns on screen-space near-field bounce.<br />
<br />
<span style="font-weight: bold;">GI.capture_shadows</span><br />
Default: false<br />
Turns on sun-shadowed lit-cube capture.<br />
<br />
<span style="font-weight: bold;">GI.chebyshev</span><br />
Default: true<br />
Turns on leak suppression when GI is enabled.<br />
<br />
<span style="font-weight: bold;">GI.half_res</span><br />
Default: true<br />
Turns on half-res evaluation and upscale.<br />
<br />
<span style="font-weight: bold;">GI.auto_light</span><br />
Default: true<br />
Derives sun and sky lighting from the live scene.<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Canonical app opt-in path</span></span><br />
<br />
An app opts in from InitPre or Init:<br />
<br />
GI.enabled = true;<br />
GI.intensity = 2.0f;<br />
GIRayTrace.auto_scene = true;<br />
GI.raytrace_radiance = true;<br />
<br />
GI.enabled is the master switch. Nothing renders GI without it.<br />
<br />
GI.intensity controls the indirect multiplier, where 1 is physical.<br />
<br />
GIRayTrace.auto_scene is optional and lets the renderer register the scene for ray tracing automatically.<br />
<br />
GI.raytrace_radiance is optional and enables ray-traced bounce radiance. It falls back if unsupported.<br />
<br />
There are also EE_GI environment-variable overrides, such as:<br />
<br />
EE_GI=1<br />
EE_GI_RAYTRACE_RADIANCE=1<br />
EE_GI_INTENSITY=...<br />
<br />
Those exist mainly for headless A/B testing. They are not the intended shipping path.<br />
<br />
Beyond the app switch, GI is gated on backend support. GI.active() requires the Vulkan compute path. The RT path is additionally gated on ray-query hardware.<br />
<br />
So on a non-Vulkan backend, or a GPU without ray-query support, the relevant paths simply no-op or fall back.<br />
<br />
Net result: if you do not opt in, nothing about your scene changes.<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Build and run one demo</span></span><br />
<br />
cmake --build cmake-build-release-vulkan --target Tutorial_14_GIShowcase -j &#36;(nproc)<br />
<br />
EE_NO_MSG_BOX=1 ./Tutorials/Tutorial_14_GIShowcase<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Documentation</span></span><br />
<br />
Full design notes and knob reference are in:<br />
<ul>
<li>[]The README’s Dynamic Global Illumination section<br />
[]docs/gi-raytracing-design.md<br />
</li></ul>
<br />
Available in this engine fork:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Improved Water]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11828</link>
			<pubDate>Mon, 22 Jun 2026 21:29:11 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11828</guid>
			<description><![CDATA[Video:<br />
<br />
<a href="https://streamable.com/wfnoh9" target="_blank">https://streamable.com/wfnoh9</a><br />
<br />
Pics:<br />
<br />
<a href="https://postimg.cc/9zSBqkDZ" target="_blank"><img src="https://i.postimg.cc/ZKYsXtZ7/Screenshot-From-2026-06-22-17-05-16.png" border="0" alt="[Image: Screenshot-From-2026-06-22-17-05-16.png]" /></a><br />
<br />
<a href="https://postimg.cc/z3MkRQyV" target="_blank"><img src="https://i.postimg.cc/CLMr9TYH/Screenshot-From-2026-06-22-17-06-14.png" border="0" alt="[Image: Screenshot-From-2026-06-22-17-06-14.png]" /></a><br />
<br />
<br />
<br />
<br />
AI written summary:<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Water Rendering Overhaul — Foam, Depth Color, Caustics, Gerstner Swell</span></span><br />
<br />
I've spent some time giving the water system a proper pass.<br />
<br />
The engine's water was already strong: scrolling normal maps, Fresnel, planar + cubemap + SSR reflection, depth refraction with leak detection, density absorption, underwater fog, capsule ripples, and large-world support.<br />
<br />
This work fills in the pieces that were missing or weak:<br />
<ul>
<li>Shoreline foam</li>
<li>Depth-graded surface color</li>
<li>Procedural caustics</li>
<li>Whitecap foam</li>
<li>A real Gerstner wave model with choppiness and wave-speed control<br />
</li></ul>
<br />
Everything is driven by new per-material parameters on <span style="font-weight: bold;">WaterMtrl</span>, all exposed in the <span style="font-weight: bold;">Titan Editor → Water Material</span> editor.<br />
<br />
The subtle effects — foam, depth color, and caustics — are on by default at low intensity, so existing water looks a little better out of the box without re-authoring.<br />
<br />
The heavier Gerstner swell is opt-in:<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Wave Count &gt; 0</code></div></div>
<br />
<span style="font-size: large;"><span style="font-weight: bold;">What's New</span></span><br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Depth-Graded Surface Color</span></span><br />
<br />
The surface now blends from a <span style="font-weight: bold;">Shallow Color</span> in thin water to the base <span style="font-weight: bold;">Color</span> in deep water, using the water-column depth the shader already computes.<br />
<br />
This works especially well for carved 3–100 m procedural lakes/rivers and beaches.<br />
<br />
<span style="font-weight: bold;">Params:</span><br />
<ul>
<li><span style="font-weight: bold;">Shallow Color</span></li>
<li><span style="font-weight: bold;">Depth Color Distance</span> — metres over which it blends; <span style="font-weight: bold;">0 = off</span><br />
</li></ul>
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Animated Shoreline Foam</span></span><br />
<br />
A foam band now appears where the water column gets thin — shorelines, around rocks, and anywhere water meets geometry.<br />
<br />
The animation is fully procedural value noise, so no texture asset is required. It churns organically instead of reading as a static pattern.<br />
<br />
<span style="font-weight: bold;">Params:</span><br />
<ul>
<li><span style="font-weight: bold;">Foam Color</span></li>
<li><span style="font-weight: bold;">Foam Amount</span> — <span style="font-weight: bold;">0 = off</span></li>
<li><span style="font-weight: bold;">Foam Depth</span> — band width in metres<br />
</li></ul>
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Procedural Caustics</span></span><br />
<br />
A cheap two-layer cellular caustic network is projected onto the submerged background and faded by depth.<br />
<br />
It is strongest in clear shallows and disappears in deep or opaque water. There is no texture binding, and it is effectively free where the water is deep.<br />
<br />
<span style="font-weight: bold;">Params:</span><br />
<ul>
<li><span style="font-weight: bold;">Caustic Scale</span> — <span style="font-weight: bold;">0 = off</span></li>
<li><span style="font-weight: bold;">Caustic Intensity</span><br />
</li></ul>
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Gerstner Swell — Ocean Technique</span></span><br />
<br />
The Ocean technique now supports real summed-wave vertex displacement with analytic normals.<br />
<br />
Set:<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Wave Count &gt; 0</code></div></div>
<br />
to switch the Ocean plane from the legacy texture-bump waves to a proper Gerstner wave sum.<br />
<br />
Up to 8 waves are synthesized from a few high-level controls.<br />
<br />
Includes:<br />
<ul>
<li><span style="font-weight: bold;">Choppiness</span> — horizontal crest displacement for peaked tops and flatter troughs, which is the defining Gerstner term.</li>
<li><span style="font-weight: bold;">Wave Speed</span> — multiplier on physical deep-water dispersion. <span style="font-weight: bold;">1 = realistic</span>; lower values produce lazy swell, higher values produce faster chop.</li>
<li><span style="font-weight: bold;">Per-wavelength distance fade</span> — long swells carry to distance while short chop settles out.</li>
<li><span style="font-weight: bold;">Global horizon envelope</span> — flattens displacement before the horizon so the far ocean does not alias into shimmer or grid moiré.<br />
</li></ul>
<br />
<span style="font-weight: bold;">Params:</span><br />
<ul>
<li><span style="font-weight: bold;">Wave Count</span></li>
<li><span style="font-weight: bold;">Wave Length</span></li>
<li><span style="font-weight: bold;">Wave Amplitude</span></li>
<li><span style="font-weight: bold;">Wave Choppiness</span></li>
<li><span style="font-weight: bold;">Wave Wind</span> — direction</li>
<li><span style="font-weight: bold;">Wave Speed</span><br />
</li></ul>
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Whitecap / Crest Foam</span></span><br />
<br />
Slope-based foam now builds on steep wave faces as the sea picks up.<br />
<br />
It is biased onto the downwind, breaking face of the wave rather than appearing symmetrically on both sides.<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Demo</span></span><br />
<br />
<span style="font-weight: bold;">Tutorial_14_OceanWater</span> shows everything running on the global sea over a sloped, bumpy seabed rising to a beach.<br />
<br />
Everything is runtime-tunable:<br />
<ul>
<li><span style="font-weight: bold;">Z / X</span> — calm ↔ storm; ramps wave count, length, amplitude, choppiness, and whitecaps</li>
<li><span style="font-weight: bold;">O / P</span> — wave speed</li>
<li><span style="font-weight: bold;">, / .</span> — rotate wind</li>
<li><span style="font-weight: bold;">F / C</span> — toggle foam / caustics</li>
<li><span style="font-weight: bold;">Mouse swing + wheel</span> — camera look and zoom<br />
</li></ul>
<br />
Available in this repo:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></description>
			<content:encoded><![CDATA[Video:<br />
<br />
<a href="https://streamable.com/wfnoh9" target="_blank">https://streamable.com/wfnoh9</a><br />
<br />
Pics:<br />
<br />
<a href="https://postimg.cc/9zSBqkDZ" target="_blank"><img src="https://i.postimg.cc/ZKYsXtZ7/Screenshot-From-2026-06-22-17-05-16.png" border="0" alt="[Image: Screenshot-From-2026-06-22-17-05-16.png]" /></a><br />
<br />
<a href="https://postimg.cc/z3MkRQyV" target="_blank"><img src="https://i.postimg.cc/CLMr9TYH/Screenshot-From-2026-06-22-17-06-14.png" border="0" alt="[Image: Screenshot-From-2026-06-22-17-06-14.png]" /></a><br />
<br />
<br />
<br />
<br />
AI written summary:<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Water Rendering Overhaul — Foam, Depth Color, Caustics, Gerstner Swell</span></span><br />
<br />
I've spent some time giving the water system a proper pass.<br />
<br />
The engine's water was already strong: scrolling normal maps, Fresnel, planar + cubemap + SSR reflection, depth refraction with leak detection, density absorption, underwater fog, capsule ripples, and large-world support.<br />
<br />
This work fills in the pieces that were missing or weak:<br />
<ul>
<li>Shoreline foam</li>
<li>Depth-graded surface color</li>
<li>Procedural caustics</li>
<li>Whitecap foam</li>
<li>A real Gerstner wave model with choppiness and wave-speed control<br />
</li></ul>
<br />
Everything is driven by new per-material parameters on <span style="font-weight: bold;">WaterMtrl</span>, all exposed in the <span style="font-weight: bold;">Titan Editor → Water Material</span> editor.<br />
<br />
The subtle effects — foam, depth color, and caustics — are on by default at low intensity, so existing water looks a little better out of the box without re-authoring.<br />
<br />
The heavier Gerstner swell is opt-in:<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Wave Count &gt; 0</code></div></div>
<br />
<span style="font-size: large;"><span style="font-weight: bold;">What's New</span></span><br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Depth-Graded Surface Color</span></span><br />
<br />
The surface now blends from a <span style="font-weight: bold;">Shallow Color</span> in thin water to the base <span style="font-weight: bold;">Color</span> in deep water, using the water-column depth the shader already computes.<br />
<br />
This works especially well for carved 3–100 m procedural lakes/rivers and beaches.<br />
<br />
<span style="font-weight: bold;">Params:</span><br />
<ul>
<li><span style="font-weight: bold;">Shallow Color</span></li>
<li><span style="font-weight: bold;">Depth Color Distance</span> — metres over which it blends; <span style="font-weight: bold;">0 = off</span><br />
</li></ul>
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Animated Shoreline Foam</span></span><br />
<br />
A foam band now appears where the water column gets thin — shorelines, around rocks, and anywhere water meets geometry.<br />
<br />
The animation is fully procedural value noise, so no texture asset is required. It churns organically instead of reading as a static pattern.<br />
<br />
<span style="font-weight: bold;">Params:</span><br />
<ul>
<li><span style="font-weight: bold;">Foam Color</span></li>
<li><span style="font-weight: bold;">Foam Amount</span> — <span style="font-weight: bold;">0 = off</span></li>
<li><span style="font-weight: bold;">Foam Depth</span> — band width in metres<br />
</li></ul>
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Procedural Caustics</span></span><br />
<br />
A cheap two-layer cellular caustic network is projected onto the submerged background and faded by depth.<br />
<br />
It is strongest in clear shallows and disappears in deep or opaque water. There is no texture binding, and it is effectively free where the water is deep.<br />
<br />
<span style="font-weight: bold;">Params:</span><br />
<ul>
<li><span style="font-weight: bold;">Caustic Scale</span> — <span style="font-weight: bold;">0 = off</span></li>
<li><span style="font-weight: bold;">Caustic Intensity</span><br />
</li></ul>
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Gerstner Swell — Ocean Technique</span></span><br />
<br />
The Ocean technique now supports real summed-wave vertex displacement with analytic normals.<br />
<br />
Set:<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Wave Count &gt; 0</code></div></div>
<br />
to switch the Ocean plane from the legacy texture-bump waves to a proper Gerstner wave sum.<br />
<br />
Up to 8 waves are synthesized from a few high-level controls.<br />
<br />
Includes:<br />
<ul>
<li><span style="font-weight: bold;">Choppiness</span> — horizontal crest displacement for peaked tops and flatter troughs, which is the defining Gerstner term.</li>
<li><span style="font-weight: bold;">Wave Speed</span> — multiplier on physical deep-water dispersion. <span style="font-weight: bold;">1 = realistic</span>; lower values produce lazy swell, higher values produce faster chop.</li>
<li><span style="font-weight: bold;">Per-wavelength distance fade</span> — long swells carry to distance while short chop settles out.</li>
<li><span style="font-weight: bold;">Global horizon envelope</span> — flattens displacement before the horizon so the far ocean does not alias into shimmer or grid moiré.<br />
</li></ul>
<br />
<span style="font-weight: bold;">Params:</span><br />
<ul>
<li><span style="font-weight: bold;">Wave Count</span></li>
<li><span style="font-weight: bold;">Wave Length</span></li>
<li><span style="font-weight: bold;">Wave Amplitude</span></li>
<li><span style="font-weight: bold;">Wave Choppiness</span></li>
<li><span style="font-weight: bold;">Wave Wind</span> — direction</li>
<li><span style="font-weight: bold;">Wave Speed</span><br />
</li></ul>
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Whitecap / Crest Foam</span></span><br />
<br />
Slope-based foam now builds on steep wave faces as the sea picks up.<br />
<br />
It is biased onto the downwind, breaking face of the wave rather than appearing symmetrically on both sides.<br />
<br />
<span style="font-size: large;"><span style="font-weight: bold;">Demo</span></span><br />
<br />
<span style="font-weight: bold;">Tutorial_14_OceanWater</span> shows everything running on the global sea over a sloped, bumpy seabed rising to a beach.<br />
<br />
Everything is runtime-tunable:<br />
<ul>
<li><span style="font-weight: bold;">Z / X</span> — calm ↔ storm; ramps wave count, length, amplitude, choppiness, and whitecaps</li>
<li><span style="font-weight: bold;">O / P</span> — wave speed</li>
<li><span style="font-weight: bold;">, / .</span> — rotate wind</li>
<li><span style="font-weight: bold;">F / C</span> — toggle foam / caustics</li>
<li><span style="font-weight: bold;">Mouse swing + wheel</span> — camera look and zoom<br />
</li></ul>
<br />
Available in this repo:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[GPU Mesh Shader Grass GoT Style]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11827</link>
			<pubDate>Fri, 19 Jun 2026 11:32:44 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11827</guid>
			<description><![CDATA[<span style="font-size: x-large;">Best seen in video not pictures:<br />
<br />
<br />
<a href="https://streamable.com/hu3bzs" target="_blank">https://streamable.com/hu3bzs</a></span><br />
<br />
<br />
<br />
<br />
<a href="https://postimg.cc/FkfNNN4X" target="_blank"><img src="https://i.postimg.cc/tJkTd4vJ/Screenshot-From-2026-06-19-07-09-33.png" border="0" alt="[Image: Screenshot-From-2026-06-19-07-09-33.png]" /></a><br />
<br />
Grass density can be set per terrain material in Titan Editor:<br />
<br />
<a href="https://postimg.cc/zHL8881P" target="_blank"><img src="https://i.postimg.cc/3N1Rjxf7/Screenshot-From-2026-06-19-07-25-00.png" border="0" alt="[Image: Screenshot-From-2026-06-19-07-25-00.png]" /></a><br />
<br />
AI written summary:<br />
<br />
GPU Grass (Ghost-of-Tsushima-style) for Titan Engine — feature + tutorial<br />
<br />
I've added a GPU-driven grass system to my Esenthel/Titan fork and wanted to share it. It generates grass entirely on the GPU each frame using Vulkan mesh shaders, writing straight into the deferred G-buffer — so the engine's normal lighting, shadows, SSR, and fog apply for free. It's a parallel path that bypasses the shader/material system (the shader compiler has no mesh/task stage), and it's purely additive — the legacy CPU GrassObj foliage still works untouched.<br />
<br />
What it does<br />
<br />
- Deferred, lit grass on the GPU — VK_EXT_mesh_shader; blades generated per frame, no vertex/index buffers. Vulkan-only (inert on GL/DX, which keep the legacy grass).<br />
- Terrain-following + per-material density — a camera-centered control texture samples terrain height + a density built from terrain slope × the material's grass_density. Blades drape the heightfield, thin on slopes, and grow only where you author them.<br />
- World-anchored + smooth LOD — blades are seeded by absolute world position (no "swim" as the field re-centers), and density fades smoothly with distance instead of popping.<br />
- Interaction:<br />
  - trample(pos, radius, strength) — footstep flatten that springs back; a decoupled, world-anchored field uploaded dirty-region only.<br />
  - blast(pos, radius, strength) — one-shot radial knockdown shockwave that springs back.<br />
  - Exclusion zones — addZoneCircle / addZoneBox(center, half, yaw, soft) → handle, removeZone: persistent analytic CUT footprints (e.g. building placements) where grass is removed with a soft edge, plus a per-patch "fully enclosed → skip" early-out.<br />
- Wind — analytic flow field with traveling gust fronts + a slow sway.<br />
<br />
Opt-in by material<br />
<br />
Grass density is a per-material attribute (Material::grass_density, 0..1) with an editor "Grass Density" slider. It defaults to 0, so GPU grass is opt-in: enabling the system on an existing world shows nothing until you raise density on the grass materials. Author it toward 1 on grass/dirt, leave it 0 on rock/sand/snow/paths. (This deliberately avoids retro-fitting grass onto every existing material.)<br />
<br />
The demo — Tutorial_14_GpuGrass<br />
<br />
A character standing in a GPU-grass field on real terrain. Controls:<br />
<br />
- WSAD — walk (leaves a trample trail)<br />
- B — blast knockdown<br />
- G — toggle a building CUT zone at the character<br />
- F — force full density everywhere (handy on a world whose materials aren't authored yet)<br />
- Mouse — orbit, Wheel — zoom, Esc — quit<br />
<br />
Note: because density is opt-in, the demo shows no grass until you either set a terrain material's Grass Density &gt; 0 in the editor or press F — there's an on-screen hint for this.<br />
<br />
Implementation notes / caveats<br />
<br />
- Vulkan-only, gated on VkMeshShaderSupported.<br />
- Shaders (grass.task/.mesh/.frag + grass_common.hlsli) are HLSL compiled to SPIR-V by the vendored DXC at engine build time — a parallel path, not via EERegen, so no shader tooling needed at build.<br />
- Drawn from RendererClass::opaque() (RT_DEFERRED) via VkDrawGrass; its own self-contained UBO + descriptor set (not a ShaderVK); reverse-Z + negative-height viewport matched to the deferred config.<br />
- Grass is tagged PSM_TRANSLUCENT and excluded from SSAO (tblades otherwise make screen-space AO shimmer at distance).<br />
<br />
In this repo:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></description>
			<content:encoded><![CDATA[<span style="font-size: x-large;">Best seen in video not pictures:<br />
<br />
<br />
<a href="https://streamable.com/hu3bzs" target="_blank">https://streamable.com/hu3bzs</a></span><br />
<br />
<br />
<br />
<br />
<a href="https://postimg.cc/FkfNNN4X" target="_blank"><img src="https://i.postimg.cc/tJkTd4vJ/Screenshot-From-2026-06-19-07-09-33.png" border="0" alt="[Image: Screenshot-From-2026-06-19-07-09-33.png]" /></a><br />
<br />
Grass density can be set per terrain material in Titan Editor:<br />
<br />
<a href="https://postimg.cc/zHL8881P" target="_blank"><img src="https://i.postimg.cc/3N1Rjxf7/Screenshot-From-2026-06-19-07-25-00.png" border="0" alt="[Image: Screenshot-From-2026-06-19-07-25-00.png]" /></a><br />
<br />
AI written summary:<br />
<br />
GPU Grass (Ghost-of-Tsushima-style) for Titan Engine — feature + tutorial<br />
<br />
I've added a GPU-driven grass system to my Esenthel/Titan fork and wanted to share it. It generates grass entirely on the GPU each frame using Vulkan mesh shaders, writing straight into the deferred G-buffer — so the engine's normal lighting, shadows, SSR, and fog apply for free. It's a parallel path that bypasses the shader/material system (the shader compiler has no mesh/task stage), and it's purely additive — the legacy CPU GrassObj foliage still works untouched.<br />
<br />
What it does<br />
<br />
- Deferred, lit grass on the GPU — VK_EXT_mesh_shader; blades generated per frame, no vertex/index buffers. Vulkan-only (inert on GL/DX, which keep the legacy grass).<br />
- Terrain-following + per-material density — a camera-centered control texture samples terrain height + a density built from terrain slope × the material's grass_density. Blades drape the heightfield, thin on slopes, and grow only where you author them.<br />
- World-anchored + smooth LOD — blades are seeded by absolute world position (no "swim" as the field re-centers), and density fades smoothly with distance instead of popping.<br />
- Interaction:<br />
  - trample(pos, radius, strength) — footstep flatten that springs back; a decoupled, world-anchored field uploaded dirty-region only.<br />
  - blast(pos, radius, strength) — one-shot radial knockdown shockwave that springs back.<br />
  - Exclusion zones — addZoneCircle / addZoneBox(center, half, yaw, soft) → handle, removeZone: persistent analytic CUT footprints (e.g. building placements) where grass is removed with a soft edge, plus a per-patch "fully enclosed → skip" early-out.<br />
- Wind — analytic flow field with traveling gust fronts + a slow sway.<br />
<br />
Opt-in by material<br />
<br />
Grass density is a per-material attribute (Material::grass_density, 0..1) with an editor "Grass Density" slider. It defaults to 0, so GPU grass is opt-in: enabling the system on an existing world shows nothing until you raise density on the grass materials. Author it toward 1 on grass/dirt, leave it 0 on rock/sand/snow/paths. (This deliberately avoids retro-fitting grass onto every existing material.)<br />
<br />
The demo — Tutorial_14_GpuGrass<br />
<br />
A character standing in a GPU-grass field on real terrain. Controls:<br />
<br />
- WSAD — walk (leaves a trample trail)<br />
- B — blast knockdown<br />
- G — toggle a building CUT zone at the character<br />
- F — force full density everywhere (handy on a world whose materials aren't authored yet)<br />
- Mouse — orbit, Wheel — zoom, Esc — quit<br />
<br />
Note: because density is opt-in, the demo shows no grass until you either set a terrain material's Grass Density &gt; 0 in the editor or press F — there's an on-screen hint for this.<br />
<br />
Implementation notes / caveats<br />
<br />
- Vulkan-only, gated on VkMeshShaderSupported.<br />
- Shaders (grass.task/.mesh/.frag + grass_common.hlsli) are HLSL compiled to SPIR-V by the vendored DXC at engine build time — a parallel path, not via EERegen, so no shader tooling needed at build.<br />
- Drawn from RendererClass::opaque() (RT_DEFERRED) via VkDrawGrass; its own self-contained UBO + descriptor set (not a ShaderVK); reverse-Z + negative-height viewport matched to the deferred config.<br />
- Grass is tagged PSM_TRANSLUCENT and excluded from SSAO (tblades otherwise make screen-space AO shimmer at distance).<br />
<br />
In this repo:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Terrain Anti-Tiling: distance dual-scale + stochastic "by-example" no asset pipeline]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11826</link>
			<pubDate>Fri, 19 Jun 2026 03:57:20 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11826</guid>
			<description><![CDATA[<span style="font-size: large;"><span style="font-weight: bold;">Terrain Anti-Tiling: distance dual-scale + stochastic “by-example” (no asset pipeline)</span></span><br />
<br />
Before anti-tile:<br />
<a href="https://postimg.cc/fSLfnkSR" target="_blank"><img src="https://i.postimg.cc/br0Cntzk/Screenshot-From-2026-06-18-23-39-08.png" border="0" alt="[Image: Screenshot-From-2026-06-18-23-39-08.png]" /></a><br />
<br />
After anti-tile:<br />
<a href="https://postimg.cc/w3TVs4fF" target="_blank"><img src="https://i.postimg.cc/44pFqrzC/Screenshot-From-2026-06-18-23-38-58.png" border="0" alt="[Image: Screenshot-From-2026-06-18-23-38-58.png]" /></a><br />
<br />
Works best when looking out over the terrain, looking straight down at terrain it doesn't help as much.<br />
<br />
<br />
<br />
Written by AI:<br />
<br />
Repeating ground textures betray themselves two different ways, and they need two different fixes:<br />
<br />
<ol type="1">
<li><span style="font-weight: bold;">The far-field grid</span> — from a hill or a grazing camera, the regular <span style="font-style: italic;">periodicity</span> of the texture reads as a checkerboard receding to the horizon.</li>
<li><span style="font-weight: bold;">The close/mid feature repeat</span> — that one distinctive grass clump / rock / crack showing up again every few meters right in front of you.<br />
</li></ol>
<br />
Two complementary techniques in the deferred terrain shader (<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Engine/Source/Shaders/Deferred.cpp</code></div></div>
) address each. Both run on the <span style="font-weight: bold;">single- and multi-material</span> terrain paths, up to 5 blended materials, and both are <span style="font-weight: bold;">gated to terrain only</span> (<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>#if HEIGHTMAP &amp;&amp; SET_POS</code></div></div>
), so buildings, props, and characters are completely untouched.<br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">1. Distance-based dual-scale: kills the far grid</span></span><br />
<br />
Sample the same ground texture at <span style="font-weight: bold;">two UV scales</span> and blend in the lower-frequency, larger-tile one with camera distance:<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>tex = lerp(near, RTex(Col, uv * TileFarScale), fade(distance));</code></div></div>
<br />
Near the camera you get full detail. Far away, the tight repeat dissolves into a coarser, non-aligned pattern. This is the standard Unreal-landscape-style trick, and it is <span style="font-weight: bold;">mip-safe for free</span>, because the two samples use implicit derivatives, so no <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>SampleGrad</code></div></div>
 is needed.<br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">2. Stochastic “by-example”: kills the near/mid feature repeat</span></span><br />
<br />
This is <span style="font-weight: bold;">Heitz–Neyret’s by-example noise, minus the histogram LUT</span>, which means it needs <span style="font-weight: bold;">no precomputed assets</span> and no per-texture bake.<br />
<ul>
<li><span style="font-weight: bold;">Triangular / “hex” grid</span> — 3 samples, no axis-aligned blend seams.</li>
<li><span style="font-weight: bold;">Per-cell random offset only, no mirror</span> — the mirror in the cheaper grid variants is what causes those directional scratches/scuffs on grass. Dropping it removes them.</li>
<li><span style="font-weight: bold;">Variance-preserving blend</span> — naive averaging washes out contrast, so instead of <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>sum(w_i * c_i)</code></div></div>
, we blend the deviation from the mean and renormalize by <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>1 / sqrt(sum(w_i^2))</code></div></div>
. The mean is read from the texture’s <span style="font-weight: bold;">smallest mip</span>, approximately its average color, so we recover contrast with zero precomputation.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>SampleGrad</code></div></div>
 keeps mipmapping correct.<br />
</li></ul>
<br />
The result: a single tiling texture stops repeating and reads as a natural, non-periodic surface — no scratches, no muddy blend blobs.<br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">How to use it</span></span><br />
<br />
Everything is driven by a single <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TerrainTile</code></div></div>
 shader cbuffer, set from the public shader-param API:<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>SPSet("TerrainTileEnable", 1.0f);&nbsp;&nbsp;// distance effect<br />
SPSet("TileFarScale", 0.22f);<br />
SPSet("TileFadeStart", 5.0f);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// ...TileFadeRange, TileFarMax<br />
<br />
SPSet("TerrainStochastic", 1.0f);&nbsp;&nbsp;// stochastic de-tiling</code></div></div>
<br />
<span style="font-weight: bold;">It is off by default.</span> The cbuffer is zero-initialized, so nothing changes unless your app sets the params. The editor and all other content render exactly as before.<br />
<br />
The params only exist once the terrain shader has loaded, so set them <span style="font-weight: bold;">each frame before drawing the world</span>. <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>SPSet</code></div></div>
 safely no-ops until then. There is no engine-wide default yet; you opt in from your render loop.<br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Parameters</span></span><br />
<ul>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TerrainTileEnable</code></div></div>
 — 0/1, distance dual-scale on/off.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TileFarScale</code></div></div>
 — coarse UV multiplier. Smaller means bigger far tiles and a stronger frequency break.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TileFadeStart</code></div></div>
 — meters: distance where the coarse blend begins.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TileFadeRange</code></div></div>
 — meters: distance over which it ramps to <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TileFarMax</code></div></div>
.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TileFarMax</code></div></div>
 — max fraction of the coarse sample blended in at far distance.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TerrainStochastic</code></div></div>
 — 0/1, stochastic de-tiling on/off.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>StochMaxDist</code></div></div>
 — performance limit: only de-tile within this distance. <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>&lt;=0</code></div></div>
 means no limit.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>StochMinWeight</code></div></div>
 — performance limit: skip materials whose blend weight is below this.  means de-tile all.<br />
</li></ul>
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Performance limits: opt-in, runtime</span></span><br />
<br />
Multi-material stochastic can hit up to <span style="font-weight: bold;">5 materials × 4 taps = 20 albedo taps</span>, so there are two runtime knobs:<br />
<ul>
<li><span style="font-weight: bold;"><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>StochMaxDist</code></div></div>
</span> — only run the stochastic taps within this distance. The far grid is handled by the distance blend anyway, so far pixels drop straight back to 1 tap.</li>
<li><span style="font-weight: bold;"><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>StochMinWeight</code></div></div>
</span> — skip stochastic for a material whose per-pixel blend weight is below a threshold, so a barely-visible layer does not cost 4 taps.<br />
</li></ul>
<br />
Both default to “no practical limit.”<br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Large-world / floating-origin</span></span><br />
<br />
Terrain UVs are <span style="font-style: italic;">world-continuous</span>, built from the absolute area index, so far out in a big world they grow large.<br />
<br />
A <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Frac(sin(...))</code></div></div>
 hash loses precision there and the de-tiling starts aliasing, so the stochastic cell hash is a <span style="font-weight: bold;">PCG2D integer hash</span> on the integer cell ID, exact at any magnitude.<br />
<br />
The pattern stays locked to the ground and is transparent to floating-origin rebasing. UVs deliberately <span style="font-weight: bold;">do not</span> rebase. Shifting them would move the non-integer-aligned stochastic grid and pop the pattern every rebase.<br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Tutorials</span></span><br />
<br />
<span style="font-weight: bold;"><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Tutorial_14_TerrainTiling</code></div></div>
</span><br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Tutorials/Source/14 - Game Basics/36 - Terrain Tiling.cpp</code></div></div>
<br />
The live A/B demo. It loads a textured world and lets you toggle and tune everything in real time:<br />
<ul>
<li><span style="font-weight: bold;">T</span> — distance anti-tiling on/off.</li>
<li><span style="font-weight: bold;">Y</span> — stochastic de-tiling on/off.</li>
<li><span style="font-weight: bold;">1–8</span> — distance params: strength, fade start, fade range, coarse scale.</li>
<li><span style="font-weight: bold;">G / H</span> — stochastic max distance, for performance.</li>
<li><span style="font-weight: bold;">J / K</span> — stochastic min weight, for performance.</li>
<li><span style="font-weight: bold;">9</span> — reset all to defaults.</li>
<li>Hold <span style="font-weight: bold;">RMB</span> — look.</li>
<li><span style="font-weight: bold;">Mouse wheel</span> — zoom.</li>
<li><span style="font-weight: bold;">ESC</span> — quit.<br />
</li></ul>
<br />
The HUD shows every value live, and the <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>EE_TILE*</code></div></div>
 env vars let you drive it headless for A/B captures:<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>EE_TILE<br />
EE_TILE_STOCH<br />
EE_TILE_CAM_*<br />
EE_TILE_FARMAX<br />
EE_TILE_STOCHDIST</code></div></div>
<br />
Since the effect lives in the shader, it shows up on <span style="font-style: italic;">any</span> terrain. The other world tutorials are good places to see it on different content once an app enables the params:<br />
<ul>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Tutorial_14_World</code></div></div>
</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Tutorial_14_WorldWithCharacter</code></div></div>
</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Tutorial_14_ProceduralWorld</code></div></div>
</li></ul>
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Caveats / future work</span></span><br />
<ul>
<li>The only thing the <span style="font-weight: bold;">full Heitz histogram LUT</span> would add over this is slightly sharper blend zones. It carries a per-texture asset-bake cost, so it is left out. The variance-preserving approximation looks clean in practice.</li>
<li>One residual issue: the triangular-grid <span style="font-weight: bold;">barycentric weights</span> lose a little float precision at <span style="font-style: italic;">extreme</span> UV. The cell hash is exact, so the de-tiling holds; only the blend transitions get marginally coarser very far out.</li>
<li><span style="font-weight: bold;">Backends:</span> Vulkan and GL shader paks are regenerated. <span style="font-weight: bold;">DX still needs a Windows tree</span> to recompile, only tested on Linux+Vulkan.<br />
</li></ul>
<br />
in this repo: <a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></description>
			<content:encoded><![CDATA[<span style="font-size: large;"><span style="font-weight: bold;">Terrain Anti-Tiling: distance dual-scale + stochastic “by-example” (no asset pipeline)</span></span><br />
<br />
Before anti-tile:<br />
<a href="https://postimg.cc/fSLfnkSR" target="_blank"><img src="https://i.postimg.cc/br0Cntzk/Screenshot-From-2026-06-18-23-39-08.png" border="0" alt="[Image: Screenshot-From-2026-06-18-23-39-08.png]" /></a><br />
<br />
After anti-tile:<br />
<a href="https://postimg.cc/w3TVs4fF" target="_blank"><img src="https://i.postimg.cc/44pFqrzC/Screenshot-From-2026-06-18-23-38-58.png" border="0" alt="[Image: Screenshot-From-2026-06-18-23-38-58.png]" /></a><br />
<br />
Works best when looking out over the terrain, looking straight down at terrain it doesn't help as much.<br />
<br />
<br />
<br />
Written by AI:<br />
<br />
Repeating ground textures betray themselves two different ways, and they need two different fixes:<br />
<br />
<ol type="1">
<li><span style="font-weight: bold;">The far-field grid</span> — from a hill or a grazing camera, the regular <span style="font-style: italic;">periodicity</span> of the texture reads as a checkerboard receding to the horizon.</li>
<li><span style="font-weight: bold;">The close/mid feature repeat</span> — that one distinctive grass clump / rock / crack showing up again every few meters right in front of you.<br />
</li></ol>
<br />
Two complementary techniques in the deferred terrain shader (<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Engine/Source/Shaders/Deferred.cpp</code></div></div>
) address each. Both run on the <span style="font-weight: bold;">single- and multi-material</span> terrain paths, up to 5 blended materials, and both are <span style="font-weight: bold;">gated to terrain only</span> (<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>#if HEIGHTMAP &amp;&amp; SET_POS</code></div></div>
), so buildings, props, and characters are completely untouched.<br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">1. Distance-based dual-scale: kills the far grid</span></span><br />
<br />
Sample the same ground texture at <span style="font-weight: bold;">two UV scales</span> and blend in the lower-frequency, larger-tile one with camera distance:<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>tex = lerp(near, RTex(Col, uv * TileFarScale), fade(distance));</code></div></div>
<br />
Near the camera you get full detail. Far away, the tight repeat dissolves into a coarser, non-aligned pattern. This is the standard Unreal-landscape-style trick, and it is <span style="font-weight: bold;">mip-safe for free</span>, because the two samples use implicit derivatives, so no <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>SampleGrad</code></div></div>
 is needed.<br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">2. Stochastic “by-example”: kills the near/mid feature repeat</span></span><br />
<br />
This is <span style="font-weight: bold;">Heitz–Neyret’s by-example noise, minus the histogram LUT</span>, which means it needs <span style="font-weight: bold;">no precomputed assets</span> and no per-texture bake.<br />
<ul>
<li><span style="font-weight: bold;">Triangular / “hex” grid</span> — 3 samples, no axis-aligned blend seams.</li>
<li><span style="font-weight: bold;">Per-cell random offset only, no mirror</span> — the mirror in the cheaper grid variants is what causes those directional scratches/scuffs on grass. Dropping it removes them.</li>
<li><span style="font-weight: bold;">Variance-preserving blend</span> — naive averaging washes out contrast, so instead of <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>sum(w_i * c_i)</code></div></div>
, we blend the deviation from the mean and renormalize by <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>1 / sqrt(sum(w_i^2))</code></div></div>
. The mean is read from the texture’s <span style="font-weight: bold;">smallest mip</span>, approximately its average color, so we recover contrast with zero precomputation.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>SampleGrad</code></div></div>
 keeps mipmapping correct.<br />
</li></ul>
<br />
The result: a single tiling texture stops repeating and reads as a natural, non-periodic surface — no scratches, no muddy blend blobs.<br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">How to use it</span></span><br />
<br />
Everything is driven by a single <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TerrainTile</code></div></div>
 shader cbuffer, set from the public shader-param API:<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>SPSet("TerrainTileEnable", 1.0f);&nbsp;&nbsp;// distance effect<br />
SPSet("TileFarScale", 0.22f);<br />
SPSet("TileFadeStart", 5.0f);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// ...TileFadeRange, TileFarMax<br />
<br />
SPSet("TerrainStochastic", 1.0f);&nbsp;&nbsp;// stochastic de-tiling</code></div></div>
<br />
<span style="font-weight: bold;">It is off by default.</span> The cbuffer is zero-initialized, so nothing changes unless your app sets the params. The editor and all other content render exactly as before.<br />
<br />
The params only exist once the terrain shader has loaded, so set them <span style="font-weight: bold;">each frame before drawing the world</span>. <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>SPSet</code></div></div>
 safely no-ops until then. There is no engine-wide default yet; you opt in from your render loop.<br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Parameters</span></span><br />
<ul>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TerrainTileEnable</code></div></div>
 — 0/1, distance dual-scale on/off.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TileFarScale</code></div></div>
 — coarse UV multiplier. Smaller means bigger far tiles and a stronger frequency break.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TileFadeStart</code></div></div>
 — meters: distance where the coarse blend begins.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TileFadeRange</code></div></div>
 — meters: distance over which it ramps to <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TileFarMax</code></div></div>
.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TileFarMax</code></div></div>
 — max fraction of the coarse sample blended in at far distance.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>TerrainStochastic</code></div></div>
 — 0/1, stochastic de-tiling on/off.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>StochMaxDist</code></div></div>
 — performance limit: only de-tile within this distance. <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>&lt;=0</code></div></div>
 means no limit.</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>StochMinWeight</code></div></div>
 — performance limit: skip materials whose blend weight is below this.  means de-tile all.<br />
</li></ul>
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Performance limits: opt-in, runtime</span></span><br />
<br />
Multi-material stochastic can hit up to <span style="font-weight: bold;">5 materials × 4 taps = 20 albedo taps</span>, so there are two runtime knobs:<br />
<ul>
<li><span style="font-weight: bold;"><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>StochMaxDist</code></div></div>
</span> — only run the stochastic taps within this distance. The far grid is handled by the distance blend anyway, so far pixels drop straight back to 1 tap.</li>
<li><span style="font-weight: bold;"><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>StochMinWeight</code></div></div>
</span> — skip stochastic for a material whose per-pixel blend weight is below a threshold, so a barely-visible layer does not cost 4 taps.<br />
</li></ul>
<br />
Both default to “no practical limit.”<br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Large-world / floating-origin</span></span><br />
<br />
Terrain UVs are <span style="font-style: italic;">world-continuous</span>, built from the absolute area index, so far out in a big world they grow large.<br />
<br />
A <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Frac(sin(...))</code></div></div>
 hash loses precision there and the de-tiling starts aliasing, so the stochastic cell hash is a <span style="font-weight: bold;">PCG2D integer hash</span> on the integer cell ID, exact at any magnitude.<br />
<br />
The pattern stays locked to the ground and is transparent to floating-origin rebasing. UVs deliberately <span style="font-weight: bold;">do not</span> rebase. Shifting them would move the non-integer-aligned stochastic grid and pop the pattern every rebase.<br />
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Tutorials</span></span><br />
<br />
<span style="font-weight: bold;"><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Tutorial_14_TerrainTiling</code></div></div>
</span><br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Tutorials/Source/14 - Game Basics/36 - Terrain Tiling.cpp</code></div></div>
<br />
The live A/B demo. It loads a textured world and lets you toggle and tune everything in real time:<br />
<ul>
<li><span style="font-weight: bold;">T</span> — distance anti-tiling on/off.</li>
<li><span style="font-weight: bold;">Y</span> — stochastic de-tiling on/off.</li>
<li><span style="font-weight: bold;">1–8</span> — distance params: strength, fade start, fade range, coarse scale.</li>
<li><span style="font-weight: bold;">G / H</span> — stochastic max distance, for performance.</li>
<li><span style="font-weight: bold;">J / K</span> — stochastic min weight, for performance.</li>
<li><span style="font-weight: bold;">9</span> — reset all to defaults.</li>
<li>Hold <span style="font-weight: bold;">RMB</span> — look.</li>
<li><span style="font-weight: bold;">Mouse wheel</span> — zoom.</li>
<li><span style="font-weight: bold;">ESC</span> — quit.<br />
</li></ul>
<br />
The HUD shows every value live, and the <div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>EE_TILE*</code></div></div>
 env vars let you drive it headless for A/B captures:<br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>EE_TILE<br />
EE_TILE_STOCH<br />
EE_TILE_CAM_*<br />
EE_TILE_FARMAX<br />
EE_TILE_STOCHDIST</code></div></div>
<br />
Since the effect lives in the shader, it shows up on <span style="font-style: italic;">any</span> terrain. The other world tutorials are good places to see it on different content once an app enables the params:<br />
<ul>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Tutorial_14_World</code></div></div>
</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Tutorial_14_WorldWithCharacter</code></div></div>
</li>
<li><div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>Tutorial_14_ProceduralWorld</code></div></div>
</li></ul>
<br />
<span style="font-size: medium;"><span style="font-weight: bold;">Caveats / future work</span></span><br />
<ul>
<li>The only thing the <span style="font-weight: bold;">full Heitz histogram LUT</span> would add over this is slightly sharper blend zones. It carries a per-texture asset-bake cost, so it is left out. The variance-preserving approximation looks clean in practice.</li>
<li>One residual issue: the triangular-grid <span style="font-weight: bold;">barycentric weights</span> lose a little float precision at <span style="font-style: italic;">extreme</span> UV. The cell hash is exact, so the de-tiling holds; only the blend transitions get marginally coarser very far out.</li>
<li><span style="font-weight: bold;">Backends:</span> Vulkan and GL shader paks are regenerated. <span style="font-weight: bold;">DX still needs a Windows tree</span> to recompile, only tested on Linux+Vulkan.<br />
</li></ul>
<br />
in this repo: <a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Shader Cache, Azgaar Map Import,  Large Worlds Editor, Procedural Terrain, Portals]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11824</link>
			<pubDate>Sat, 13 Jun 2026 03:39:07 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11824</guid>
			<description><![CDATA[<span style="font-style: italic;">Please note I have only been testing Linux Vulkan RADV in my repo, not sure if any of this runs on other platforms/renderers/drivers.</span><br />
<br />
Added a large set of features focused on large-world support, procedural terrain, and streaming.<br />
<br />
Titan Editor can now import and edit blended worlds that combine procedural terrain with hand-authored tiled areas. A 250 km x 250 km procedural world is under 100 MB on disk, compared to over 3 TB for an equivalent fully tiled world at the same resolution.<br />
<br />
The engine now supports 64-bit large worlds through rebasing and tile-relative storage. Object rendering and PhysX simulation remain accurate and stable at world coordinates like 250,000,250,000 , just as they are near 0,0.<br />
<br />
Azgaar (<a href="https://azgaar.github.io/Fantasy-Map-Generator/" target="_blank">https://azgaar.github.io/Fantasy-Map-Generator/</a>) procedural map import now supports biome data, lake generation, river carving, and water mesh generation. Runtime procedural terrain also supports ring-LOD generation so distant terrain can stream and render efficiently.<br />
<br />
Titan Editor fully supports 64-bit worlds by rebasing the editor view and storing all relevant world data relative to tiles.<br />
<br />
Portal support was also added. Portals are placed through waypoints in Titan Editor and allow real-time movement to disconnected areas of the large world without a loading screen. As the player approaches a portal, the engine pre-streams tiled areas and generates procedural areas on the destination side so the transition is ready before crossing.<br />
<br />
<br />
<br />
AI Generated repo review of engine changes:<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">1. Linux-native shader toolchain + EERegen</span></span><br />
<ul>
<li>Vendored DXC v1.9.2602 on Linux, with SPIRV-Cross built from source. Vulkan and GL shader paks can now be regenerated natively on Linux without a Windows machine.</li>
<li><span style="font-weight: bold;">EERegen</span>: a single headless tool that performs shader compilation and Engine.pak / Editor.pak rebundling in one process. This replaces the old two-tool chain of shader-compile build + Engine Builder GUI.</li>
<li>Added a content-hash shader variant cache. Warm regen time dropped from roughly <span style="font-weight: bold;">40 minutes to about 30 seconds</span>. Editing one HLSL file now rebuilds only its affected variants.</li>
<li>Fixed a DXC RelaxedPrecision / OpPhi issue that was causing <span style="font-weight: bold;">544 compile failures</span> in Motion Blur and Temporal shaders.<br />
</li></ul>
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">2. Compiler bug fixed: -fshort-wchar removed</span></span><br />
<br />
Clang's loop-idiom recognizer was rewriting the engine's string-length loop into a glibc wcslen call built for 32-bit wchar_t. This caused path truncation and memory over-reads at -O2.<br />
<br />
The -fshort-wchar flag was removed, which fixed the issue.<br />
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">3. Vulkan backend fixes and improvements</span></span><br />
<br />
Fixed <span style="font-weight: bold;">15 tracked Vulkan issues</span>, including:<br />
<ul>
<li>Device loss from cube-texture views bound to 2D descriptor slots.</li>
<li>Crashes from in-flight buffer/image destruction, fixed with a new deferred-destruction system.</li>
<li>Scene corruption under heavy draw load caused by per-draw UBO ring wrapping. This was replaced with a byte-budget ring.</li>
<li>Eye-adaptation black frame caused by cross-stage image slot collisions.</li>
<li>Per-draw descriptor allocation was O(draws). Switched to dynamic UBO offsets.</li>
<li>Terrain “sparkle” caused by the BRDF LUT being deleted on every render-target reset.</li>
<li>Added per-pipeline descriptor sets.</li>
<li>Added a disk-persistent pipeline cache.</li>
<li>Reduced sync-validation errors from <span style="font-weight: bold;">10 to 0</span>.</li>
<li>Added env-gated abort-on-error debugging tools.<br />
</li></ul>
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">4. Upstream Esenthel sync</span></span><br />
<br />
Merged upstream main back in, regenerated affected shader paks, and fixed a .gitattributes issue that was causing bogus whole-file merge conflicts.<br />
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">5. Large World: 64-bit floating origin</span></span><br />
<ul>
<li>Added runtime floating-origin support in Game::World.</li>
<li>Added camera rebase support.</li>
<li>Added tile-relative area storage.</li>
<li>Added water, decal, and navmesh support for large worlds.</li>
<li>Added local-frame pathfinding wrappers.</li>
<li>Added editor support for large worlds, including jitter-free terrain editing and object placement far from origin.</li>
<li>Updated undo/redo so it survives rebasing.</li>
<li>Added world-coordinate display panels.</li>
<li>Added tile-relative game export.</li>
<li>Added an area relocate/convert tool for existing worlds.<br />
</li></ul>
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">6. Procedural terrain + Azgaar world import</span></span><br />
<ul>
<li>Titan Editor supports Import of Azgaar Maps and seamless blending of Procedural+Tiled areas</li>
<li>Added runtime streamed procedural terrain generation.</li>
<li>Generation is multithreaded and prioritizes areas nearest the camera.</li>
<li>Added a far-LOD geo-clipmap backdrop ring so terrain reaches the horizon.</li>
<li>Added climate-driven biomes with per-biome terrain shapes: flat, rolling, rugged, and dunes.</li>
<li>Added three blended ground materials to reduce visible tiling.</li>
<li>Added terrain multi-texturing by slope and altitude.</li>
<li>Added object scatter for trees and rocks, including static collision.</li>
<li>Added sea level and ocean support.</li>
<li>Added rivers carved from Azgaar map data with mitred winding channels.</li>
<li>Added above-sea-level lake basins.</li>
<li>Water and terrain now render at full detail to the horizon.</li>
<li>Added Azgaar Fantasy Map Generator import. Heightmap and biome control maps drive the generated world.</li>
<li>Added an in-game minimap using M, with player position, facing arrow, and click-to-teleport.</li>
<li>Added editor tools: Procedural Terrain dialog, Biome Editor, and Rivers &amp; Lakes dialog.</li>
<li>Procedural config is included with the exported world.</li>
<li>Added per-area navmesh tiles for generated terrain.</li>
<li>Added a tutorial: <span style="font-weight: bold;">“Procedural World”</span>. 250 km x 250 km Azgaar Map Demo.<br />
</li></ul>
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">7. Portals</span></span><br />
<ul>
<li>Added portal authoring in the editor.</li>
<li>Added runtime portal crossing.</li>
<li>Added a secondary-streaming-foci API so the destination side can pre-stream before the player crosses.</li>
<li>Added tutorial <span style="font-weight: bold;">“21 - Portals”</span> with a sample world.</li>
<li>Portal crossing is seamless, with no flash.</li>
<li>The streaming ring on the departed side is preserved during crossing.</li>
<li>Added a streaming-status HUD.<br />
</li></ul>
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">Honorable mentions</span></span><br />
<ul>
<li>RmlUi improvements: icon: texture loading from the engine image cache, plus same-texture draw batching.</li>
<li>Fixed stale input-action state on popped or masked input maps.</li>
<li>Added memory hardening from ASAN runs, including out-of-bounds guards and completion of the global new/delete family.</li>
<li>Made sizeof(Image) renderer-independent.<br />
</li></ul>
<br />
Source is in this fork: <a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a><br />
<br />
<a href="https://postimg.cc/QHx7vK6v" target="_blank"><img src="https://i.postimg.cc/wTDkyhDx/Screenshot-From-2026-06-12-21-24-05.png" border="0" alt="[Image: Screenshot-From-2026-06-12-21-24-05.png]" /></a><br />
<br />
<a href="https://postimg.cc/fJ2dsWpW" target="_blank"><img src="https://i.postimg.cc/XY3kwpg5/Screenshot-From-2026-06-12-21-24-39.png" border="0" alt="[Image: Screenshot-From-2026-06-12-21-24-39.png]" /></a><br />
<br />
<a href="https://postimg.cc/w7fNzxZ9" target="_blank"><img src="https://i.postimg.cc/cLZBQrct/Screenshot-From-2026-06-12-21-25-50.png" border="0" alt="[Image: Screenshot-From-2026-06-12-21-25-50.png]" /></a><br />
<br />
<a href="https://postimg.cc/fJ2dsWpR" target="_blank"><img src="https://i.postimg.cc/9QVYGrBZ/Screenshot-From-2026-06-12-21-27-49.png" border="0" alt="[Image: Screenshot-From-2026-06-12-21-27-49.png]" /></a><br />
<br />
<a href="https://postimg.cc/PP3DHf9r" target="_blank"><img src="https://i.postimg.cc/6pwfdyhR/Screenshot-From-2026-06-12-21-28-00.png" border="0" alt="[Image: Screenshot-From-2026-06-12-21-28-00.png]" /></a><br />
<br />
<a href="https://postimg.cc/sv6WzfqW" target="_blank"><img src="https://i.postimg.cc/13PrpfKv/Screenshot-From-2026-06-12-23-08-05.png" border="0" alt="[Image: Screenshot-From-2026-06-12-23-08-05.png]" /></a><br />
<br />
<a href="https://postimg.cc/1fCFSR2w" target="_blank"><img src="https://i.postimg.cc/YCkz1hxn/Screenshot-From-2026-06-12-23-08-23.png" border="0" alt="[Image: Screenshot-From-2026-06-12-23-08-23.png]" /></a><br />
<br />
<a href="https://postimg.cc/qzbKrBSG" target="_blank"><img src="https://i.postimg.cc/8zDmhjbx/Screenshot-From-2026-06-12-23-09-07.png" border="0" alt="[Image: Screenshot-From-2026-06-12-23-09-07.png]" /></a>]]></description>
			<content:encoded><![CDATA[<span style="font-style: italic;">Please note I have only been testing Linux Vulkan RADV in my repo, not sure if any of this runs on other platforms/renderers/drivers.</span><br />
<br />
Added a large set of features focused on large-world support, procedural terrain, and streaming.<br />
<br />
Titan Editor can now import and edit blended worlds that combine procedural terrain with hand-authored tiled areas. A 250 km x 250 km procedural world is under 100 MB on disk, compared to over 3 TB for an equivalent fully tiled world at the same resolution.<br />
<br />
The engine now supports 64-bit large worlds through rebasing and tile-relative storage. Object rendering and PhysX simulation remain accurate and stable at world coordinates like 250,000,250,000 , just as they are near 0,0.<br />
<br />
Azgaar (<a href="https://azgaar.github.io/Fantasy-Map-Generator/" target="_blank">https://azgaar.github.io/Fantasy-Map-Generator/</a>) procedural map import now supports biome data, lake generation, river carving, and water mesh generation. Runtime procedural terrain also supports ring-LOD generation so distant terrain can stream and render efficiently.<br />
<br />
Titan Editor fully supports 64-bit worlds by rebasing the editor view and storing all relevant world data relative to tiles.<br />
<br />
Portal support was also added. Portals are placed through waypoints in Titan Editor and allow real-time movement to disconnected areas of the large world without a loading screen. As the player approaches a portal, the engine pre-streams tiled areas and generates procedural areas on the destination side so the transition is ready before crossing.<br />
<br />
<br />
<br />
AI Generated repo review of engine changes:<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">1. Linux-native shader toolchain + EERegen</span></span><br />
<ul>
<li>Vendored DXC v1.9.2602 on Linux, with SPIRV-Cross built from source. Vulkan and GL shader paks can now be regenerated natively on Linux without a Windows machine.</li>
<li><span style="font-weight: bold;">EERegen</span>: a single headless tool that performs shader compilation and Engine.pak / Editor.pak rebundling in one process. This replaces the old two-tool chain of shader-compile build + Engine Builder GUI.</li>
<li>Added a content-hash shader variant cache. Warm regen time dropped from roughly <span style="font-weight: bold;">40 minutes to about 30 seconds</span>. Editing one HLSL file now rebuilds only its affected variants.</li>
<li>Fixed a DXC RelaxedPrecision / OpPhi issue that was causing <span style="font-weight: bold;">544 compile failures</span> in Motion Blur and Temporal shaders.<br />
</li></ul>
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">2. Compiler bug fixed: -fshort-wchar removed</span></span><br />
<br />
Clang's loop-idiom recognizer was rewriting the engine's string-length loop into a glibc wcslen call built for 32-bit wchar_t. This caused path truncation and memory over-reads at -O2.<br />
<br />
The -fshort-wchar flag was removed, which fixed the issue.<br />
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">3. Vulkan backend fixes and improvements</span></span><br />
<br />
Fixed <span style="font-weight: bold;">15 tracked Vulkan issues</span>, including:<br />
<ul>
<li>Device loss from cube-texture views bound to 2D descriptor slots.</li>
<li>Crashes from in-flight buffer/image destruction, fixed with a new deferred-destruction system.</li>
<li>Scene corruption under heavy draw load caused by per-draw UBO ring wrapping. This was replaced with a byte-budget ring.</li>
<li>Eye-adaptation black frame caused by cross-stage image slot collisions.</li>
<li>Per-draw descriptor allocation was O(draws). Switched to dynamic UBO offsets.</li>
<li>Terrain “sparkle” caused by the BRDF LUT being deleted on every render-target reset.</li>
<li>Added per-pipeline descriptor sets.</li>
<li>Added a disk-persistent pipeline cache.</li>
<li>Reduced sync-validation errors from <span style="font-weight: bold;">10 to 0</span>.</li>
<li>Added env-gated abort-on-error debugging tools.<br />
</li></ul>
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">4. Upstream Esenthel sync</span></span><br />
<br />
Merged upstream main back in, regenerated affected shader paks, and fixed a .gitattributes issue that was causing bogus whole-file merge conflicts.<br />
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">5. Large World: 64-bit floating origin</span></span><br />
<ul>
<li>Added runtime floating-origin support in Game::World.</li>
<li>Added camera rebase support.</li>
<li>Added tile-relative area storage.</li>
<li>Added water, decal, and navmesh support for large worlds.</li>
<li>Added local-frame pathfinding wrappers.</li>
<li>Added editor support for large worlds, including jitter-free terrain editing and object placement far from origin.</li>
<li>Updated undo/redo so it survives rebasing.</li>
<li>Added world-coordinate display panels.</li>
<li>Added tile-relative game export.</li>
<li>Added an area relocate/convert tool for existing worlds.<br />
</li></ul>
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">6. Procedural terrain + Azgaar world import</span></span><br />
<ul>
<li>Titan Editor supports Import of Azgaar Maps and seamless blending of Procedural+Tiled areas</li>
<li>Added runtime streamed procedural terrain generation.</li>
<li>Generation is multithreaded and prioritizes areas nearest the camera.</li>
<li>Added a far-LOD geo-clipmap backdrop ring so terrain reaches the horizon.</li>
<li>Added climate-driven biomes with per-biome terrain shapes: flat, rolling, rugged, and dunes.</li>
<li>Added three blended ground materials to reduce visible tiling.</li>
<li>Added terrain multi-texturing by slope and altitude.</li>
<li>Added object scatter for trees and rocks, including static collision.</li>
<li>Added sea level and ocean support.</li>
<li>Added rivers carved from Azgaar map data with mitred winding channels.</li>
<li>Added above-sea-level lake basins.</li>
<li>Water and terrain now render at full detail to the horizon.</li>
<li>Added Azgaar Fantasy Map Generator import. Heightmap and biome control maps drive the generated world.</li>
<li>Added an in-game minimap using M, with player position, facing arrow, and click-to-teleport.</li>
<li>Added editor tools: Procedural Terrain dialog, Biome Editor, and Rivers &amp; Lakes dialog.</li>
<li>Procedural config is included with the exported world.</li>
<li>Added per-area navmesh tiles for generated terrain.</li>
<li>Added a tutorial: <span style="font-weight: bold;">“Procedural World”</span>. 250 km x 250 km Azgaar Map Demo.<br />
</li></ul>
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">7. Portals</span></span><br />
<ul>
<li>Added portal authoring in the editor.</li>
<li>Added runtime portal crossing.</li>
<li>Added a secondary-streaming-foci API so the destination side can pre-stream before the player crosses.</li>
<li>Added tutorial <span style="font-weight: bold;">“21 - Portals”</span> with a sample world.</li>
<li>Portal crossing is seamless, with no flash.</li>
<li>The streaming ring on the departed side is preserved during crossing.</li>
<li>Added a streaming-status HUD.<br />
</li></ul>
<br />
<br />
<span style="font-size: 14pt;"><span style="font-weight: bold;">Honorable mentions</span></span><br />
<ul>
<li>RmlUi improvements: icon: texture loading from the engine image cache, plus same-texture draw batching.</li>
<li>Fixed stale input-action state on popped or masked input maps.</li>
<li>Added memory hardening from ASAN runs, including out-of-bounds guards and completion of the global new/delete family.</li>
<li>Made sizeof(Image) renderer-independent.<br />
</li></ul>
<br />
Source is in this fork: <a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a><br />
<br />
<a href="https://postimg.cc/QHx7vK6v" target="_blank"><img src="https://i.postimg.cc/wTDkyhDx/Screenshot-From-2026-06-12-21-24-05.png" border="0" alt="[Image: Screenshot-From-2026-06-12-21-24-05.png]" /></a><br />
<br />
<a href="https://postimg.cc/fJ2dsWpW" target="_blank"><img src="https://i.postimg.cc/XY3kwpg5/Screenshot-From-2026-06-12-21-24-39.png" border="0" alt="[Image: Screenshot-From-2026-06-12-21-24-39.png]" /></a><br />
<br />
<a href="https://postimg.cc/w7fNzxZ9" target="_blank"><img src="https://i.postimg.cc/cLZBQrct/Screenshot-From-2026-06-12-21-25-50.png" border="0" alt="[Image: Screenshot-From-2026-06-12-21-25-50.png]" /></a><br />
<br />
<a href="https://postimg.cc/fJ2dsWpR" target="_blank"><img src="https://i.postimg.cc/9QVYGrBZ/Screenshot-From-2026-06-12-21-27-49.png" border="0" alt="[Image: Screenshot-From-2026-06-12-21-27-49.png]" /></a><br />
<br />
<a href="https://postimg.cc/PP3DHf9r" target="_blank"><img src="https://i.postimg.cc/6pwfdyhR/Screenshot-From-2026-06-12-21-28-00.png" border="0" alt="[Image: Screenshot-From-2026-06-12-21-28-00.png]" /></a><br />
<br />
<a href="https://postimg.cc/sv6WzfqW" target="_blank"><img src="https://i.postimg.cc/13PrpfKv/Screenshot-From-2026-06-12-23-08-05.png" border="0" alt="[Image: Screenshot-From-2026-06-12-23-08-05.png]" /></a><br />
<br />
<a href="https://postimg.cc/1fCFSR2w" target="_blank"><img src="https://i.postimg.cc/YCkz1hxn/Screenshot-From-2026-06-12-23-08-23.png" border="0" alt="[Image: Screenshot-From-2026-06-12-23-08-23.png]" /></a><br />
<br />
<a href="https://postimg.cc/qzbKrBSG" target="_blank"><img src="https://i.postimg.cc/8zDmhjbx/Screenshot-From-2026-06-12-23-09-07.png" border="0" alt="[Image: Screenshot-From-2026-06-12-23-09-07.png]" /></a>]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Lag compensation]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11816</link>
			<pubDate>Wed, 22 Apr 2026 01:28:27 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11816</guid>
			<description><![CDATA[I had Claude Code improve and bring into the engine the lag compensation I use in my project, it makes FPS and Chivalry/Mount and Blade weapon trail swing games playable and fair even with some clients having 200+ ms pings (overseas players). I use ENET in my case but I kept it transport layer agnostic here.<br />
<br />
<span style="font-weight: bold;">Networking: Lag Compensation + Physics Rollback</span><br />
<span style="font-style: italic;">Transport-agnostic server-authoritative simulation layer (lag-compensation branch)</span><br />
<br />
A commercial-grade networking substrate built from scratch under *Engine/Net/*. Two independent tiers of temporal correction, built using shipped-game references:<br />
<ul>
<li><span style="font-weight: bold;">Tier 1 — selective hitbox rewind</span> (Photon Fusion 2 / Source / CS:GO model). Each server tick, every replicated actor contributes a flat list of hitbox samples into a 256-tick ring buffer (~4.27 s @ 60 Hz). Spatial queries take a *(tick, frac)* pair, linearly interpolate the two bracketing snapshots, and test against the interpolated hitboxes. The live gameplay scene is never touched — perfect for hitscan / melee where you only need “where was this limb when the client fired”.</li>
<li><span style="font-weight: bold;">Tier 2 — full PhysX rewind + resimulation</span> (UE5 networked-physics model). Snapshots pose / linear + angular velocity / kinematic / sleeping flags for every tracked *Actor* each tick; on demand restores them and drives *Physics.stepOnce(dt)* forward to catch up to the current tick. A *RollbackGuard* (TLS depth counter) is held active for the resim window so gameplay / animation / audio code that checks *RollbackGuard::active()* can suppress side-effects (event fire, notifies, SFX) during replay.<br />
</li></ul>
<br />
All modules are <span style="font-weight: bold;">unconditionally compiled</span> — no CMake flag, no opt-out, no extra link cost (the dependency surface is zero — pure C++, no third-party libraries). The transport layer is <span style="font-weight: bold;">abstract</span>: the engine ships *LoopbackTransport* (in-process queue) and *FakeLatencyTransport* (wraps any transport with configurable latency / jitter / loss), plus sketches for *ENetTransport* and a multi-hop *ForwardingTransport* in *Engine/H/Net/NetTransport.h* header comments. User projects subclass *INetTransport* and forward to ENet / Steam Sockets / WebRTC / whatever fits their deployment.<br />
<br />
<span style="font-weight: bold;">Module layout:</span><br />
<ul>
<li>*Net/NetTick.h/cpp* — *TickSeq* (*ULong*) + *NetTick{seq, frac}* shared tick + sub-tick type</li>
<li>*Net/NetClock.h/cpp* — NTP-style clock sync; rolling 5-sample median-of-offset + RTT + jitter; monotonic server timeline</li>
<li>*Net/NetTransport.h/cpp* — *INetTransport* abstract + *LoopbackTransport* + *FakeLatencyTransport* + ENet / multi-hop sketches</li>
<li>*Net/NetObject.h/cpp* — *INetReplicated* interface, *NetBase* convenience, *NET_REGISTER_CLASS* factory, *HitboxSample*, *NetWriter* / *NetReader* byte-buffer codecs with opt-in *putUIntPacked* (varint) / *putPosQ16* (quantized position, 6 B vs 12 B) compression helpers</li>
<li>*Net/InputCommand.h/cpp* — *InputCommand* POD + *InputBuffer* (client redundancy — re-sends last N unacked cmds) + *InputQueue* (server per-connection, wrap-safe *UShort* seq compare)</li>
<li>*Net/NetReplicator.h/cpp* — spawn / despawn / snapshot build + apply / input routing / ping-pong / per-connection relevance callback hook</li>
<li>*Net/LagCompensation.h/cpp* — <span style="font-weight: bold;">Tier 1</span> 256-tick hitbox ring; *ray* / *sweep* / *sweepArc* / *overlap* / *fetchInterp* CPU-analytic queries (sphere + capsule + box) with *(tick, frac)* OR *(real_time)* addressing for clustered servers</li>
<li>*Net/RollbackGuard.h/cpp* — thread-local depth counter; *RollbackGuard::active()* callable from gameplay / anim code</li>
<li>*Net/NetRollback.h/cpp* — <span style="font-weight: bold;">Tier 2</span> snapshot / restore *Actor* state, *Physics.stepOnce*-driven resim, 8-tick default cap (user-configurable), *InputReplayFn* hook, *AnimCaptureFn* / *AnimRestoreFn* hooks for bone-sourced hitboxes<br />
</li></ul>
<br />
~3,200 lines of new engine code across 18 files, zero third-party links, zero *#ifdef*s outside platform branches.<br />
<br />
<span style="font-weight: bold;">Quick start (single-process, both sides in one *Update()* — this is how every category-20 tutorial is structured):</span><br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>// --- Replicated actor ---<br />
struct Target : NetBase<br />
{<br />
Vec pos;<br />
NetClassID netClassId()C override { return 0x54475431u; /* 'TGT1' */ }<br />
void netSerialize&nbsp;&nbsp;(NetWriter &amp;w, UInt fields)C override { w.putVec(pos); }<br />
void netDeserialize(NetReader &amp;r, UInt fields)&nbsp;&nbsp;override { pos = r.getVec(); }<br />
void netHitboxes(Memc&lt;HitboxSample&gt; &amp;out)C override<br />
{<br />
HitboxSample &amp;s = out.New();<br />
s.shape = HITBOX_SPHERE;<br />
s.group = 0x0001;<br />
s.half_ext.set(0.3f, 0.3f, 0.3f);<br />
s.xform.identity(); s.xform.pos = pos;<br />
}<br />
};<br />
NET_REGISTER_CLASS(Target, 0x54475431u);<br />
<br />
// --- Server ---<br />
NetReplicator SvrRep;&nbsp;&nbsp;NetClock SvrClk;&nbsp;&nbsp;LagComp SvrLag;<br />
SvrClk.initServer(60);<br />
SvrRep.serverInit(&amp;transport, &amp;SvrClk);<br />
<br />
each_tick:<br />
SvrRep.serverDrainInputs();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // apply client inputs via INetReplicated::netApplyInput<br />
// ...gameplay simulation...<br />
SvrLag.recordTick(SvrRep, tick, Time.curTime());<br />
if(tick % 3 == 0) SvrRep.sendSnapshots(tick);&nbsp;&nbsp; // 20 Hz @ 60 tick<br />
<br />
on_client_fire(from, to, client_tick):<br />
LagHit h;<br />
if(SvrLag.ray(client_tick, 0.0f, from, to, h))<br />
apply_damage(h.owner, h.owner_idx);<br />
<br />
// --- Client ---<br />
NetReplicator CliRep;&nbsp;&nbsp;NetClock CliClk;&nbsp;&nbsp;InputBuffer CliInputs;<br />
CliClk.initClient(60);<br />
CliRep.clientInit(&amp;transport, &amp;CliClk, /*server_conn*/1);<br />
<br />
each_frame:<br />
CliRep.sendPingMaybe();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // periodic NTP sync<br />
CliRep.update(Time.curTime());&nbsp;&nbsp;&nbsp;&nbsp;// drains spawn/despawn/snapshot/pong<br />
Vec input = sample_keyboard();<br />
pawn-&gt;predict(input);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // local prediction on owned pawn<br />
CliInputs.push(input, buttons, CliClk.tick());<br />
CliInputs.sendTo(CliRep);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // redundant — includes last N unacked<br />
CliInputs.ack(CliRep.lastAckedSeq());<br />
<br />
on_fire(from, to):<br />
// Ship the compensation tick with the fire cmd so the server can rewind<br />
// to exactly what this client was seeing.<br />
fire_cmd.compensation_tick = CliRep.lastSnapshotTick();</code></div></div>
<br />
<span style="font-weight: bold;">Tier 2 (physics rewind) — only when you actually need resim; most games won't:</span><br />
<br />
<span style="font-style: italic;">NetRollback SvrRoll;</span><br />
<span style="font-style: italic;">SvrRoll.init(1.0f/60.0f, /*max_resim=*/8);</span><br />
<span style="font-style: italic;">for(Actor *a : dynamic_physics_actors) SvrRoll.track(*a);</span><br />
<br />
<span style="font-style: italic;">on_physics_step_completed(tick):</span><br />
<span style="font-style: italic;">SvrRoll.recordTick(tick);</span><br />
<br />
<span style="font-style: italic;">on_authoritative_correction(target_tick):</span><br />
<span style="font-style: italic;">SvrRoll.rewindAndResimulate(target_tick, currentTick, replayInputs, user);</span><br />
<br />
<span style="font-weight: bold;">Physics engine touch points</span> — two small, surgical additions; everything else is pure add-on:<br />
<ul>
<li>*Physics.stepOnce(Flt dt)* — direct *simulate* + *fetchResults* with no accumulator, no *simulation_step_completed* callback. This is what drives Tier 2 resim. Safe only between frames (after *stopSimulation()* or inside *WorldManager::physics_update()*).</li>
<li>*Time.rollbackDt(Flt dt)* — thread-local override for *Time.d()*. When *&gt; 0*, *Time.d()* returns it instead of the real frame delta. Set automatically by *NetRollback::rewindAndResimulate* per resim tick to the recorded *dt_used*, so animation code (*Motion::updateAuto*, *AnimatedSkeleton::animate*) replays at the exact dt the forward sim used — deterministic bone poses during rewind.<br />
</li></ul>
<br />
<span style="font-weight: bold;">Animation sync hooks for bone-driven hitboxes</span> — if your game's hitboxes are attached to *AnimatedSkeleton* bones (limbs parented to bone slots), both forward-sim animation and rollback-resim animation must reproduce the same per-tick bone poses. Two engine hooks pair them:<br />
<br />
<ol type="1">
<li><span style="font-weight: bold;">Forward sim</span> — set *Time.rollbackDt(icf.timeDeltaPhysics)* before animating each player tick, so *Motion::updateAuto* uses the physics dt instead of the variable frame dt. Restore with *Time.rollbackDt(0)*.</li>
<li><span style="font-weight: bold;">Rollback resim</span> — pass *NetRollback::setAnimCallbacks(cap, rest, user)*. *AnimCaptureFn* serialises every tracked *Motion* (via *Motion::save* or direct field writes) into a small opaque blob per tick (~64 B per motion × N motions × 256 ticks — fits easily). *AnimRestoreFn* does the inverse <span style="font-weight: bold;">before</span> the user's *InputReplayFn* fires, so replayed animation code starts from the correct *Motion.time* / *Motion.blend*. Pair this with *if(RollbackGuard::active()) return;* at the top of anim-event consumers (weapon clash SFX, hit sparks) so they don't re-fire during replay.<br />
</li></ol>
<br />
<br />
Includes 7 more tutorials.<br />
<a href="https://postimg.cc/hhh408RZ" target="_blank"><img src="https://i.postimg.cc/gcHwm4Sp/Screenshot-from-2026-04-21-21-12-54.png" border="0" alt="[Image: Screenshot-from-2026-04-21-21-12-54.png]" /></a><br />
<br />
in this fork:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></description>
			<content:encoded><![CDATA[I had Claude Code improve and bring into the engine the lag compensation I use in my project, it makes FPS and Chivalry/Mount and Blade weapon trail swing games playable and fair even with some clients having 200+ ms pings (overseas players). I use ENET in my case but I kept it transport layer agnostic here.<br />
<br />
<span style="font-weight: bold;">Networking: Lag Compensation + Physics Rollback</span><br />
<span style="font-style: italic;">Transport-agnostic server-authoritative simulation layer (lag-compensation branch)</span><br />
<br />
A commercial-grade networking substrate built from scratch under *Engine/Net/*. Two independent tiers of temporal correction, built using shipped-game references:<br />
<ul>
<li><span style="font-weight: bold;">Tier 1 — selective hitbox rewind</span> (Photon Fusion 2 / Source / CS:GO model). Each server tick, every replicated actor contributes a flat list of hitbox samples into a 256-tick ring buffer (~4.27 s @ 60 Hz). Spatial queries take a *(tick, frac)* pair, linearly interpolate the two bracketing snapshots, and test against the interpolated hitboxes. The live gameplay scene is never touched — perfect for hitscan / melee where you only need “where was this limb when the client fired”.</li>
<li><span style="font-weight: bold;">Tier 2 — full PhysX rewind + resimulation</span> (UE5 networked-physics model). Snapshots pose / linear + angular velocity / kinematic / sleeping flags for every tracked *Actor* each tick; on demand restores them and drives *Physics.stepOnce(dt)* forward to catch up to the current tick. A *RollbackGuard* (TLS depth counter) is held active for the resim window so gameplay / animation / audio code that checks *RollbackGuard::active()* can suppress side-effects (event fire, notifies, SFX) during replay.<br />
</li></ul>
<br />
All modules are <span style="font-weight: bold;">unconditionally compiled</span> — no CMake flag, no opt-out, no extra link cost (the dependency surface is zero — pure C++, no third-party libraries). The transport layer is <span style="font-weight: bold;">abstract</span>: the engine ships *LoopbackTransport* (in-process queue) and *FakeLatencyTransport* (wraps any transport with configurable latency / jitter / loss), plus sketches for *ENetTransport* and a multi-hop *ForwardingTransport* in *Engine/H/Net/NetTransport.h* header comments. User projects subclass *INetTransport* and forward to ENet / Steam Sockets / WebRTC / whatever fits their deployment.<br />
<br />
<span style="font-weight: bold;">Module layout:</span><br />
<ul>
<li>*Net/NetTick.h/cpp* — *TickSeq* (*ULong*) + *NetTick{seq, frac}* shared tick + sub-tick type</li>
<li>*Net/NetClock.h/cpp* — NTP-style clock sync; rolling 5-sample median-of-offset + RTT + jitter; monotonic server timeline</li>
<li>*Net/NetTransport.h/cpp* — *INetTransport* abstract + *LoopbackTransport* + *FakeLatencyTransport* + ENet / multi-hop sketches</li>
<li>*Net/NetObject.h/cpp* — *INetReplicated* interface, *NetBase* convenience, *NET_REGISTER_CLASS* factory, *HitboxSample*, *NetWriter* / *NetReader* byte-buffer codecs with opt-in *putUIntPacked* (varint) / *putPosQ16* (quantized position, 6 B vs 12 B) compression helpers</li>
<li>*Net/InputCommand.h/cpp* — *InputCommand* POD + *InputBuffer* (client redundancy — re-sends last N unacked cmds) + *InputQueue* (server per-connection, wrap-safe *UShort* seq compare)</li>
<li>*Net/NetReplicator.h/cpp* — spawn / despawn / snapshot build + apply / input routing / ping-pong / per-connection relevance callback hook</li>
<li>*Net/LagCompensation.h/cpp* — <span style="font-weight: bold;">Tier 1</span> 256-tick hitbox ring; *ray* / *sweep* / *sweepArc* / *overlap* / *fetchInterp* CPU-analytic queries (sphere + capsule + box) with *(tick, frac)* OR *(real_time)* addressing for clustered servers</li>
<li>*Net/RollbackGuard.h/cpp* — thread-local depth counter; *RollbackGuard::active()* callable from gameplay / anim code</li>
<li>*Net/NetRollback.h/cpp* — <span style="font-weight: bold;">Tier 2</span> snapshot / restore *Actor* state, *Physics.stepOnce*-driven resim, 8-tick default cap (user-configurable), *InputReplayFn* hook, *AnimCaptureFn* / *AnimRestoreFn* hooks for bone-sourced hitboxes<br />
</li></ul>
<br />
~3,200 lines of new engine code across 18 files, zero third-party links, zero *#ifdef*s outside platform branches.<br />
<br />
<span style="font-weight: bold;">Quick start (single-process, both sides in one *Update()* — this is how every category-20 tutorial is structured):</span><br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>// --- Replicated actor ---<br />
struct Target : NetBase<br />
{<br />
Vec pos;<br />
NetClassID netClassId()C override { return 0x54475431u; /* 'TGT1' */ }<br />
void netSerialize&nbsp;&nbsp;(NetWriter &amp;w, UInt fields)C override { w.putVec(pos); }<br />
void netDeserialize(NetReader &amp;r, UInt fields)&nbsp;&nbsp;override { pos = r.getVec(); }<br />
void netHitboxes(Memc&lt;HitboxSample&gt; &amp;out)C override<br />
{<br />
HitboxSample &amp;s = out.New();<br />
s.shape = HITBOX_SPHERE;<br />
s.group = 0x0001;<br />
s.half_ext.set(0.3f, 0.3f, 0.3f);<br />
s.xform.identity(); s.xform.pos = pos;<br />
}<br />
};<br />
NET_REGISTER_CLASS(Target, 0x54475431u);<br />
<br />
// --- Server ---<br />
NetReplicator SvrRep;&nbsp;&nbsp;NetClock SvrClk;&nbsp;&nbsp;LagComp SvrLag;<br />
SvrClk.initServer(60);<br />
SvrRep.serverInit(&amp;transport, &amp;SvrClk);<br />
<br />
each_tick:<br />
SvrRep.serverDrainInputs();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // apply client inputs via INetReplicated::netApplyInput<br />
// ...gameplay simulation...<br />
SvrLag.recordTick(SvrRep, tick, Time.curTime());<br />
if(tick % 3 == 0) SvrRep.sendSnapshots(tick);&nbsp;&nbsp; // 20 Hz @ 60 tick<br />
<br />
on_client_fire(from, to, client_tick):<br />
LagHit h;<br />
if(SvrLag.ray(client_tick, 0.0f, from, to, h))<br />
apply_damage(h.owner, h.owner_idx);<br />
<br />
// --- Client ---<br />
NetReplicator CliRep;&nbsp;&nbsp;NetClock CliClk;&nbsp;&nbsp;InputBuffer CliInputs;<br />
CliClk.initClient(60);<br />
CliRep.clientInit(&amp;transport, &amp;CliClk, /*server_conn*/1);<br />
<br />
each_frame:<br />
CliRep.sendPingMaybe();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // periodic NTP sync<br />
CliRep.update(Time.curTime());&nbsp;&nbsp;&nbsp;&nbsp;// drains spawn/despawn/snapshot/pong<br />
Vec input = sample_keyboard();<br />
pawn-&gt;predict(input);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // local prediction on owned pawn<br />
CliInputs.push(input, buttons, CliClk.tick());<br />
CliInputs.sendTo(CliRep);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // redundant — includes last N unacked<br />
CliInputs.ack(CliRep.lastAckedSeq());<br />
<br />
on_fire(from, to):<br />
// Ship the compensation tick with the fire cmd so the server can rewind<br />
// to exactly what this client was seeing.<br />
fire_cmd.compensation_tick = CliRep.lastSnapshotTick();</code></div></div>
<br />
<span style="font-weight: bold;">Tier 2 (physics rewind) — only when you actually need resim; most games won't:</span><br />
<br />
<span style="font-style: italic;">NetRollback SvrRoll;</span><br />
<span style="font-style: italic;">SvrRoll.init(1.0f/60.0f, /*max_resim=*/8);</span><br />
<span style="font-style: italic;">for(Actor *a : dynamic_physics_actors) SvrRoll.track(*a);</span><br />
<br />
<span style="font-style: italic;">on_physics_step_completed(tick):</span><br />
<span style="font-style: italic;">SvrRoll.recordTick(tick);</span><br />
<br />
<span style="font-style: italic;">on_authoritative_correction(target_tick):</span><br />
<span style="font-style: italic;">SvrRoll.rewindAndResimulate(target_tick, currentTick, replayInputs, user);</span><br />
<br />
<span style="font-weight: bold;">Physics engine touch points</span> — two small, surgical additions; everything else is pure add-on:<br />
<ul>
<li>*Physics.stepOnce(Flt dt)* — direct *simulate* + *fetchResults* with no accumulator, no *simulation_step_completed* callback. This is what drives Tier 2 resim. Safe only between frames (after *stopSimulation()* or inside *WorldManager::physics_update()*).</li>
<li>*Time.rollbackDt(Flt dt)* — thread-local override for *Time.d()*. When *&gt; 0*, *Time.d()* returns it instead of the real frame delta. Set automatically by *NetRollback::rewindAndResimulate* per resim tick to the recorded *dt_used*, so animation code (*Motion::updateAuto*, *AnimatedSkeleton::animate*) replays at the exact dt the forward sim used — deterministic bone poses during rewind.<br />
</li></ul>
<br />
<span style="font-weight: bold;">Animation sync hooks for bone-driven hitboxes</span> — if your game's hitboxes are attached to *AnimatedSkeleton* bones (limbs parented to bone slots), both forward-sim animation and rollback-resim animation must reproduce the same per-tick bone poses. Two engine hooks pair them:<br />
<br />
<ol type="1">
<li><span style="font-weight: bold;">Forward sim</span> — set *Time.rollbackDt(icf.timeDeltaPhysics)* before animating each player tick, so *Motion::updateAuto* uses the physics dt instead of the variable frame dt. Restore with *Time.rollbackDt(0)*.</li>
<li><span style="font-weight: bold;">Rollback resim</span> — pass *NetRollback::setAnimCallbacks(cap, rest, user)*. *AnimCaptureFn* serialises every tracked *Motion* (via *Motion::save* or direct field writes) into a small opaque blob per tick (~64 B per motion × N motions × 256 ticks — fits easily). *AnimRestoreFn* does the inverse <span style="font-weight: bold;">before</span> the user's *InputReplayFn* fires, so replayed animation code starts from the correct *Motion.time* / *Motion.blend*. Pair this with *if(RollbackGuard::active()) return;* at the top of anim-event consumers (weapon clash SFX, hit sparks) so they don't re-fire during replay.<br />
</li></ol>
<br />
<br />
Includes 7 more tutorials.<br />
<a href="https://postimg.cc/hhh408RZ" target="_blank"><img src="https://i.postimg.cc/gcHwm4Sp/Screenshot-from-2026-04-21-21-12-54.png" border="0" alt="[Image: Screenshot-from-2026-04-21-21-12-54.png]" /></a><br />
<br />
in this fork:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[RmlUi]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11815</link>
			<pubDate>Mon, 20 Apr 2026 13:58:58 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11815</guid>
			<description><![CDATA[Claude Code was able to one-shot RmlUi integration into Esenthel.<br />
<br />
I'm not a web-dev person, but with Vibe Designing coming soon<br />
( e.g. <a href="https://claude.ai/design" target="_blank">https://claude.ai/design</a> ) , I think there is value in having a UI<br />
system defined by a declarative language like HTML/CSS.<br />
<br />
This also makes it much easier for users to mod or skin the UI if they<br />
want. But really, it's about making it easier for the AI to do it.<br />
Claude Code really seems to struggle with building Esenthel GUIs<br />
programmatically that look good and have the correct font size, etc.,<br />
but with HTML/CSS this problem doesn't exist.<br />
<br />
I haven't profiled this, the implementation has not been optimized,<br />
and I'm sure there is a performance penalty. That said, RmlUi is<br />
highly regarded and is designed for games; it is not a full WebKit.<br />
<br />
An integration of <span style="font-weight: bold;"><a href="https://github.com/mikke89/RmlUi" target="_blank">RmlUi</a> v6.2</span> (MIT) as<br />
a retained-mode HTML/CSS UI library that <span style="font-weight: bold;">coexists</span> with Esenthel's built-in<br />
<span style="font-style: italic;">Gui</span> system without touching it. Both run in the same frame — the native<br />
widget set is still available for lightweight 3D-integrated controls, while<br />
RmlUi handles HTML/CSS/animation-heavy UI (menus, HUDs, settings screens,<br />
chat windows). A single RmlUi render backend lives inside the Engine static<br />
library and drives RmlUi's vertex stream through the engine's existing 2D<br />
batcher, so every renderer backend (DX11, DX12, Vulkan, OpenGL) gets RmlUi<br />
for free with zero backend-specific code.<br />
<br />
Game HUD tutorial<br />
<a href="https://pixeldrain.com/api/file/nWMwv7tq" target="_blank"><img src="https://pixeldrain.com/api/file/nWMwv7tq" width="800" height="339" border="0" alt="[Image: nWMwv7tq]" /></a><br />
<br />
<br />
Using Esenthel GUI elements and RmlUi elements side-by-side tutorial<br />
<a href="https://pixeldrain.com/api/file/3cB1Nk65" target="_blank"><img src="https://pixeldrain.com/api/file/3cB1Nk65" width="800" height="339" border="0" alt="[Image: 3cB1Nk65]" /></a><br />
<br />
<br />
<span style="font-weight: bold;">Quick start:</span><br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>#include "Gui/RmlUi.h"&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// EE::RmlUi::* facade<br />
<br />
// In a single .cpp where you author Rml event listeners, DOM manipulation,<br />
// etc. — wrap the RmlUi includes so Esenthel macros and X11 defines don't<br />
// collide with RmlUi's template parameters and enum names:<br />
#include "../../ThirdPartyLibs/begin.h"<br />
#undef T1&nbsp;&nbsp;#undef T2&nbsp;&nbsp;#undef T3&nbsp;&nbsp;#undef T4&nbsp;&nbsp;#undef T5<br />
#undef T6&nbsp;&nbsp;#undef T7&nbsp;&nbsp;#undef T8&nbsp;&nbsp;#undef T9&nbsp;&nbsp;#undef T10&nbsp;&nbsp;#undef T11<br />
#undef Always&nbsp;&nbsp;#undef NotUseful<br />
#include &lt;RmlUi/Core.h&gt;<br />
#include "../../ThirdPartyLibs/end.h"<br />
<br />
static Rml::Context&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; *Ctx=null;<br />
static Rml::ElementDocument *Doc=null;<br />
<br />
Bool Init()<br />
{<br />
&nbsp;&nbsp;&nbsp;&nbsp;EE::RmlUi::Init();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // install render + system interfaces<br />
&nbsp;&nbsp;&nbsp;&nbsp;EE::RmlUi::LoadFontFace("Data/RmlUi/LatoLatin-Regular.ttf");<br />
&nbsp;&nbsp;&nbsp;&nbsp;Ctx=EE::RmlUi::CreateContext();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// sized to D.resW()/D.resH()<br />
&nbsp;&nbsp;&nbsp;&nbsp;Doc=Ctx-&gt;LoadDocument("Data/RmlUi/demo.rml");<br />
&nbsp;&nbsp;&nbsp;&nbsp;if(Doc)Doc-&gt;Show();<br />
&nbsp;&nbsp;&nbsp;&nbsp;return true;<br />
}<br />
void Shut()&nbsp;&nbsp; { EE::RmlUi::Shut(); }<br />
Bool Update() {<br />
&nbsp;&nbsp;&nbsp;&nbsp;if(Kb.bp(KB_ESC))return false;<br />
&nbsp;&nbsp;&nbsp;&nbsp;Gui.update();<br />
&nbsp;&nbsp;&nbsp;&nbsp;EE::RmlUi::PumpInput(Ctx);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // Ms/Kb -&gt; context<br />
&nbsp;&nbsp;&nbsp;&nbsp;EE::RmlUi::Update&nbsp;&nbsp; (Ctx);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // events, animations, dirty geometry<br />
&nbsp;&nbsp;&nbsp;&nbsp;return true;<br />
}<br />
void Draw()&nbsp;&nbsp; {<br />
&nbsp;&nbsp;&nbsp;&nbsp;Renderer(RenderScene);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // optional 3D world<br />
&nbsp;&nbsp;&nbsp;&nbsp;Gui.draw();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// Esenthel Gui above 3D<br />
&nbsp;&nbsp;&nbsp;&nbsp;EE::RmlUi::Render(Ctx);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// RmlUi on top of everything<br />
}</code></div></div>
<br />
<span style="font-weight: bold;">Shipped features:</span><br />
<ul>
<li><span style="font-weight: bold;">Single-TU backend</span> — <span style="font-style: italic;">Engine/Source/Gui/RmlUi.cpp</span> is the only engine<br />
source file that includes RmlUi headers. It subclasses<br />
<span style="font-style: italic;">Rml::RenderInterface</span> and <span style="font-style: italic;">Rml::SystemInterface</span>, owns the 1×1 white<br />
fallback texture, and exports the <span style="font-style: italic;">EE::RmlUi::</span> facade used by app code.<br />
Macro pollution is neutralised locally: Esenthel's template-shortcut<br />
macros <span style="font-style: italic;">T1..T11</span> (from <span style="font-style: italic;">Engine/H/_/defines.h</span>, they collide with<br />
<span style="font-style: italic;">robin_hood.h</span>'s template parameters) and X11's <span style="font-style: italic;">Always</span> / <span style="font-style: italic;">NotUseful</span><br />
(not stripped by <span style="font-style: italic;">Engine/H/_/headers.h</span>) are undefined inside the<br />
<span style="font-style: italic;">ThirdPartyLibs/begin.h</span> / <span style="font-style: italic;">end.h</span> bracket just for this one TU.</li>
<li><span style="font-weight: bold;">API-agnostic rendering</span> — <span style="font-style: italic;">RenderGeometry</span> feeds RmlUi's pixel-space<br />
vertex stream into the engine's existing 2D path. Per-vertex<br />
conversion goes through <span style="font-style: italic;">D.pixelToScreen(Vec2)</span> (canonical helper; no<br />
hand-rolled Y-flip). <span style="font-style: italic;">D.alpha(ALPHA_MERGE)</span> handles RmlUi's<br />
premultiplied RGBA (which it emits since v5). Scissor goes through<br />
<span style="font-style: italic;">D.clip(&amp;engine_rect)</span> / <span style="font-style: italic;">D.pixelToScreen(RectI)</span>. Works identically on<br />
DX11, DX12, Vulkan, OpenGL.</li>
<li><span style="font-weight: bold;">Untextured-path fallback</span> — RmlUi calls <span style="font-style: italic;">RenderGeometry</span> with<br />
<span style="font-style: italic;">texture=0</span> for solid-colour geometry. The backend binds a pre-built<br />
1×1 opaque-white <span style="font-style: italic;">Image</span> so the textured shader path effectively<br />
degenerates to plain vertex colour. This avoids needing a separate<br />
colour-only code path.</li>
<li><span style="font-weight: bold;">FreeType sharing</span> — RmlUi's default FreeType font engine is pointed<br />
at Esenthel's prebuilt <span style="font-style: italic;">EE_FreeType</span> static lib via<br />
<span style="font-style: italic;">FREETYPE_INCLUDE_DIRS</span> / <span style="font-style: italic;">FREETYPE_LIBRARY</span> cache vars in<br />
<span style="font-style: italic;">ThirdPartyLibs/RmlUi/CMakeLists.txt</span>, so one copy of FreeType ships in<br />
the final link. <span style="font-style: italic;">EE_RMLUI_USE_BUNDLED_FREETYPE=ON</span> is an escape hatch<br />
if the prebuilt's config flags ever diverge from what RmlUi expects.</li>
<li><span style="font-weight: bold;">Texture upload</span> — <span style="font-style: italic;">LoadTexture</span> routes to <span style="font-style: italic;">Image.Import</span>, accepting<br />
every format Esenthel's asset pipeline supports (BMP/PNG/JPG/JXL/WEBP/<br />
AVIF/HEIF/TGA/TIF/DDS/PSD/ICO/HDR). <span style="font-style: italic;">GenerateTexture</span> uploads raw<br />
RGBA8 via a soft image, lock/write, row-copy, unlock, and GPU upload<br />
(used for the font atlas and procedural decorator textures).</li>
<li><span style="font-weight: bold;">Dual-mode input pump</span> — <span style="font-style: italic;">EE::RmlUi::PumpInput(ctx)</span> polls<br />
mouse position, mouse buttons, and wheel every frame, walks all<br />
256 <span style="font-style: italic;">KB_KEY</span> values firing key down/up events on edge changes, and<br />
pumps text characters out of <span style="font-style: italic;">Kb.k.c</span>. When the focused RmlUi element<br />
is an input field or textarea the key queue is drained so fast typing<br />
never drops characters; otherwise the head of the queue is peeked<br />
non-destructively so the built-in Gui still sees the same keys on the<br />
same frame.</li>
<li><span style="font-weight: bold;">Opt-in via CMake</span> — <span style="font-style: italic;">option(EE_RMLUI "... " ON)</span> in the root<br />
<span style="font-style: italic;">CMakeLists.txt</span>, default ON on Linux x64 + Windows x64, fatal on iOS<br />
/ Android (deferred until those platforms are tested). With<br />
<span style="font-style: italic;">EE_RMLUI=OFF</span> every integration TU is empty, the<br />
<span style="font-style: italic;">rmlui_core</span> / <span style="font-style: italic;">rmlui_debugger</span> targets are not added to<br />
the build graph, and the tutorial targets are skipped in<br />
<span style="font-style: italic;">Tutorials/CMakeLists.txt</span>.</li>
<li><span style="font-weight: bold;">Built from source</span> — RmlUi v6.2 is vendored at<br />
<span style="font-style: italic;">ThirdPartyLibs/RmlUi/lib/</span> and compiled via subdirectory inclusion so<br />
<span style="font-style: italic;">rmlui_core</span> + <span style="font-style: italic;">rmlui_debugger</span> become Engine interface link deps.<br />
No prebuilt library like the other third-parties, because upstream<br />
ships clean CMake and the library is small. ABI-visible flags are<br />
forced onto both targets so they match Engine's ABI.</li>
<li><span style="font-weight: bold;">Debugger overlay</span> — <span style="font-style: italic;">rmlui_debugger</span> is linked automatically when<br />
<span style="font-style: italic;">EE_RMLUI=ON</span>. Call <span style="font-style: italic;">Rml::Debugger::Initialise(ctx)</span> once, then<br />
<span style="font-style: italic;">Rml::Debugger::SetVisible(true)</span> (or bind to a key) to pop up the live<br />
inspector — Event Log, Element Info, Outlines, Data Models tabs.</li>
<li><span style="font-weight: bold;">Thin facade</span> — <span style="font-style: italic;">Engine/H/Gui/RmlUi.h</span> exposes only the<br />
<span style="font-style: italic;">EE::RmlUi::</span> lifecycle and helper functions. It forward-declares<br />
<span style="font-style: italic;">Rml::Context</span>, <span style="font-style: italic;">ElementDocument</span>, <span style="font-style: italic;">RenderInterface</span>, and<br />
<span style="font-style: italic;">SystemInterface</span>, so app translation units that just drive lifecycle<br />
do not pay to parse all of <span style="font-style: italic;">&amp;lt;RmlUi/Core.h&amp;gt;</span>. App translation units<br />
that want event listeners, DOM mutation, or typed element access can<br />
include <span style="font-style: italic;">&amp;lt;RmlUi/Core.h&amp;gt;</span> themselves.<br />
</li></ul>
<br />
<span style="font-weight: bold;">Non-invasive integration</span> — zero changes to <span style="font-style: italic;">Engine/Source/Gui/Gui.cpp</span>,<br />
<span style="font-style: italic;">Gui.draw</span>, <span style="font-style: italic;">Gui.update</span>, no new state in <span style="font-style: italic;">DisplayClass</span>, no new<br />
alpha mode, no new shaders. The pre-existing <span style="font-style: italic;">VI</span> + <span style="font-style: italic;">D</span> 2D path is the<br />
only rendering surface used; the existing 2D shaders are the only ones<br />
touched. The tutorial harness (<span style="font-style: italic;">Tutorials/TutorialAuto.{h,cpp}</span>,<br />
<span style="font-style: italic;">Tutorials/stdafx.h</span>) is unchanged. <span style="font-style: italic;">EE_RMLUI=OFF</span> builds are<br />
pixel-identical to pre-integration builds (verified against<br />
<span style="font-style: italic;">Tutorial_05_Bars</span>, <span style="font-style: italic;">Tutorial_04_BatchedDrawing</span>,<br />
<span style="font-style: italic;">Tutorial_12_RenderToTexture</span>).<br />
<br />
<span style="font-weight: bold;">RCSS gotcha</span> — RmlUi's transition/animation parser recognises tween<br />
names as defined in <span style="font-style: italic;">Source/Core/PropertyParserAnimation.cpp</span>. Unlike<br />
standard CSS, it does <span style="font-weight: bold;">not</span> accept bare <span style="font-style: italic;">linear</span> or the CSS keyword<br />
<span style="font-style: italic;">ease</span>; use <span style="font-style: italic;">linear-in-out</span> and <span style="font-style: italic;">cubic-in-out</span> (or any of the<br />
supported <span style="font-style: italic;">{back,bounce,circular,cubic,elastic,exponential,linear,quadratic,quartic,quintic&#8203;,sine}-{in,out,in-out}</span><br />
variants). Multi-transitions are comma-separated.<br />
<br />
<span style="font-weight: bold;">Deferred work:</span><br />
<ul>
<li>Custom <span style="font-style: italic;">Rml::FontEngineInterface</span> on top of Esenthel's <span style="font-style: italic;">Font</span> class, so<br />
<span style="font-style: italic;">.pak</span>-packaged fonts could feed RmlUi. Upstream's default FreeType<br />
engine is sufficient for now; TTF files ship alongside the tutorials.</li>
<li><span style="font-style: italic;">Rml::FileInterface</span> on <span style="font-style: italic;">EE::File</span> — would let RmlUi resolve<br />
<span style="font-style: italic;">.rml</span> / <span style="font-style: italic;">.rcss</span> / texture paths through engine <span style="font-style: italic;">.pak</span> archives. The<br />
stdio <span style="font-style: italic;">fopen</span> default is used currently; tutorials ship plain files<br />
under <span style="font-style: italic;">Tutorials/Data/RmlUi/</span>.</li>
<li>Data Binding (MVC layer via <span style="font-style: italic;">Rml::DataModelConstructor</span>) — supported by<br />
the linked <span style="font-style: italic;">rmlui_core</span> but not yet demonstrated by a tutorial.</li>
<li>Lua bindings (<span style="font-style: italic;">rmlui_lua</span>) — disabled in the wrapper (<span style="font-style: italic;">RMLUI_LUA_BINDINGS=OFF</span>);<br />
enabling would require wiring an Esenthel Lua runtime into the engine<br />
first.</li>
<li>Windows smoke sweep via <span style="font-style: italic;">run_smoke_test.ps1</span>. Linux is covered<br />
(all five tutorials + three regression targets pass).<br />
</li></ul>
<br />
Available in this repo:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></description>
			<content:encoded><![CDATA[Claude Code was able to one-shot RmlUi integration into Esenthel.<br />
<br />
I'm not a web-dev person, but with Vibe Designing coming soon<br />
( e.g. <a href="https://claude.ai/design" target="_blank">https://claude.ai/design</a> ) , I think there is value in having a UI<br />
system defined by a declarative language like HTML/CSS.<br />
<br />
This also makes it much easier for users to mod or skin the UI if they<br />
want. But really, it's about making it easier for the AI to do it.<br />
Claude Code really seems to struggle with building Esenthel GUIs<br />
programmatically that look good and have the correct font size, etc.,<br />
but with HTML/CSS this problem doesn't exist.<br />
<br />
I haven't profiled this, the implementation has not been optimized,<br />
and I'm sure there is a performance penalty. That said, RmlUi is<br />
highly regarded and is designed for games; it is not a full WebKit.<br />
<br />
An integration of <span style="font-weight: bold;"><a href="https://github.com/mikke89/RmlUi" target="_blank">RmlUi</a> v6.2</span> (MIT) as<br />
a retained-mode HTML/CSS UI library that <span style="font-weight: bold;">coexists</span> with Esenthel's built-in<br />
<span style="font-style: italic;">Gui</span> system without touching it. Both run in the same frame — the native<br />
widget set is still available for lightweight 3D-integrated controls, while<br />
RmlUi handles HTML/CSS/animation-heavy UI (menus, HUDs, settings screens,<br />
chat windows). A single RmlUi render backend lives inside the Engine static<br />
library and drives RmlUi's vertex stream through the engine's existing 2D<br />
batcher, so every renderer backend (DX11, DX12, Vulkan, OpenGL) gets RmlUi<br />
for free with zero backend-specific code.<br />
<br />
Game HUD tutorial<br />
<a href="https://pixeldrain.com/api/file/nWMwv7tq" target="_blank"><img src="https://pixeldrain.com/api/file/nWMwv7tq" width="800" height="339" border="0" alt="[Image: nWMwv7tq]" /></a><br />
<br />
<br />
Using Esenthel GUI elements and RmlUi elements side-by-side tutorial<br />
<a href="https://pixeldrain.com/api/file/3cB1Nk65" target="_blank"><img src="https://pixeldrain.com/api/file/3cB1Nk65" width="800" height="339" border="0" alt="[Image: 3cB1Nk65]" /></a><br />
<br />
<br />
<span style="font-weight: bold;">Quick start:</span><br />
<br />
<div class="codeblock">
<div class="title">Code:<br />
</div><div class="body" dir="ltr"><code>#include "Gui/RmlUi.h"&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// EE::RmlUi::* facade<br />
<br />
// In a single .cpp where you author Rml event listeners, DOM manipulation,<br />
// etc. — wrap the RmlUi includes so Esenthel macros and X11 defines don't<br />
// collide with RmlUi's template parameters and enum names:<br />
#include "../../ThirdPartyLibs/begin.h"<br />
#undef T1&nbsp;&nbsp;#undef T2&nbsp;&nbsp;#undef T3&nbsp;&nbsp;#undef T4&nbsp;&nbsp;#undef T5<br />
#undef T6&nbsp;&nbsp;#undef T7&nbsp;&nbsp;#undef T8&nbsp;&nbsp;#undef T9&nbsp;&nbsp;#undef T10&nbsp;&nbsp;#undef T11<br />
#undef Always&nbsp;&nbsp;#undef NotUseful<br />
#include &lt;RmlUi/Core.h&gt;<br />
#include "../../ThirdPartyLibs/end.h"<br />
<br />
static Rml::Context&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; *Ctx=null;<br />
static Rml::ElementDocument *Doc=null;<br />
<br />
Bool Init()<br />
{<br />
&nbsp;&nbsp;&nbsp;&nbsp;EE::RmlUi::Init();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // install render + system interfaces<br />
&nbsp;&nbsp;&nbsp;&nbsp;EE::RmlUi::LoadFontFace("Data/RmlUi/LatoLatin-Regular.ttf");<br />
&nbsp;&nbsp;&nbsp;&nbsp;Ctx=EE::RmlUi::CreateContext();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// sized to D.resW()/D.resH()<br />
&nbsp;&nbsp;&nbsp;&nbsp;Doc=Ctx-&gt;LoadDocument("Data/RmlUi/demo.rml");<br />
&nbsp;&nbsp;&nbsp;&nbsp;if(Doc)Doc-&gt;Show();<br />
&nbsp;&nbsp;&nbsp;&nbsp;return true;<br />
}<br />
void Shut()&nbsp;&nbsp; { EE::RmlUi::Shut(); }<br />
Bool Update() {<br />
&nbsp;&nbsp;&nbsp;&nbsp;if(Kb.bp(KB_ESC))return false;<br />
&nbsp;&nbsp;&nbsp;&nbsp;Gui.update();<br />
&nbsp;&nbsp;&nbsp;&nbsp;EE::RmlUi::PumpInput(Ctx);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // Ms/Kb -&gt; context<br />
&nbsp;&nbsp;&nbsp;&nbsp;EE::RmlUi::Update&nbsp;&nbsp; (Ctx);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // events, animations, dirty geometry<br />
&nbsp;&nbsp;&nbsp;&nbsp;return true;<br />
}<br />
void Draw()&nbsp;&nbsp; {<br />
&nbsp;&nbsp;&nbsp;&nbsp;Renderer(RenderScene);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // optional 3D world<br />
&nbsp;&nbsp;&nbsp;&nbsp;Gui.draw();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// Esenthel Gui above 3D<br />
&nbsp;&nbsp;&nbsp;&nbsp;EE::RmlUi::Render(Ctx);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// RmlUi on top of everything<br />
}</code></div></div>
<br />
<span style="font-weight: bold;">Shipped features:</span><br />
<ul>
<li><span style="font-weight: bold;">Single-TU backend</span> — <span style="font-style: italic;">Engine/Source/Gui/RmlUi.cpp</span> is the only engine<br />
source file that includes RmlUi headers. It subclasses<br />
<span style="font-style: italic;">Rml::RenderInterface</span> and <span style="font-style: italic;">Rml::SystemInterface</span>, owns the 1×1 white<br />
fallback texture, and exports the <span style="font-style: italic;">EE::RmlUi::</span> facade used by app code.<br />
Macro pollution is neutralised locally: Esenthel's template-shortcut<br />
macros <span style="font-style: italic;">T1..T11</span> (from <span style="font-style: italic;">Engine/H/_/defines.h</span>, they collide with<br />
<span style="font-style: italic;">robin_hood.h</span>'s template parameters) and X11's <span style="font-style: italic;">Always</span> / <span style="font-style: italic;">NotUseful</span><br />
(not stripped by <span style="font-style: italic;">Engine/H/_/headers.h</span>) are undefined inside the<br />
<span style="font-style: italic;">ThirdPartyLibs/begin.h</span> / <span style="font-style: italic;">end.h</span> bracket just for this one TU.</li>
<li><span style="font-weight: bold;">API-agnostic rendering</span> — <span style="font-style: italic;">RenderGeometry</span> feeds RmlUi's pixel-space<br />
vertex stream into the engine's existing 2D path. Per-vertex<br />
conversion goes through <span style="font-style: italic;">D.pixelToScreen(Vec2)</span> (canonical helper; no<br />
hand-rolled Y-flip). <span style="font-style: italic;">D.alpha(ALPHA_MERGE)</span> handles RmlUi's<br />
premultiplied RGBA (which it emits since v5). Scissor goes through<br />
<span style="font-style: italic;">D.clip(&amp;engine_rect)</span> / <span style="font-style: italic;">D.pixelToScreen(RectI)</span>. Works identically on<br />
DX11, DX12, Vulkan, OpenGL.</li>
<li><span style="font-weight: bold;">Untextured-path fallback</span> — RmlUi calls <span style="font-style: italic;">RenderGeometry</span> with<br />
<span style="font-style: italic;">texture=0</span> for solid-colour geometry. The backend binds a pre-built<br />
1×1 opaque-white <span style="font-style: italic;">Image</span> so the textured shader path effectively<br />
degenerates to plain vertex colour. This avoids needing a separate<br />
colour-only code path.</li>
<li><span style="font-weight: bold;">FreeType sharing</span> — RmlUi's default FreeType font engine is pointed<br />
at Esenthel's prebuilt <span style="font-style: italic;">EE_FreeType</span> static lib via<br />
<span style="font-style: italic;">FREETYPE_INCLUDE_DIRS</span> / <span style="font-style: italic;">FREETYPE_LIBRARY</span> cache vars in<br />
<span style="font-style: italic;">ThirdPartyLibs/RmlUi/CMakeLists.txt</span>, so one copy of FreeType ships in<br />
the final link. <span style="font-style: italic;">EE_RMLUI_USE_BUNDLED_FREETYPE=ON</span> is an escape hatch<br />
if the prebuilt's config flags ever diverge from what RmlUi expects.</li>
<li><span style="font-weight: bold;">Texture upload</span> — <span style="font-style: italic;">LoadTexture</span> routes to <span style="font-style: italic;">Image.Import</span>, accepting<br />
every format Esenthel's asset pipeline supports (BMP/PNG/JPG/JXL/WEBP/<br />
AVIF/HEIF/TGA/TIF/DDS/PSD/ICO/HDR). <span style="font-style: italic;">GenerateTexture</span> uploads raw<br />
RGBA8 via a soft image, lock/write, row-copy, unlock, and GPU upload<br />
(used for the font atlas and procedural decorator textures).</li>
<li><span style="font-weight: bold;">Dual-mode input pump</span> — <span style="font-style: italic;">EE::RmlUi::PumpInput(ctx)</span> polls<br />
mouse position, mouse buttons, and wheel every frame, walks all<br />
256 <span style="font-style: italic;">KB_KEY</span> values firing key down/up events on edge changes, and<br />
pumps text characters out of <span style="font-style: italic;">Kb.k.c</span>. When the focused RmlUi element<br />
is an input field or textarea the key queue is drained so fast typing<br />
never drops characters; otherwise the head of the queue is peeked<br />
non-destructively so the built-in Gui still sees the same keys on the<br />
same frame.</li>
<li><span style="font-weight: bold;">Opt-in via CMake</span> — <span style="font-style: italic;">option(EE_RMLUI "... " ON)</span> in the root<br />
<span style="font-style: italic;">CMakeLists.txt</span>, default ON on Linux x64 + Windows x64, fatal on iOS<br />
/ Android (deferred until those platforms are tested). With<br />
<span style="font-style: italic;">EE_RMLUI=OFF</span> every integration TU is empty, the<br />
<span style="font-style: italic;">rmlui_core</span> / <span style="font-style: italic;">rmlui_debugger</span> targets are not added to<br />
the build graph, and the tutorial targets are skipped in<br />
<span style="font-style: italic;">Tutorials/CMakeLists.txt</span>.</li>
<li><span style="font-weight: bold;">Built from source</span> — RmlUi v6.2 is vendored at<br />
<span style="font-style: italic;">ThirdPartyLibs/RmlUi/lib/</span> and compiled via subdirectory inclusion so<br />
<span style="font-style: italic;">rmlui_core</span> + <span style="font-style: italic;">rmlui_debugger</span> become Engine interface link deps.<br />
No prebuilt library like the other third-parties, because upstream<br />
ships clean CMake and the library is small. ABI-visible flags are<br />
forced onto both targets so they match Engine's ABI.</li>
<li><span style="font-weight: bold;">Debugger overlay</span> — <span style="font-style: italic;">rmlui_debugger</span> is linked automatically when<br />
<span style="font-style: italic;">EE_RMLUI=ON</span>. Call <span style="font-style: italic;">Rml::Debugger::Initialise(ctx)</span> once, then<br />
<span style="font-style: italic;">Rml::Debugger::SetVisible(true)</span> (or bind to a key) to pop up the live<br />
inspector — Event Log, Element Info, Outlines, Data Models tabs.</li>
<li><span style="font-weight: bold;">Thin facade</span> — <span style="font-style: italic;">Engine/H/Gui/RmlUi.h</span> exposes only the<br />
<span style="font-style: italic;">EE::RmlUi::</span> lifecycle and helper functions. It forward-declares<br />
<span style="font-style: italic;">Rml::Context</span>, <span style="font-style: italic;">ElementDocument</span>, <span style="font-style: italic;">RenderInterface</span>, and<br />
<span style="font-style: italic;">SystemInterface</span>, so app translation units that just drive lifecycle<br />
do not pay to parse all of <span style="font-style: italic;">&amp;lt;RmlUi/Core.h&amp;gt;</span>. App translation units<br />
that want event listeners, DOM mutation, or typed element access can<br />
include <span style="font-style: italic;">&amp;lt;RmlUi/Core.h&amp;gt;</span> themselves.<br />
</li></ul>
<br />
<span style="font-weight: bold;">Non-invasive integration</span> — zero changes to <span style="font-style: italic;">Engine/Source/Gui/Gui.cpp</span>,<br />
<span style="font-style: italic;">Gui.draw</span>, <span style="font-style: italic;">Gui.update</span>, no new state in <span style="font-style: italic;">DisplayClass</span>, no new<br />
alpha mode, no new shaders. The pre-existing <span style="font-style: italic;">VI</span> + <span style="font-style: italic;">D</span> 2D path is the<br />
only rendering surface used; the existing 2D shaders are the only ones<br />
touched. The tutorial harness (<span style="font-style: italic;">Tutorials/TutorialAuto.{h,cpp}</span>,<br />
<span style="font-style: italic;">Tutorials/stdafx.h</span>) is unchanged. <span style="font-style: italic;">EE_RMLUI=OFF</span> builds are<br />
pixel-identical to pre-integration builds (verified against<br />
<span style="font-style: italic;">Tutorial_05_Bars</span>, <span style="font-style: italic;">Tutorial_04_BatchedDrawing</span>,<br />
<span style="font-style: italic;">Tutorial_12_RenderToTexture</span>).<br />
<br />
<span style="font-weight: bold;">RCSS gotcha</span> — RmlUi's transition/animation parser recognises tween<br />
names as defined in <span style="font-style: italic;">Source/Core/PropertyParserAnimation.cpp</span>. Unlike<br />
standard CSS, it does <span style="font-weight: bold;">not</span> accept bare <span style="font-style: italic;">linear</span> or the CSS keyword<br />
<span style="font-style: italic;">ease</span>; use <span style="font-style: italic;">linear-in-out</span> and <span style="font-style: italic;">cubic-in-out</span> (or any of the<br />
supported <span style="font-style: italic;">{back,bounce,circular,cubic,elastic,exponential,linear,quadratic,quartic,quintic&#8203;,sine}-{in,out,in-out}</span><br />
variants). Multi-transitions are comma-separated.<br />
<br />
<span style="font-weight: bold;">Deferred work:</span><br />
<ul>
<li>Custom <span style="font-style: italic;">Rml::FontEngineInterface</span> on top of Esenthel's <span style="font-style: italic;">Font</span> class, so<br />
<span style="font-style: italic;">.pak</span>-packaged fonts could feed RmlUi. Upstream's default FreeType<br />
engine is sufficient for now; TTF files ship alongside the tutorials.</li>
<li><span style="font-style: italic;">Rml::FileInterface</span> on <span style="font-style: italic;">EE::File</span> — would let RmlUi resolve<br />
<span style="font-style: italic;">.rml</span> / <span style="font-style: italic;">.rcss</span> / texture paths through engine <span style="font-style: italic;">.pak</span> archives. The<br />
stdio <span style="font-style: italic;">fopen</span> default is used currently; tutorials ship plain files<br />
under <span style="font-style: italic;">Tutorials/Data/RmlUi/</span>.</li>
<li>Data Binding (MVC layer via <span style="font-style: italic;">Rml::DataModelConstructor</span>) — supported by<br />
the linked <span style="font-style: italic;">rmlui_core</span> but not yet demonstrated by a tutorial.</li>
<li>Lua bindings (<span style="font-style: italic;">rmlui_lua</span>) — disabled in the wrapper (<span style="font-style: italic;">RMLUI_LUA_BINDINGS=OFF</span>);<br />
enabling would require wiring an Esenthel Lua runtime into the engine<br />
first.</li>
<li>Windows smoke sweep via <span style="font-style: italic;">run_smoke_test.ps1</span>. Linux is covered<br />
(all five tutorials + three regression targets pass).<br />
</li></ul>
<br />
Available in this repo:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Improved Destruction]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11814</link>
			<pubDate>Mon, 20 Apr 2026 02:21:50 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11814</guid>
			<description><![CDATA[<span style="font-weight: bold;">Fracture Destruction System</span> (improve-destruction-v3 branch)<br />
<br />
A production-grade destructible-object system located at Engine/Source/Physics/Fracture*.cpp plus Engine/Source/Game/Objects/Fractured.cpp.<br />
<br />
The existing DestructMesh / Game::Destructible implementation from Tutorial 23 is left untouched. The new system ships alongside it under new type names.<br />
<br />
<span style="font-weight: bold;">Core idea:</span><br />
<br />
A <span style="font-weight: bold;">FractureMesh asset</span> pre-bakes a mesh into chunks, a cluster hierarchy, and a connection graph that records which chunks share faces, with per-edge breaking strain.<br />
<br />
A <span style="font-weight: bold;">Game::Fractured runtime object</span> owns the per-instance state. At runtime, it exists as either a single intact-cluster Actor (cluster proxy) or an array of per-chunk Actors connected by breakable PhysX joints.<br />
<br />
Damage accumulates per node, or on the cluster as a whole. Once the threshold is crossed, the object transitions from <span style="font-weight: bold;">INTACT</span> to <span style="font-weight: bold;">SHATTERED</span> by destroying the proxy and spawning per-chunk actors.<br />
<br />
Mass is force-set from bake-time chunk volume so the convex-hull proxy’s mass overestimate does not cause a visible pop at the moment of breakup.<br />
<br />
Strain then propagates along the connection graph, so a hit on one chunk weakens its neighbours. This reproduces “spider cracks” style breakage without requiring a full spatial-field solver.<br />
<br />
<a href="https://postimg.cc/34G95frD" target="_blank"><img src="https://i.postimg.cc/j5g1fBGv/Screenshot-from-2026-04-19-22-14-24.png" border="0" alt="[Image: Screenshot-from-2026-04-19-22-14-24.png]" /></a><br />
<br />
<a href="https://postimg.cc/SnZMZL4n" target="_blank"><img src="https://i.postimg.cc/Jh85zKd5/Screenshot-from-2026-04-19-21-51-13.png" border="0" alt="[Image: Screenshot-from-2026-04-19-21-51-13.png]" /></a><br />
<br />
<span style="font-weight: bold;">Shipped features:</span><br />
<ul>
[]<span style="font-weight: bold;">Five bake patterns</span> (FRACTURE_PATTERN_) —<br />
PLANAR_SLICES uses random plane cuts and mirrors the classic DestructMesh algorithm.<br />
UNIFORM_VORONOI and CLUSTERED_VORONOI use iterative SplitMeshSolid with perpendicular-bisector planes; the older CSG-based spike was dropped because it was numerically unstable.<br />
RADIAL produces true pie-slice wedges radiating from an impact_point through the wall’s thickness axis.<br />
BRICK uses a dedicated axis-aligned 6-plane-per-cell path (BakeBrickChunks) with <span style="font-weight: bold;">shared-boundary jitter</span>: adjacent bricks read the same boundary entry for their shared seam, so jitter never produces gaps between neighbours.</li>
<li><span style="font-weight: bold;">Cluster proxy</span> (FractureSettings::build_cluster_proxy) — the bake computes a single convex hull over all chunks and stores it on the root Cluster, plus the sum of per-chunk volumes as mass_sum. At spawn, the Fractured object creates one kinematic Actor using this proxy instead of N per-chunk actors. 500 intact crates cost 500 bodies, not 10,000.</li>
<li><span style="font-weight: bold;">Localised release cascade</span> — when damage crosses cluster_break_threshold, Fractured::releaseRootCluster(impact_pos, velocity_delta) destroys the proxy and spawns per-chunk actors, inheriting the proxy’s linear and angular velocity and force-setting each chunk’s mass to the bake-time volume estimate. A <span style="font-weight: bold;">distance-falloff</span> kick is then applied only to chunks within 1.5 m of the impact (mass-scaled, velocity-capped at 4 m/s), so a hit near the bottom of a wall does not catapult distant chunks and make anchored sections rubber-band through joints.</li>
<li><span style="font-weight: bold;">Deferred release</span> — applyPointDamage is called from inside PhysX’s onContact callback; destroying and creating actors there corrupts the scene and crashes the next PxRigidActor::is&lt;&gt;() cast. Instead, threshold crossing sets a pending flag plus the impact position and velocity, and the next update() tick flushes the release outside any callback.</li>
<li><span style="font-weight: bold;">Connection-graph strain propagation</span> — Fractured::propagateStrain walks live_edges each frame when damage_dirty. Damage above the weakest-link elastic limit (0.9 * break_threshold) bleeds across edges; smaller hits dissipate via damage_decay_tau without creeping. Broken joints are removed by BFS; any connected component that does not reach an anchor becomes dynamic.</li>
<li><span style="font-weight: bold;">Bake-time anchors</span> — FractureSettings::anchor_volume marks chunks whose centroid is inside the box with FRAC_FLAG_ANCHORED_AT_BAKE. They spawn kinematic, or the cluster proxy spawns kinematic if any member is anchored. Runtime Fracture::setAnchor(obj, chunk_index, anchored) can flip the anchor state live.</li>
<li><span style="font-weight: bold;">Damage API</span> —<br />
Fracture::applyRadialDamage(pos, radius, strength, impulse, type)<br />
applyContactDamage(a, b, contacts, n, type)<br />
breakNode(obj, node_index, dir, type)<br />
setAnchor(...)<br />
<br />
All accept a DAMAGE_TYPE enum (KINETIC, EXPLOSIVE, PIERCING, THERMAL, CUSTOM) scaled by a global damage_type_scale[5] table, so game code can do things like make grenades 1.5x stronger against concrete with a single tweak.</li>
<li><span style="font-weight: bold;">Multi-subscriber BreakEventCallback</span> — Fracture::addBreakEventCallback is void-return and never consumes the event. addBreakEventCallbackB is bool-return; returning true consumes the event and stops the chain. Legacy setBreakEventCallback is preserved as a single-slot wrapper.</li>
<li><span style="font-weight: bold;">Debris pool</span> — Fracture::setVfxPoolCap, setSfxVoicesCap, debrisActiveCount, plus low-energy sleep and debris-pool caps in updateDebrisHeuristics. Chunks at rest for debris_sleep_timeout seconds are put to sleep so PhysX stops simulating them.</li>
<li><span style="font-weight: bold;">Save / Load</span> — FractureMesh::save(path) and load(path) round-trip the whole asset (chunks, cooked PhysX convex hulls, cluster, and connection graph) through a versioned CC4('F','R','A','C') binary format.<br />
FractureMesh::adjustStorage(universal, physx, bullet) matches the PhysBody contract and allows the asset to be reloaded on a different backend. A cache (Cache&lt;FractureMesh&gt; FractureMeshes("Fracture Mesh")) is preserved so UID-addressed baked assets can live inside pak files like any other engine asset.<br />
<br />
[]<span style="font-weight: bold;">Instance sharing</span> — many Game::Fractured instances can point at the same FractureMesh. Geometry and cooked hulls are shared; only runtime state (damage, actor transforms, live joints) is per-instance.<br />
</li></ul>
<br />
<span style="font-weight: bold;">Non-invasive integration:</span><br />
<br />
Zero changes to DestructMesh, Game::Destructible, or Tutorial 23.<br />
<br />
PhysX’s Physics.reportContact is <span style="font-weight: bold;">not</span> hijacked. The host app registers its own contact callback and can optionally call Fracture::applyContactDamage from inside it.<br />
<br />
Contact damage is PhysX-only, since Bullet does not deliver contact events in this engine. Radial damage works on both backends.<br />
<br />
<span style="font-weight: bold;">Deferred work:</span><br />
<ul>
<li>Multi-level hierarchical fracture (more than 2 levels deep, with per-level break thresholds and cluster-tree view-level LOD). The data model already supports it via Cluster::parent and Cluster::children; the runtime currently walks one level.</li>
<li>Runtime on-the-fly fracture (CSG against an unbaked mesh). Pre-bake only is the shipping path.</li>
<li>View-distance simulation LOD (far objects deferred until the player enters a bubble). The cluster proxy is the foundation; bubble activation logic is planned as a later follow-up.</li>
<li>Multi-platform cooked-hull round-trip. Saved files are currently PhysX-specific; cross-platform support needs adjustStorage(universal=true, ...) before save.</li>
<li>Titan Editor UI for anchor-volume painting, per-chunk material picking, and interactive fracture preview. This branch is intentionally code-only.</li>
<li>Auto-dispatch BreakPreset VFX/SFX from a material tag, so Tutorial 31’s 40-line OnBreak puff could collapse to 5 lines. The scaffolding is already in place (multi-subscriber callbacks, damage types), but the preset registry is deferred.<br />
</li></ul>
<br />
in this fork:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></description>
			<content:encoded><![CDATA[<span style="font-weight: bold;">Fracture Destruction System</span> (improve-destruction-v3 branch)<br />
<br />
A production-grade destructible-object system located at Engine/Source/Physics/Fracture*.cpp plus Engine/Source/Game/Objects/Fractured.cpp.<br />
<br />
The existing DestructMesh / Game::Destructible implementation from Tutorial 23 is left untouched. The new system ships alongside it under new type names.<br />
<br />
<span style="font-weight: bold;">Core idea:</span><br />
<br />
A <span style="font-weight: bold;">FractureMesh asset</span> pre-bakes a mesh into chunks, a cluster hierarchy, and a connection graph that records which chunks share faces, with per-edge breaking strain.<br />
<br />
A <span style="font-weight: bold;">Game::Fractured runtime object</span> owns the per-instance state. At runtime, it exists as either a single intact-cluster Actor (cluster proxy) or an array of per-chunk Actors connected by breakable PhysX joints.<br />
<br />
Damage accumulates per node, or on the cluster as a whole. Once the threshold is crossed, the object transitions from <span style="font-weight: bold;">INTACT</span> to <span style="font-weight: bold;">SHATTERED</span> by destroying the proxy and spawning per-chunk actors.<br />
<br />
Mass is force-set from bake-time chunk volume so the convex-hull proxy’s mass overestimate does not cause a visible pop at the moment of breakup.<br />
<br />
Strain then propagates along the connection graph, so a hit on one chunk weakens its neighbours. This reproduces “spider cracks” style breakage without requiring a full spatial-field solver.<br />
<br />
<a href="https://postimg.cc/34G95frD" target="_blank"><img src="https://i.postimg.cc/j5g1fBGv/Screenshot-from-2026-04-19-22-14-24.png" border="0" alt="[Image: Screenshot-from-2026-04-19-22-14-24.png]" /></a><br />
<br />
<a href="https://postimg.cc/SnZMZL4n" target="_blank"><img src="https://i.postimg.cc/Jh85zKd5/Screenshot-from-2026-04-19-21-51-13.png" border="0" alt="[Image: Screenshot-from-2026-04-19-21-51-13.png]" /></a><br />
<br />
<span style="font-weight: bold;">Shipped features:</span><br />
<ul>
[]<span style="font-weight: bold;">Five bake patterns</span> (FRACTURE_PATTERN_) —<br />
PLANAR_SLICES uses random plane cuts and mirrors the classic DestructMesh algorithm.<br />
UNIFORM_VORONOI and CLUSTERED_VORONOI use iterative SplitMeshSolid with perpendicular-bisector planes; the older CSG-based spike was dropped because it was numerically unstable.<br />
RADIAL produces true pie-slice wedges radiating from an impact_point through the wall’s thickness axis.<br />
BRICK uses a dedicated axis-aligned 6-plane-per-cell path (BakeBrickChunks) with <span style="font-weight: bold;">shared-boundary jitter</span>: adjacent bricks read the same boundary entry for their shared seam, so jitter never produces gaps between neighbours.</li>
<li><span style="font-weight: bold;">Cluster proxy</span> (FractureSettings::build_cluster_proxy) — the bake computes a single convex hull over all chunks and stores it on the root Cluster, plus the sum of per-chunk volumes as mass_sum. At spawn, the Fractured object creates one kinematic Actor using this proxy instead of N per-chunk actors. 500 intact crates cost 500 bodies, not 10,000.</li>
<li><span style="font-weight: bold;">Localised release cascade</span> — when damage crosses cluster_break_threshold, Fractured::releaseRootCluster(impact_pos, velocity_delta) destroys the proxy and spawns per-chunk actors, inheriting the proxy’s linear and angular velocity and force-setting each chunk’s mass to the bake-time volume estimate. A <span style="font-weight: bold;">distance-falloff</span> kick is then applied only to chunks within 1.5 m of the impact (mass-scaled, velocity-capped at 4 m/s), so a hit near the bottom of a wall does not catapult distant chunks and make anchored sections rubber-band through joints.</li>
<li><span style="font-weight: bold;">Deferred release</span> — applyPointDamage is called from inside PhysX’s onContact callback; destroying and creating actors there corrupts the scene and crashes the next PxRigidActor::is&lt;&gt;() cast. Instead, threshold crossing sets a pending flag plus the impact position and velocity, and the next update() tick flushes the release outside any callback.</li>
<li><span style="font-weight: bold;">Connection-graph strain propagation</span> — Fractured::propagateStrain walks live_edges each frame when damage_dirty. Damage above the weakest-link elastic limit (0.9 * break_threshold) bleeds across edges; smaller hits dissipate via damage_decay_tau without creeping. Broken joints are removed by BFS; any connected component that does not reach an anchor becomes dynamic.</li>
<li><span style="font-weight: bold;">Bake-time anchors</span> — FractureSettings::anchor_volume marks chunks whose centroid is inside the box with FRAC_FLAG_ANCHORED_AT_BAKE. They spawn kinematic, or the cluster proxy spawns kinematic if any member is anchored. Runtime Fracture::setAnchor(obj, chunk_index, anchored) can flip the anchor state live.</li>
<li><span style="font-weight: bold;">Damage API</span> —<br />
Fracture::applyRadialDamage(pos, radius, strength, impulse, type)<br />
applyContactDamage(a, b, contacts, n, type)<br />
breakNode(obj, node_index, dir, type)<br />
setAnchor(...)<br />
<br />
All accept a DAMAGE_TYPE enum (KINETIC, EXPLOSIVE, PIERCING, THERMAL, CUSTOM) scaled by a global damage_type_scale[5] table, so game code can do things like make grenades 1.5x stronger against concrete with a single tweak.</li>
<li><span style="font-weight: bold;">Multi-subscriber BreakEventCallback</span> — Fracture::addBreakEventCallback is void-return and never consumes the event. addBreakEventCallbackB is bool-return; returning true consumes the event and stops the chain. Legacy setBreakEventCallback is preserved as a single-slot wrapper.</li>
<li><span style="font-weight: bold;">Debris pool</span> — Fracture::setVfxPoolCap, setSfxVoicesCap, debrisActiveCount, plus low-energy sleep and debris-pool caps in updateDebrisHeuristics. Chunks at rest for debris_sleep_timeout seconds are put to sleep so PhysX stops simulating them.</li>
<li><span style="font-weight: bold;">Save / Load</span> — FractureMesh::save(path) and load(path) round-trip the whole asset (chunks, cooked PhysX convex hulls, cluster, and connection graph) through a versioned CC4('F','R','A','C') binary format.<br />
FractureMesh::adjustStorage(universal, physx, bullet) matches the PhysBody contract and allows the asset to be reloaded on a different backend. A cache (Cache&lt;FractureMesh&gt; FractureMeshes("Fracture Mesh")) is preserved so UID-addressed baked assets can live inside pak files like any other engine asset.<br />
<br />
[]<span style="font-weight: bold;">Instance sharing</span> — many Game::Fractured instances can point at the same FractureMesh. Geometry and cooked hulls are shared; only runtime state (damage, actor transforms, live joints) is per-instance.<br />
</li></ul>
<br />
<span style="font-weight: bold;">Non-invasive integration:</span><br />
<br />
Zero changes to DestructMesh, Game::Destructible, or Tutorial 23.<br />
<br />
PhysX’s Physics.reportContact is <span style="font-weight: bold;">not</span> hijacked. The host app registers its own contact callback and can optionally call Fracture::applyContactDamage from inside it.<br />
<br />
Contact damage is PhysX-only, since Bullet does not deliver contact events in this engine. Radial damage works on both backends.<br />
<br />
<span style="font-weight: bold;">Deferred work:</span><br />
<ul>
<li>Multi-level hierarchical fracture (more than 2 levels deep, with per-level break thresholds and cluster-tree view-level LOD). The data model already supports it via Cluster::parent and Cluster::children; the runtime currently walks one level.</li>
<li>Runtime on-the-fly fracture (CSG against an unbaked mesh). Pre-bake only is the shipping path.</li>
<li>View-distance simulation LOD (far objects deferred until the player enters a bubble). The cluster proxy is the foundation; bubble activation logic is planned as a later follow-up.</li>
<li>Multi-platform cooked-hull round-trip. Saved files are currently PhysX-specific; cross-platform support needs adjustStorage(universal=true, ...) before save.</li>
<li>Titan Editor UI for anchor-volume painting, per-chunk material picking, and interactive fracture preview. This branch is intentionally code-only.</li>
<li>Auto-dispatch BreakPreset VFX/SFX from a material tag, so Tutorial 31’s 40-line OnBreak puff could collapse to 5 lines. The scaffolding is already in place (multi-subscriber callbacks, damage types), but the preset registry is deferred.<br />
</li></ul>
<br />
in this fork:<br />
<a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Cloth Physics]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11813</link>
			<pubDate>Sat, 18 Apr 2026 12:56:10 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11813</guid>
			<description><![CDATA[Cloth Physics (XPBD)<br />
A first-party CPU cloth solver built into the engine at Engine/Source/Physics/Cloth.{h,cpp} + Engine/Source/Physics/Cloth Solver.{h,cpp}, sitting behind the existing Cloth / ClothMesh public API. Replaces NVIDIA's deprecated PhysX 3.x cloth (removed in PhysX 4+) with an XPBD (Extended Position-Based Dynamics) implementation — stable, deterministic, tunable, and owned entirely by the engine.<br />
<br />
Core idea — each cloth is a set of mass-weighted particles connected by constraints. Per simulate(dt) the solver substeps (default 8) Jakobsen-style: semi-implicit-Euler predict → XPBD Gauss-Seidel constraint solve (Δλ = (-C - α̃λ)/(w_i+w_j+α̃), α̃ = compliance/dt²) → collision projection against author-placed balls/capsules → derive velocity from position delta. Pinned particles (inv_mass == 0) follow skeleton bones each sub-step via smooth prev↔cur lerp so fast character motion doesn't yank the cape.<br />
<br />
<a href="https://postimg.cc/RW665C3M" target="_blank"><img src="https://i.postimg.cc/GmzkDHds/Screenshot-from-2026-04-18-08-13-11.png" border="0" alt="[Image: Screenshot-from-2026-04-18-08-13-11.png]" /></a><br />
<br />
<a href="https://postimg.cc/1fYnjVzg" target="_blank"><img src="https://i.postimg.cc/x19vqKDP/Screenshot-from-2026-04-18-08-15-29.png" border="0" alt="[Image: Screenshot-from-2026-04-18-08-15-29.png]" /></a><br />
<br />
In this repo: <a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a><br />
See README.md for more info.]]></description>
			<content:encoded><![CDATA[Cloth Physics (XPBD)<br />
A first-party CPU cloth solver built into the engine at Engine/Source/Physics/Cloth.{h,cpp} + Engine/Source/Physics/Cloth Solver.{h,cpp}, sitting behind the existing Cloth / ClothMesh public API. Replaces NVIDIA's deprecated PhysX 3.x cloth (removed in PhysX 4+) with an XPBD (Extended Position-Based Dynamics) implementation — stable, deterministic, tunable, and owned entirely by the engine.<br />
<br />
Core idea — each cloth is a set of mass-weighted particles connected by constraints. Per simulate(dt) the solver substeps (default 8) Jakobsen-style: semi-implicit-Euler predict → XPBD Gauss-Seidel constraint solve (Δλ = (-C - α̃λ)/(w_i+w_j+α̃), α̃ = compliance/dt²) → collision projection against author-placed balls/capsules → derive velocity from position delta. Pinned particles (inv_mass == 0) follow skeleton bones each sub-step via smooth prev↔cur lerp so fast character motion doesn't yank the cape.<br />
<br />
<a href="https://postimg.cc/RW665C3M" target="_blank"><img src="https://i.postimg.cc/GmzkDHds/Screenshot-from-2026-04-18-08-13-11.png" border="0" alt="[Image: Screenshot-from-2026-04-18-08-13-11.png]" /></a><br />
<br />
<a href="https://postimg.cc/1fYnjVzg" target="_blank"><img src="https://i.postimg.cc/x19vqKDP/Screenshot-from-2026-04-18-08-15-29.png" border="0" alt="[Image: Screenshot-from-2026-04-18-08-15-29.png]" /></a><br />
<br />
In this repo: <a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a><br />
See README.md for more info.]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Behavior Trees, Animation Graph, IK]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11812</link>
			<pubDate>Sat, 18 Apr 2026 00:06:09 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11812</guid>
			<description><![CDATA[Behavior Trees (Bt*, animation-state-machine branch)<br />
A data-oriented behavior tree framework for AI decision-making, built into the engine at Engine/H/Game/AI/BehaviorTree.h + Engine/Source/Game/AI/BehaviorTree.cpp. Designed for attachment to Game::Chr subclasses: build a tree of BtNode objects once in Chr::create(), call bt.tick(Time.d()) from Chr::update() each frame.<br />
<br />
<a href="https://postimg.cc/ThH741M7" target="_blank"><img src="https://i.postimg.cc/TYTzh5k6/Screenshot-from-2026-04-17-19-48-04.png" border="0" alt="[Image: Screenshot-from-2026-04-17-19-48-04.png]" /></a><br />
<br />
<br />
Animation Graph (Ag*) and IK (animation-state-machine branch)<br />
A production-grade composable pose-graph animation system, built into the engine at Engine/H/Animation/AnimGraph.h + Engine/Source/Animation/AnimGraph.cpp. Sits on top of the existing AnimatedSkeleton::animate() primitives without modifying them, stylistically parallels the BehaviorTree framework on the same branch (Bt* prefix, namespace Game{}, non-virtual update() wrapper + protected doUpdate() dispatch, tree-walking virtuals for viz).<br />
<br />
<a href="https://postimg.cc/JthYxJgf" target="_blank"><img src="https://i.postimg.cc/QNgLXJqd/Screenshot-from-2026-04-17-19-49-22.png" border="0" alt="[Image: Screenshot-from-2026-04-17-19-49-22.png]" /></a><br />
<br />
IK On:<br />
<a href="https://postimg.cc/4n32Ptqk" target="_blank"><img src="https://i.postimg.cc/xjGr03PC/Screenshot-from-2026-04-17-16-02-10.png" border="0" alt="[Image: Screenshot-from-2026-04-17-16-02-10.png]" /></a><br />
<br />
IK Off:<br />
<a href="https://postimg.cc/XXv1LFmM" target="_blank"><img src="https://i.postimg.cc/y6hqVyTN/Screenshot-from-2026-04-17-16-02-24.png" border="0" alt="[Image: Screenshot-from-2026-04-17-16-02-24.png]" /></a><br />
<br />
In this repo: <a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a><br />
See the README.md for more info.]]></description>
			<content:encoded><![CDATA[Behavior Trees (Bt*, animation-state-machine branch)<br />
A data-oriented behavior tree framework for AI decision-making, built into the engine at Engine/H/Game/AI/BehaviorTree.h + Engine/Source/Game/AI/BehaviorTree.cpp. Designed for attachment to Game::Chr subclasses: build a tree of BtNode objects once in Chr::create(), call bt.tick(Time.d()) from Chr::update() each frame.<br />
<br />
<a href="https://postimg.cc/ThH741M7" target="_blank"><img src="https://i.postimg.cc/TYTzh5k6/Screenshot-from-2026-04-17-19-48-04.png" border="0" alt="[Image: Screenshot-from-2026-04-17-19-48-04.png]" /></a><br />
<br />
<br />
Animation Graph (Ag*) and IK (animation-state-machine branch)<br />
A production-grade composable pose-graph animation system, built into the engine at Engine/H/Animation/AnimGraph.h + Engine/Source/Animation/AnimGraph.cpp. Sits on top of the existing AnimatedSkeleton::animate() primitives without modifying them, stylistically parallels the BehaviorTree framework on the same branch (Bt* prefix, namespace Game{}, non-virtual update() wrapper + protected doUpdate() dispatch, tree-walking virtuals for viz).<br />
<br />
<a href="https://postimg.cc/JthYxJgf" target="_blank"><img src="https://i.postimg.cc/QNgLXJqd/Screenshot-from-2026-04-17-19-49-22.png" border="0" alt="[Image: Screenshot-from-2026-04-17-19-49-22.png]" /></a><br />
<br />
IK On:<br />
<a href="https://postimg.cc/4n32Ptqk" target="_blank"><img src="https://i.postimg.cc/xjGr03PC/Screenshot-from-2026-04-17-16-02-10.png" border="0" alt="[Image: Screenshot-from-2026-04-17-16-02-10.png]" /></a><br />
<br />
IK Off:<br />
<a href="https://postimg.cc/XXv1LFmM" target="_blank"><img src="https://i.postimg.cc/y6hqVyTN/Screenshot-from-2026-04-17-16-02-24.png" border="0" alt="[Image: Screenshot-from-2026-04-17-16-02-24.png]" /></a><br />
<br />
In this repo: <a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a><br />
See the README.md for more info.]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Frame profiler and Input Action System]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11811</link>
			<pubDate>Fri, 17 Apr 2026 01:51:30 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11811</guid>
			<description><![CDATA[In-Engine Frame Profiler<br />
A lightweight CPU scope profiler with an on-screen stats overlay, built into the engine at Engine/H/Misc/Profiler.h + Engine/Source/Misc/Profiler.cpp. Works on all backends (DX11, DX12, Vulkan, GL). Zero overhead when disabled (~1 ns per PROFILE_SCOPE site -- one predictable branch on a cached bool).<br />
<br />
Tutorial_04_FrameProfiler (Tutorials/Source/04 - Graphics/Frame Profiler.cpp<br />
<br />
<a href="https://postimg.cc/216yymw6" target="_blank"><img src="https://i.postimg.cc/y8hZf83F/Screenshot-from-2026-04-15-12-49-24.png" border="0" alt="[Image: Screenshot-from-2026-04-15-12-49-24.png]" /></a><br />
<br />
<a href="https://postimg.cc/RWFhh9R0" target="_blank"><img src="https://i.postimg.cc/rwGrhwR5/Screenshot-from-2026-04-15-12-49-34.png" border="0" alt="[Image: Screenshot-from-2026-04-15-12-49-34.png]" /></a><br />
<br />
<br />
Input Action System (rebindable, savable, stacked)<br />
An Unreal/Unity-style action abstraction over Esenthel's raw polling API, built into the engine at Engine/H/Input/Input Action.h + Engine/Source/Input/Input Action.cpp. 100% additive -- no changes to Kb/Ms/Joypads/Touches. Works on all backends and platforms.<br />
<br />
Tutorial_05_InputRebinding (Tutorials/Source/05 - Gui/20 - Input Rebinding.cpp)<br />
<br />
<a href="https://postimg.cc/xXC88YRf" target="_blank"><img src="https://i.postimg.cc/K87M983M/Screenshot-from-2026-04-16-21-41-02.png" border="0" alt="[Image: Screenshot-from-2026-04-16-21-41-02.png]" /></a><br />
<br />
In this repo: <a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></description>
			<content:encoded><![CDATA[In-Engine Frame Profiler<br />
A lightweight CPU scope profiler with an on-screen stats overlay, built into the engine at Engine/H/Misc/Profiler.h + Engine/Source/Misc/Profiler.cpp. Works on all backends (DX11, DX12, Vulkan, GL). Zero overhead when disabled (~1 ns per PROFILE_SCOPE site -- one predictable branch on a cached bool).<br />
<br />
Tutorial_04_FrameProfiler (Tutorials/Source/04 - Graphics/Frame Profiler.cpp<br />
<br />
<a href="https://postimg.cc/216yymw6" target="_blank"><img src="https://i.postimg.cc/y8hZf83F/Screenshot-from-2026-04-15-12-49-24.png" border="0" alt="[Image: Screenshot-from-2026-04-15-12-49-24.png]" /></a><br />
<br />
<a href="https://postimg.cc/RWFhh9R0" target="_blank"><img src="https://i.postimg.cc/rwGrhwR5/Screenshot-from-2026-04-15-12-49-34.png" border="0" alt="[Image: Screenshot-from-2026-04-15-12-49-34.png]" /></a><br />
<br />
<br />
Input Action System (rebindable, savable, stacked)<br />
An Unreal/Unity-style action abstraction over Esenthel's raw polling API, built into the engine at Engine/H/Input/Input Action.h + Engine/Source/Input/Input Action.cpp. 100% additive -- no changes to Kb/Ms/Joypads/Touches. Works on all backends and platforms.<br />
<br />
Tutorial_05_InputRebinding (Tutorials/Source/05 - Gui/20 - Input Rebinding.cpp)<br />
<br />
<a href="https://postimg.cc/xXC88YRf" target="_blank"><img src="https://i.postimg.cc/K87M983M/Screenshot-from-2026-04-16-21-41-02.png" border="0" alt="[Image: Screenshot-from-2026-04-16-21-41-02.png]" /></a><br />
<br />
In this repo: <a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a>]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[DirectX 12 Ultimate rendering backend]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11810</link>
			<pubDate>Tue, 14 Apr 2026 12:12:36 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11810</guid>
			<description><![CDATA[In this repo: <a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a><br />
<br />
DX12 seems working nicely on Windows 11 nVidia A2000 GPU.<br />
<br />
Lines added (by Claude Code) 11,566<br />
<br />
<a href="https://postimages.org/" target="_blank"><img src="https://i.postimg.cc/jSgX3ZwX/dx12meshshaders.png" border="0" alt="[Image: dx12meshshaders.png]" /></a><br />
<br />
See README.md for info on the added DX12 specific tutorials.]]></description>
			<content:encoded><![CDATA[In this repo: <a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a><br />
<br />
DX12 seems working nicely on Windows 11 nVidia A2000 GPU.<br />
<br />
Lines added (by Claude Code) 11,566<br />
<br />
<a href="https://postimages.org/" target="_blank"><img src="https://i.postimg.cc/jSgX3ZwX/dx12meshshaders.png" border="0" alt="[Image: dx12meshshaders.png]" /></a><br />
<br />
See README.md for info on the added DX12 specific tutorials.]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Vulkan rendering backend with hardware ray tracing]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11809</link>
			<pubDate>Thu, 09 Apr 2026 22:18:40 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11809</guid>
			<description><![CDATA[In this repo: <a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a><br />
<br />
Claude Code did ~97% of the work. Added some Vulkan specific tutorials:<br />
<br />
<br />
<a href="https://postimg.cc/jwvsQt8K" target="_blank"><img src="https://i.postimg.cc/PJnPHqYL/raytracing.png" border="0" alt="[Image: raytracing.png]" /></a><br />
<br />
Tutorial_17_RayTracing — Hardware ray tracing<br />
The marquee Vulkan-vs-OpenGL feature. Renders a small scene (ground plane + 3 colored boxes) entirely via vkCmdTraceRaysKHR running on the GPU's RT cores — no rasterization at all. Includes:<br />
<br />
BLAS (bottom-level acceleration structure) built from raw vertex/index buffers<br />
TLAS (top-level) with one instance of the BLAS<br />
4-shader raytracing pipeline: raygen + primary miss + shadow miss + closest hit<br />
Shader binding table built per Vulkan spec alignment rules<br />
Hardware ray-traced primary rays + hardware ray-traced shadow rays from a directional light, with Lambert shading using flat geometric normals computed in the closest-hit shader via SSBO lookups into the vertex/index buffers<br />
OpenGL has no hardware RT path at all — the legacy GL RT extensions are NV-only and dead. The OpenGL build of this tutorial compiles cleanly but only renders the "Ray tracing: N/A on OpenGL" HUD line.<br />
<br />
GPU requirements: any GPU exposing VK_KHR_ray_tracing_pipeline, VK_KHR_acceleration_structure, and VK_KHR_deferred_host_operations. Verified working on AMD Radeon RX 6650 XT (RDNA2) under Mesa RADV; should work on any RTX 20-series+ NVIDIA, RDNA2+ AMD, or Arc Alchemist+ Intel GPU.<br />
<br />
<br />
<br />
<a href="https://postimg.cc/G8zcJdMb" target="_blank"><img src="https://i.postimg.cc/3NQdhwm0/async-compute.png" border="0" alt="[Image: async-compute.png]" /></a><br />
<br />
Tutorial_17_AsyncCompute — Particle sim on a dedicated compute queue<br />
Demonstrates Vulkan's async-compute feature: a 4096-particle gravity simulation runs on a separate compute queue (when the GPU exposes a queue family with VK_QUEUE_COMPUTE_BIT but NOT VK_QUEUE_GRAPHICS_BIT) while the engine's frame rendering happens in parallel on the graphics queue. OpenGL has no equivalent — it is strictly single-queue.<br />
<br />
The HUD reports whether the GPU has a dedicated compute family. The compute shader is hand-written GLSL (particle_update.comp), pre-compiled once to SPIR-V via glslangValidator and checked in as a C header (particle_update_spv.h) so the build needs no shader tooling installed.<br />
<br />
<br />
<br />
<br />
<a href="https://postimg.cc/yg71ZkT1" target="_blank"><img src="https://i.postimg.cc/1tqfbFnq/gputimer.png" border="0" alt="[Image: gputimer.png]" /></a><br />
<br />
Tutorial_17_Benchmark — Renderer throughput benchmark<br />
Renders 50 animated characters in a 10×5 grid with vsync disabled, an atmospheric sky, the FPS counter, and the active API name. On Vulkan it also shows a per-phase GPU time breakdown (Prepare / GBuffer / Light / Sky / Blend / Post) measured via real vkCmdWriteTimestamp query pools. Useful for measuring relative renderer throughput between OpenGL and Vulkan builds on the same hardware.<br />
<br />
The GPU timer API (Engine/H/Graphics/GpuTimer.h: GpuTimerEnable, GpuTimerMs, GPU_TIMER_PHASE) is exposed engine-wide so any tutorial can read per-phase GPU times. On DX11/GL it compiles to stubs that return 0.<br />
<br />
<br />
See README.md for more info.]]></description>
			<content:encoded><![CDATA[In this repo: <a href="https://github.com/DrewGilpin/EsenthelEngine" target="_blank">https://github.com/DrewGilpin/EsenthelEngine</a><br />
<br />
Claude Code did ~97% of the work. Added some Vulkan specific tutorials:<br />
<br />
<br />
<a href="https://postimg.cc/jwvsQt8K" target="_blank"><img src="https://i.postimg.cc/PJnPHqYL/raytracing.png" border="0" alt="[Image: raytracing.png]" /></a><br />
<br />
Tutorial_17_RayTracing — Hardware ray tracing<br />
The marquee Vulkan-vs-OpenGL feature. Renders a small scene (ground plane + 3 colored boxes) entirely via vkCmdTraceRaysKHR running on the GPU's RT cores — no rasterization at all. Includes:<br />
<br />
BLAS (bottom-level acceleration structure) built from raw vertex/index buffers<br />
TLAS (top-level) with one instance of the BLAS<br />
4-shader raytracing pipeline: raygen + primary miss + shadow miss + closest hit<br />
Shader binding table built per Vulkan spec alignment rules<br />
Hardware ray-traced primary rays + hardware ray-traced shadow rays from a directional light, with Lambert shading using flat geometric normals computed in the closest-hit shader via SSBO lookups into the vertex/index buffers<br />
OpenGL has no hardware RT path at all — the legacy GL RT extensions are NV-only and dead. The OpenGL build of this tutorial compiles cleanly but only renders the "Ray tracing: N/A on OpenGL" HUD line.<br />
<br />
GPU requirements: any GPU exposing VK_KHR_ray_tracing_pipeline, VK_KHR_acceleration_structure, and VK_KHR_deferred_host_operations. Verified working on AMD Radeon RX 6650 XT (RDNA2) under Mesa RADV; should work on any RTX 20-series+ NVIDIA, RDNA2+ AMD, or Arc Alchemist+ Intel GPU.<br />
<br />
<br />
<br />
<a href="https://postimg.cc/G8zcJdMb" target="_blank"><img src="https://i.postimg.cc/3NQdhwm0/async-compute.png" border="0" alt="[Image: async-compute.png]" /></a><br />
<br />
Tutorial_17_AsyncCompute — Particle sim on a dedicated compute queue<br />
Demonstrates Vulkan's async-compute feature: a 4096-particle gravity simulation runs on a separate compute queue (when the GPU exposes a queue family with VK_QUEUE_COMPUTE_BIT but NOT VK_QUEUE_GRAPHICS_BIT) while the engine's frame rendering happens in parallel on the graphics queue. OpenGL has no equivalent — it is strictly single-queue.<br />
<br />
The HUD reports whether the GPU has a dedicated compute family. The compute shader is hand-written GLSL (particle_update.comp), pre-compiled once to SPIR-V via glslangValidator and checked in as a C header (particle_update_spv.h) so the build needs no shader tooling installed.<br />
<br />
<br />
<br />
<br />
<a href="https://postimg.cc/yg71ZkT1" target="_blank"><img src="https://i.postimg.cc/1tqfbFnq/gputimer.png" border="0" alt="[Image: gputimer.png]" /></a><br />
<br />
Tutorial_17_Benchmark — Renderer throughput benchmark<br />
Renders 50 animated characters in a 10×5 grid with vsync disabled, an atmospheric sky, the FPS counter, and the active API name. On Vulkan it also shows a per-phase GPU time breakdown (Prepare / GBuffer / Light / Sky / Blend / Post) measured via real vkCmdWriteTimestamp query pools. Useful for measuring relative renderer throughput between OpenGL and Vulkan builds on the same hardware.<br />
<br />
The GPU timer API (Engine/H/Graphics/GpuTimer.h: GpuTimerEnable, GpuTimerMs, GPU_TIMER_PHASE) is exposed engine-wide so any tutorial can read per-phase GPU times. On DX11/GL it compiles to stubs that return 0.<br />
<br />
<br />
See README.md for more info.]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[ChatGPT Codex and GitHub Actions Example Repo]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11779</link>
			<pubDate>Sun, 08 Jun 2025 05:21:58 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11779</guid>
			<description><![CDATA[It has been awhile and my previous tutorials/examples using Visual Studio+WSL for Linux dev is a bit outdated, as CLion has improved dramatically on Linux and is now the best IDE imo. So I have made a demo repo of how to use CMake+GitHub Actions to do cross-platform Titan/Esenthel Engine development with full CI in GitHub and also completely build-able by ChatGPT Codex's containers. The engine binaries packaged with this also allow for completely headless linux apps. The example is non-trivial as it also shows ENet, spdlog, and Google Test integration.<br />
<br />
<br />
<a href="http://github.com/DrewGilpin/AdvancedCMakeTitanEngineExample" target="_blank">GitHub Repo</a><br />
<br />
<a href="https://medium.com/@drew.gilpin/advanced-esenthel-titan-engine-cmake-ddb85cb3803a" target="_blank">Medium Article</a><br />
<br />
<br />
<img src="https://miro.medium.com/v2/resize:fit:700/format:webp/1*G1LGF401TZoA6c_P_NRRRA.png" border="0" alt="[Image: 1*G1LGF401TZoA6c_P_NRRRA.png]" /><br />
<br />
<img src="https://miro.medium.com/v2/resize:fit:700/format:webp/1*y9Pay5hTczY38b4SURDerg.png" border="0" alt="[Image: 1*y9Pay5hTczY38b4SURDerg.png]" /><br />
<br />
<img src="https://miro.medium.com/v2/resize:fit:700/format:webp/1*NWo1OsotcrcvcZwUzxHyBw.png" border="0" alt="[Image: 1*NWo1OsotcrcvcZwUzxHyBw.png]" />]]></description>
			<content:encoded><![CDATA[It has been awhile and my previous tutorials/examples using Visual Studio+WSL for Linux dev is a bit outdated, as CLion has improved dramatically on Linux and is now the best IDE imo. So I have made a demo repo of how to use CMake+GitHub Actions to do cross-platform Titan/Esenthel Engine development with full CI in GitHub and also completely build-able by ChatGPT Codex's containers. The engine binaries packaged with this also allow for completely headless linux apps. The example is non-trivial as it also shows ENet, spdlog, and Google Test integration.<br />
<br />
<br />
<a href="http://github.com/DrewGilpin/AdvancedCMakeTitanEngineExample" target="_blank">GitHub Repo</a><br />
<br />
<a href="https://medium.com/@drew.gilpin/advanced-esenthel-titan-engine-cmake-ddb85cb3803a" target="_blank">Medium Article</a><br />
<br />
<br />
<img src="https://miro.medium.com/v2/resize:fit:700/format:webp/1*G1LGF401TZoA6c_P_NRRRA.png" border="0" alt="[Image: 1*G1LGF401TZoA6c_P_NRRRA.png]" /><br />
<br />
<img src="https://miro.medium.com/v2/resize:fit:700/format:webp/1*y9Pay5hTczY38b4SURDerg.png" border="0" alt="[Image: 1*y9Pay5hTczY38b4SURDerg.png]" /><br />
<br />
<img src="https://miro.medium.com/v2/resize:fit:700/format:webp/1*NWo1OsotcrcvcZwUzxHyBw.png" border="0" alt="[Image: 1*NWo1OsotcrcvcZwUzxHyBw.png]" />]]></content:encoded>
		</item>
		<item>
			<title><![CDATA[Tank simulation demo]]></title>
			<link>https://esenthel.com/forum/showthread.php?tid=11742</link>
			<pubDate>Mon, 11 Nov 2024 04:15:04 +0000</pubDate>
			<guid isPermaLink="false">https://esenthel.com/forum/showthread.php?tid=11742</guid>
			<description><![CDATA[Let me introduce my gun simulation prototype, featuring:<br />
<br />
- An edit mode for each tank component (chassis, mount, barrel, wheels)<br />
- A test mode that displays a procedural physics simulation of tank movement and its main gun/turret control<br />
<br />
Download: <a href="https://mega.nz/file/bd0XmaLK#LGpboIzX5fVrRh5Ewzjuw-QzzwFSzOmBfHDdE9vI9bA" target="_blank">https://mega.nz/file/bd0XmaLK#LGpboIzX5f...HDdE9vI9bA</a><br />
<br />
Press H to open Help<br />
<br />
Enjoy!]]></description>
			<content:encoded><![CDATA[Let me introduce my gun simulation prototype, featuring:<br />
<br />
- An edit mode for each tank component (chassis, mount, barrel, wheels)<br />
- A test mode that displays a procedural physics simulation of tank movement and its main gun/turret control<br />
<br />
Download: <a href="https://mega.nz/file/bd0XmaLK#LGpboIzX5fVrRh5Ewzjuw-QzzwFSzOmBfHDdE9vI9bA" target="_blank">https://mega.nz/file/bd0XmaLK#LGpboIzX5f...HDdE9vI9bA</a><br />
<br />
Press H to open Help<br />
<br />
Enjoy!]]></content:encoded>
		</item>
	</channel>
</rss>