<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">

 <title>Somnath Sarkar</title>
 <link href="https://somnathsarkar.io/atom.xml" rel="self"/>
 <link href="https://somnathsarkar.io/"/>
 <updated>2026-10-12T17:46:31-07:00</updated>
 <id>https://somnathsarkar.io</id>
 <author>
   <name>Somnath Sarkar</name>
   <email></email>
 </author>

 
 <entry>
   <title>openntc</title>
   <link href="https://somnathsarkar.io/2026/08/31/openntc/"/>
   <updated>2026-08-31T00:00:00-07:00</updated>
   <id>https://somnathsarkar.io/2026/08/31/openntc</id>
   <content type="html">&lt;h1 id=&quot;compressing-textures-by-95-with-minimal-loss-of-quality&quot;&gt;Compressing textures by 95%+ with minimal loss of quality&lt;/h1&gt;

&lt;p&gt;&lt;a href=&quot;/assets/openntc/hero_alt.webp&quot;&gt;&lt;img src=&quot;/assets/openntc/hero_alt.webp&quot; alt=&quot;Header&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In every major modern video game and engine, large sets of textures are used to drive rendering materials. Physically Based Rendering (PBR) materials can be defined by half a dozen or more high resolution textures defining properties like Albedo, Specular, Ambient Occlusion, etc. This means that a single material type may take up hundreds of MB at higher resolutions. In a world where games can contain tens of thousands of unique materials and VRAM is at an absolute premium, this is unviable. To overcome this game engines employ multiple methods; Typically, at least by applying BCn compression to the textures. BCn compression has the benefit of wide hardware support and efficient decode times, but are fixed to certain per-texture compression schemes with conservative compression factors [&lt;a href=&quot;#ref-16&quot;&gt;16&lt;/a&gt;]. Recent hardware and API changes have enabled more powerful compression algorithms, ones that take every channel in a PBR material into consideration [&lt;a href=&quot;#ref-1&quot;&gt;1&lt;/a&gt;][&lt;a href=&quot;#ref-3&quot;&gt;3&lt;/a&gt;][&lt;a href=&quot;#ref-4&quot;&gt;4&lt;/a&gt;][&lt;a href=&quot;#ref-5&quot;&gt;5&lt;/a&gt;].&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/assets/openntc/gui.webp&quot;&gt;&lt;img src=&quot;/assets/openntc/gui.webp&quot; alt=&quot;GUI&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We’ll walk through a new compression method in this article: Neural Texture Compression (NTC) [&lt;a href=&quot;#ref-1&quot;&gt;1&lt;/a&gt;] which is able to compress entire texture sets by 95%+ vs uncompressed and deliver substantially higher quality than BCn at lower bitrates. We’ll do this through the development of openntc, a source-available implementation of NTC from scratch that provides both API and GUI interfaces to its own compression utilities. We’ll go through a quick guide on how to use the library before discussing details. We’ll present the major ideas, then we’ll walk through an implementation from scratch. Finally, we’ll examine performance both at a per-material scale and in a more realistic game setting.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;h3 id=&quot;table-of-contents&quot;&gt;Table of Contents&lt;/h3&gt;
  &lt;ol&gt;
    &lt;li&gt;&lt;a href=&quot;#openntc-quick-start-and-user-guide&quot;&gt;Quick start and User guide&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#feature-pyramids&quot;&gt;Feature Pyramids&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#decoding&quot;&gt;Decoding&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#compression&quot;&gt;Compression&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#optimizing-inference&quot;&gt;Optimizing Inference&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#optimizing-compression&quot;&gt;Optimizing Compression&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#sampling--filtering&quot;&gt;Sampling &amp;amp; Filtering&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#performance&quot;&gt;Performance&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#tips-and-tricks&quot;&gt;Tips and Tricks&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#conclusions-and-benefits&quot;&gt;Conclusions and Benefits&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#references&quot;&gt;References&lt;/a&gt;&lt;/li&gt;
  &lt;/ol&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;openntc-quick-start-and-user-guide&quot;&gt;openntc: Quick start and User guide&lt;/h2&gt;

&lt;p&gt;Here’s a quick guide to testing out our implementation of NTC tools on your own textures:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Grab a copy of the latest release &lt;a href=&quot;https://github.com/somnathsarkar/openntc/releases&quot;&gt;here&lt;/a&gt;. Currently, we support Windows/CUDA machines for compression, and DirectX 12 for the runtime (decoding).&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Create a description of your material using a JSON manifest. The format generally matches those of other NTC tools [&lt;a href=&quot;#ref-2&quot;&gt;2&lt;/a&gt;]:&lt;/p&gt;

    &lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;width&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4096&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;height&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4096&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;textures&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;fileName&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;BaseColor.png&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;isSRGB&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;semantics&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;Albedo&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;RGB&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;fileName&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Normal.png&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;semantics&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;Normal&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;RGB&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;fileName&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;RoughnessMetalness.png&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;semantics&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;Roughness&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;G&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;Metallic&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;B&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;fileName&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;AmbientOcclusion.png&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;semantics&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;Occlusion&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;R&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Select a profile in the GUI. Each profile has a different compression scheme, each with different bytes-per-pixel channel and quality values. Here are example results on a 9-channel, 4096 x 4096 PBR texture set at openntc’s Ultra preset. Note that the uncompressed size of this texture set is 201.3 MB.&lt;/p&gt;

    &lt;table&gt;
      &lt;thead&gt;
        &lt;tr&gt;
          &lt;th&gt; &lt;/th&gt;
          &lt;th&gt;openntc 0.2 bpp&lt;/th&gt;
          &lt;th&gt;openntc 0.5 bpp&lt;/th&gt;
          &lt;th&gt;openntc 1.0 bpp&lt;/th&gt;
          &lt;th&gt;openntc 2.25 bpp&lt;/th&gt;
        &lt;/tr&gt;
      &lt;/thead&gt;
      &lt;tbody&gt;
        &lt;tr&gt;
          &lt;td&gt;Size&lt;/td&gt;
          &lt;td&gt;3.93 MB&lt;/td&gt;
          &lt;td&gt;9.52 MB&lt;/td&gt;
          &lt;td&gt;19.0 MB&lt;/td&gt;
          &lt;td&gt;42.5 MB&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
          &lt;td&gt;Quality (PSNR)&lt;/td&gt;
          &lt;td&gt;33.91 dB&lt;/td&gt;
          &lt;td&gt;36.66 dB&lt;/td&gt;
          &lt;td&gt;40.25 dB&lt;/td&gt;
          &lt;td&gt;43.87 dB&lt;/td&gt;
        &lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
  &lt;/li&gt;
  &lt;li&gt;Select a training quality and hit train, progress is viewable via the training bar.&lt;/li&gt;
  &lt;li&gt;Compare reconstruction quality with the original by choosing one of the NTC options in the view panels, then Save Compressed via Ctrl-S.&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Alternatively, we can use the libopenntc API via C++ as follows:&lt;/p&gt;

    &lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;#include&lt;/span&gt; &lt;span class=&quot;cpf&quot;&gt;&amp;lt;libopenntc/libopenntc.h&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;openntc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;Context&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;ContextInitInfo&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;init&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;profile_&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Profile&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Bpp_0_5&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Result&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Success&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LoadManifest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;manifest.json&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Result&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Success&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;TrainInfo&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;train&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quality_&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Quality&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Medium&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;grids_per_batch_&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;BeginTraining&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TrainUntilComplete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;EvalResults&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;eval&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Eval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;std&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cout&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;PSNR: &quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;eval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;psnr_&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot; dB&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;Dump&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;bricks.ntc&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GetCompressedData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;To sample openntc-compressed textures in your own engine or project, use the openntc-runtime library to load buffers from the .ntc file. Bind these buffers as HLSL descriptors to your shader:&lt;/p&gt;

    &lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;#include&lt;/span&gt; &lt;span class=&quot;cpf&quot;&gt;&amp;lt;libopenntc/openntc_runtime.h&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;openntc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;FileData&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;openntc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Load&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;bricks.ntc&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;openntc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Result&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Success&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;openntc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CompressedData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cd&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Upload data to GPU, CreateBufferWithData is a placeholder helper function&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// SRV: Buffer&amp;lt;uint&amp;gt;, Format: DXGI_FORMAT_R32_UINT&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;ID3D12Resource&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;g0_buf&lt;/span&gt;  &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CreateBufferWithData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;g0_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;g0_total_size_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// SRV: Buffer&amp;lt;uint&amp;gt;, Format: DXGI_FORMAT_R32_UINT&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;ID3D12Resource&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;g1_buf&lt;/span&gt;  &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CreateBufferWithData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;g1_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;g1_total_size_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// SRV: ByteAddressBuffer, Format: DXGI_FORMAT_R32_TYPELESS&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;ID3D12Resource&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dec_buf&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CreateBufferWithData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;decoder_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;decoder_size_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Constant buffer with texture-related information&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;openntc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NTCConstants&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;consts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;openntc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;FillNTCConstants&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;consts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;ID3D12Resource&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ntc_cb&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CreateBufferWithData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;consts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;sizeof&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;consts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Include the ntc_decode.hlsli HLSL header and call our helper functions to retrieve your sample!&lt;/p&gt;

    &lt;div class=&quot;language-hlsl highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;#define BPP_0_5
#include&lt;/span&gt; &lt;span class=&quot;cpf&quot;&gt;&quot;ntc_decode.hlsli&quot;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;
&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;ConstantBuffer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NTC&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;NTCCBV&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;register&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;b1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;// NTCConstants from FillNTCConstants&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;Buffer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;uint&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;g0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;register&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;              &lt;span class=&quot;c1&quot;&gt;// Feature pyramid 0&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;Buffer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;uint&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;g1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;register&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;              &lt;span class=&quot;c1&quot;&gt;// Feature pyramid 1&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;ByteAddressBuffer&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;decoder&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;register&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;// Decoder MLP weights&lt;/span&gt;

&lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;openntc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MaterialParams&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;openntc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SampleMaterial&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;g0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;g1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;decoder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;NTCCBV&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;uv&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Now use material params mat.albedo_, mat.roughness_, etc. to perform lighting.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let’s now go deeper into how this all works:&lt;/p&gt;

&lt;h2 id=&quot;feature-pyramids&quot;&gt;Feature Pyramids&lt;/h2&gt;

&lt;p&gt;We can think of a uncompressed 2D texture as a grid where each cell of the grid contains up to 4 channels or features and each channel takes up a number of bits, typically 8/16/32 depending on the texture format. In a game engine, downsampled versions of the full resolution texture are also used when distances between sample locations are larger. This forms a mip chain, which can resemble a pyramid of grids of features.&lt;/p&gt;

&lt;p&gt;Neural compressed textures generalize this, where we have a pyramid of features, except each cell of each grid may have an arbitrary number of features each with an arbitrary number of bits. Instead of each cell containing actual texture data directly, they contain input features to a decoder, typically a small MLP. Instead of each mip representing a single LOD for one channel of a material, each level of a feature pyramid represents all of the channels for multiple LODs of a material. The compression ratio can be tweaked by changing of the number of bits per feature and features per cell.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/assets/openntc/feature_pyramids.webp&quot;&gt;&lt;img src=&quot;/assets/openntc/feature_pyramids.webp&quot; alt=&quot;Feature pyramids&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;decoding&quot;&gt;Decoding&lt;/h2&gt;

&lt;p&gt;Decoding NTC textures can be done prior to their use in a shader, essentially creating an uncompressed version of the textures before sampling (&lt;strong&gt;Inference-on-Load&lt;/strong&gt;), or in real-time as they are being sampled (&lt;strong&gt;Inference-on-Sample&lt;/strong&gt;). &lt;strong&gt;Inference-on-Load&lt;/strong&gt; can be followed by compressing the textures to BCn, which means we don’t actually get any VRAM savings for a NTC compressed texture at runtime over BCn, but this can still help with reducing download sizes and install footprints. A huge benefit of &lt;strong&gt;Inference-on-Load&lt;/strong&gt; is that the graphics programmer doesn’t need to change his shaders or sampling behavior in any way. On the other hand, &lt;strong&gt;Inference-on-Sample&lt;/strong&gt; allows us to only store the compressed NTC data at runtime. This means every sample requires a full inference step, which involves modifying shader code. This inference step is described below.&lt;/p&gt;

&lt;p&gt;For &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;openntc&lt;/code&gt;, similarly to RTXNTC [&lt;a href=&quot;#ref-1&quot;&gt;1&lt;/a&gt;], we use two separate feature pyramids. The first pyramid is higher resolution, and we draw four samples from it corresponding to the four closest cells to the sample location. The second feature pyramid uses a bilinear sample of the four nearest cells. This combination allows the network to learn its own filtering scheme for the first set of features. Since feature grids are coarser than the raw textures they compress, it is critical to provide additional positional information to distinguish between them. We supply triangle waves at multiple resolutions as a positional encoding [&lt;a href=&quot;#ref-1&quot;&gt;1&lt;/a&gt;]. Finally, a single normalized float provides LOD information.&lt;/p&gt;

&lt;p&gt;These are input features fed into a simple neural network called a Multi-Layer Perceptron (MLP). Multiple decoder architectures have been proposed [&lt;a href=&quot;#ref-1&quot;&gt;1&lt;/a&gt;][&lt;a href=&quot;#ref-3&quot;&gt;3&lt;/a&gt;][&lt;a href=&quot;#ref-4&quot;&gt;4&lt;/a&gt;][&lt;a href=&quot;#ref-5&quot;&gt;5&lt;/a&gt;], for openntc I opted for a simple two layer network each with 64 neurons. This means that inference in the shader involves three matrix-vector multiplications, each separated by a non-linear activation function. The specific activation function we use is hardGELU, a piecewise approximation of GELU [&lt;a href=&quot;#ref-11&quot;&gt;11&lt;/a&gt;] which is relatively computationally efficient:&lt;/p&gt;
&lt;div class=&quot;language-hlsl highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;hardgelu&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;compression&quot;&gt;Compression&lt;/h2&gt;

&lt;p&gt;This diagram breaks down how openntc works, using a set of uncompressed textures as input, using our 0.5 bits-per-pixel-channel profile as reference. A similar process works for the other profiles.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/assets/openntc/compression_flow.webp&quot;&gt;&lt;img src=&quot;/assets/openntc/compression_flow.webp&quot; alt=&quot;Compression flow&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;First we generate any missing mips for the input textures using a high-quality edge preserving Lanczos filter [&lt;a href=&quot;#ref-12&quot;&gt;12&lt;/a&gt;].&lt;/li&gt;
  &lt;li&gt;We choose a profile. Each profile has a different bits-per-pixel-per-channel number, mainly due to the number of features and bits-per-feature of the feature grids.&lt;/li&gt;
  &lt;li&gt;For each training iteration, we draw an LOD at random and randomly sample a square grid from the mips corresponding to this LOD for each texture in the material. This forms our ground truth information for the current batch.&lt;/li&gt;
  &lt;li&gt;We then draw features from matching positions in the untrained feature pyramids, as well as the positional and LOD information laid out in the &lt;a href=&quot;#decoding&quot;&gt;previous section&lt;/a&gt;. Through training we use 32-bit floats to represent weights, however the compressed weights will have far lower bit widths (generally 2-4 bits). To make the model robust to this quantization, we add an amount of noise to the sampled feature pyramids proportionate to the final bit width [&lt;a href=&quot;#ref-1&quot;&gt;1&lt;/a&gt;].&lt;/li&gt;
  &lt;li&gt;This forms a classic supervised learning pattern. We make a forward pass of the input features through the network to obtain the models prediction of the input at this time.&lt;/li&gt;
  &lt;li&gt;These predictions can be compared against the ground truth using the mean-squared error loss function. (L2 Loss)&lt;/li&gt;
  &lt;li&gt;We then backpropagate this loss through the network, updating decoder weights and feature pyramids to minimize the loss between predictions and ground truth.&lt;/li&gt;
  &lt;li&gt;Finally we lock the feature grid weights to their final quantized values, rounding to the nearest quantization bin matching the selected profile.&lt;/li&gt;
  &lt;li&gt;We let the decoder train for a short time longer (~5% of total training budget) at the settled grid weights.&lt;/li&gt;
  &lt;li&gt;The finished compressed feature grids are packed tightly into their profile bit-widths and the decoder is then quantized to 8-bit integers for efficient DP4A/Cooperative Vector decoding. Since we need to requantize inputs after each layer we must also measure the absolute maximum output of each layer as a calibration step.&lt;/li&gt;
  &lt;li&gt;The feature grids, decoder and certain NTC metadata specific to that material are distributed together as the final compressed texture.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;optimizing-inference&quot;&gt;Optimizing Inference&lt;/h2&gt;

&lt;p&gt;Running a forward pass multi-layer neural network for every pixel is extremely computationally intensive. Naive matrix multiplication on floats using sequential multiply and add instructions will be bound by loading weights. Inference-on-Sample models are especially incentivized to maximize performance in this area.&lt;/p&gt;

&lt;h3 id=&quot;dp4a&quot;&gt;DP4A&lt;/h3&gt;

&lt;p&gt;Shader model 6.4 introduced Dot Product of 4 Elements and Accumulate (DP4A) instructions that are able to interpret a single 32-bit integer register as vector of 4 8-bit integers, and compute the dot product of two such vectors and storing the result in a single 32-bit integer, all in one instruction [&lt;a href=&quot;#ref-6&quot;&gt;6&lt;/a&gt;].&lt;/p&gt;

&lt;p&gt;This was further supported by SM 6.6 (NVIDIA Pascal, AMD RDNA) which added helper functions to pack and clamp sets of integers into 8-bit slots of a single 32-bit integer [&lt;a href=&quot;#ref-7&quot;&gt;7&lt;/a&gt;]. This dramatically reduces the loads and instructions required, and brings the inference time into acceptable territory. Since matrix-vector multiplication involves taking the dot products of each row of the matrix with the vector, its a natural fit.&lt;/p&gt;

&lt;div class=&quot;language-hlsl highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Matrix multiplication using DP4A&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// input and output are HIDDEN_DIM-element vectors with each 4 consecutive&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;//  8-bit elements packed into 32-bit uints&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;uint&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;HIDDEN_DIM&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;uint&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;HIDDEN_DIM&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Evaluate 4 rows at a time&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HIDDEN_DIM&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;int4&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;acc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;unroll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;j&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;j&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HIDDEN_DIM&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;j&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// MatrixLoad4: Placeholder to load 4x4 contiguous weights starting from column j in row i&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Each row is also packed into 8-bits, meaning a uint4 contains 16 values.&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;uint4&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Wx&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MatrixLoad4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;j&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;uint4&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Wy&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MatrixLoad4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;j&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;uint4&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Wz&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MatrixLoad4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;j&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;uint4&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Ww&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MatrixLoad4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;j&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;unroll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;// Requires Shader Model 6.4&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;acc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dot4add_i8packed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Wx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;j&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;acc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;acc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dot4add_i8packed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Wy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;j&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;acc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;acc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;z&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dot4add_i8packed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Wz&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;j&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;acc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;z&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;acc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;w&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dot4add_i8packed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Ww&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;j&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;acc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;w&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// When repacking into 8-bits, we need to provide scaling factors from the Calibration step&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;//  to ensure best use of bits available. ScalingForRows is the factor for rows [i, i+3]&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;float4&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;facc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;float4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;acc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ScalingForRows&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;facc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hardGELU&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;facc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ScalingForActivation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;int4&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unpacked&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;facc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// Requires Shader Model 6.6&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pack_clamp_s8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;unpacked&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;cooperative-vectors&quot;&gt;Cooperative Vectors&lt;/h3&gt;

&lt;p&gt;The real benefit, however, comes with cooperative vector support. This refers to hardware-accelerated matrix multiplication using NVIDIA’s tensor cores and AMD’s matrix cores. The API for this hasn’t settled yet. DirectX 12 proposed a version of this called cooperative vectors in Shader Model 6.9 [&lt;a href=&quot;#ref-8&quot;&gt;8&lt;/a&gt;], then reworked it into a more general linear algebra library for Shader Model 6.10 [&lt;a href=&quot;#ref-9&quot;&gt;9&lt;/a&gt;]. At the time of writing, its still in preview, requiring beta drivers and a preview Agility SDK. As we’ll see in the performance section, however, this is by far the most performant approach since it executes all loads and and matrix-vector multiplications for a single warp in one step.&lt;/p&gt;

&lt;div class=&quot;language-hlsl highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Matrix multiplication using cooperative vectors&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// Requires Shader Model 6.10&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;#include&lt;/span&gt; &lt;span class=&quot;cpf&quot;&gt;&amp;lt;dx/linalg.h&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dx&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;linalg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// input and output are HIDDEN_DIM-element vectors with each 4 consecutive&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;//  8-bit elements packed into 32-bit uints&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;vector&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;uint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HIDDEN_DIM&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;vector&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;uint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HIDDEN_DIM&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;typedef&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Matrix&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ComponentType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;I8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HIDDEN_DIM&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HIDDEN_DIM&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MatrixUse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MatrixScope&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Thread&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;W_t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;W_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;W&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;W_t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Load&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MatrixLayout&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RowMajor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;decoder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MATRIX_OFFSET&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;cm&quot;&gt;/*row stride*/&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HIDDEN_DIM&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Tell the unit to read the packed uints as int8&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;InterpretedVector&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;uint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HIDDEN_DIM&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ComponentType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;I8&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MakeInterpretedVector&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ComponentType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;I8&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;vector&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;int32_t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HIDDEN_DIM&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;acc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MultiplyAdd&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;int32_t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;W&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;vector&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;int32_t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HIDDEN_DIM&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// When repacking into 8-bits, we need to provide scaling factors from the Calibration step&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;//  to ensure best use of bits available. ScalingForRows is the factor for all HIDDEN_DIM rows&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;vector&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HIDDEN_DIM&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;facc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;vector&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HIDDEN_DIM&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;acc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;ScalingForRows&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;facc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hardGELU&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;facc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;ScalingForActivation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;vector&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;int32_t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HIDDEN_DIM&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unpacked&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;vector&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;int32_t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HIDDEN_DIM&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;facc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;unroll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;HIDDEN_DIM&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// Requires Shader Model 6.6&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pack_clamp_s8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;unpacked&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unpacked&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unpacked&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unpacked&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]));&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;optimizing-compression&quot;&gt;Optimizing Compression&lt;/h2&gt;

&lt;p&gt;While deep-learning frameworks like pytorch were helpful for the initial research/proof-of-concept period, I quickly grew tired of the longer iteration times for experiments and needed a faster approach. Switching to hand-written CUDA kernels offered tremendous speed-up oppurtunities:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Training involved dozens of hundreds of small operations per step with significant torch/python overhead and many intermediate tensors. Rolling our own allows us to be explicit about which operations map to a single kernal invocation, and eliminate dynamic memory allocation along the critical path.&lt;/li&gt;
  &lt;li&gt;Using CUBLAS for the large matrix multiplication operations involved in forward and backward passes over the network, allows us to benefit from hardware-accelerated tensor cores when available.&lt;/li&gt;
  &lt;li&gt;Because we train on contiguous grids of the input textures, the backward pass over feature pyramids updates the same features multiple times. Practically, this means we need atomic updates which increases compression time based on contention. We can exploit the fact that threads in the same warp are likely to share features (due to the way features are drawn, see the section on decoding). We can use warp-level synchronization [&lt;a href=&quot;#ref-10&quot;&gt;10&lt;/a&gt;] to reduce contention as follows:&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-cuda highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;__device__&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mask_aggregate_atomic_increment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;grid_grad&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cell&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// Lanes in this warp (among the active ones) whose cell equals mine&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;unsigned&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;peers&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__match_any_sync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;__activemask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cell&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lane&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;threadIdx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;leader&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__ffs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;peers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;          &lt;span class=&quot;c1&quot;&gt;// lowest peer lane does the write&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// Pull each other peer&apos;s value with a shuffle&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;total&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;unsigned&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;others&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;peers&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1u&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lane&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;others&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;next&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__ffs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;others&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;total&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__shfl_sync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;peers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;others&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1u&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lane&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;leader&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;atomicAdd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;grid_grad&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cell&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;total&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;sampling--filtering&quot;&gt;Sampling &amp;amp; Filtering&lt;/h2&gt;

&lt;p&gt;Since decoding neural compressed textures must be done in shader code vs. hardware support for BCn textures, we must take special care for how they are filtered. Moreover, since decoding a single sample is computationally expensive, taking multiple samples for trilinear or anisotropic sampling is unviable. We use stochastic filtering instead [&lt;a href=&quot;#ref-1&quot;&gt;1&lt;/a&gt;][&lt;a href=&quot;#ref-13&quot;&gt;13&lt;/a&gt;]. Here’s an example of how that works with trilinear sampling. Lets say we need to sample lod 2.4. In conventional hardware filtering, the sampler takes two bilinear samples at LOD 2 and 3, then lerps between them with parameter 0.4. For stochastic filtering, instead we just randomly sample a single LOD: we sample LOD 2 with probability 0.6, and LOD 3 with probability 0.4. Similarly for bilinear sampling, we can individually select an integral texel position based on the fractional value of the sample location.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/assets/openntc/stochastic_lod.webp&quot;&gt;&lt;img src=&quot;/assets/openntc/stochastic_lod.webp&quot; alt=&quot;Stochastic LOD filtering&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Anisotropic filtering is trickier. We can think of the uv space spanned by a single sample to have two axes, measured by taking the derivatives along the x and y axis of the quad. At grazing angles, the uv space spanned along one axis (major axis) may be much larger than a perpendicular axis (minor axis). We use the minor axis for LOD selection, then take a sample at random along the major axis.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/assets/openntc/stochastic_aniso.webp&quot;&gt;&lt;img src=&quot;/assets/openntc/stochastic_aniso.webp&quot; alt=&quot;Stochastic anisotropic filtering&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Making use of stochastic filtering means that some sort of temporal accumulation is necessary to avoid jittering. This is less of a drawback now that TAA is the most common form of anti-aliasing, but is worth mentioning. We use a simple TAA implementation with color clamping, more details here [&lt;a href=&quot;#ref-14&quot;&gt;14&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/assets/openntc/stochastic_taa.webp&quot;&gt;&lt;img src=&quot;/assets/openntc/stochastic_taa.webp&quot; alt=&quot;Stochastic filtering with TAA&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;performance&quot;&gt;Performance&lt;/h2&gt;

&lt;p&gt;Analyzing performance of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;openntc&lt;/code&gt; requires being aware of the different axes along which members can be compared. There is the actual space the compressed texture takes on disk, the quality of the texture after decompression and the performance cost of the decoding process of the texture. These experiments were conducted on the public domain ambientCG PBR material set [&lt;a href=&quot;#ref-17&quot;&gt;17&lt;/a&gt;] at 4096x4096 resolution. We will compare them both against the uncompressed data and BCn compressed textures. Since there are many possible schemes to encode each texture using Block Compression, we use a reasonable batch of settings that resembles Unreal Engine 5’s default encoding scheme [&lt;a href=&quot;#ref-19&quot;&gt;19&lt;/a&gt;]. We use the open-source &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;texconv&lt;/code&gt; package [&lt;a href=&quot;#ref-15&quot;&gt;15&lt;/a&gt;] to perform BCn compression.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Format&lt;/th&gt;
      &lt;th&gt;Texture Layout Description&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Uncompressed&lt;/td&gt;
      &lt;td&gt;Albedo R8G8B8 (3), Normal R8G8B8 (3), AO / Rough / Disp R8 (1 each)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;BCn&lt;/td&gt;
      &lt;td&gt;UE5 defaults* [&lt;a href=&quot;#ref-19&quot;&gt;19&lt;/a&gt;]: Albedo BC1 (0.5), Normal BC5 (1), AO / Rough / Disp BC4 (0.5 each)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;openntc 2.25 bpp&lt;/td&gt;
      &lt;td&gt;G0 16 ch x 4 b at 2048², G1 12 ch x 4 b at 1024², 5 levels&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;openntc 1.0 bpp&lt;/td&gt;
      &lt;td&gt;G0 12 ch x 2 b at 2048², G1 10 ch x 4 b at 1024², 5 levels&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;openntc 0.5 bpp&lt;/td&gt;
      &lt;td&gt;G0 12 ch x 4 b at 1024², G1 20 ch x 4 b at 512², 5 levels&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;openntc 0.2 bpp&lt;/td&gt;
      &lt;td&gt;G0 8 ch x 2 b at 1024², G1 12 ch x 4 b at 512², 5 levels&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;em&gt;*Single channel maps typically use the ‘Alpha’ setting in UE5, which uses BC4.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;size&quot;&gt;Size&lt;/h3&gt;

&lt;p&gt;This refers to the size of the textures on disk and in VRAM for the Inference-on-Sample path. Smaller values are better.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/assets/openntc/size_chart.webp&quot;&gt;&lt;img src=&quot;/assets/openntc/size_chart.webp&quot; alt=&quot;Size chart&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Format&lt;/th&gt;
      &lt;th&gt;Size with Mips (MB)&lt;/th&gt;
      &lt;th&gt;Compression ratio vs uncompressed&lt;/th&gt;
      &lt;th&gt;Compression ratio vs BCn&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Uncompressed&lt;/td&gt;
      &lt;td&gt;201.3 MB&lt;/td&gt;
      &lt;td&gt;1:1&lt;/td&gt;
      &lt;td&gt;1:3&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;BCn&lt;/td&gt;
      &lt;td&gt;67.1 MB&lt;/td&gt;
      &lt;td&gt;3:1&lt;/td&gt;
      &lt;td&gt;1:1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;openntc 2.25 bpp&lt;/td&gt;
      &lt;td&gt;42.5 MB&lt;/td&gt;
      &lt;td&gt;4.7:1&lt;/td&gt;
      &lt;td&gt;1.6:1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;openntc 1.0 bpp&lt;/td&gt;
      &lt;td&gt;19.0 MB&lt;/td&gt;
      &lt;td&gt;10.6:1&lt;/td&gt;
      &lt;td&gt;3.5:1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;openntc 0.5 bpp&lt;/td&gt;
      &lt;td&gt;9.52 MB&lt;/td&gt;
      &lt;td&gt;21:1&lt;/td&gt;
      &lt;td&gt;7:1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;openntc 0.2 bpp&lt;/td&gt;
      &lt;td&gt;3.93 MB&lt;/td&gt;
      &lt;td&gt;51:1&lt;/td&gt;
      &lt;td&gt;17:1&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;quality&quot;&gt;Quality&lt;/h3&gt;

&lt;p&gt;Quality is measured by Peak Signal-to-Noise Ratio. (PSNR), where higher values are better:&lt;/p&gt;

\[\mathrm{PSNR} = 10 \log_{10} \frac{\mathrm{MAX}^2}{\mathrm{MSE}} = -10 \log_{10} \mathrm{MSE}, \qquad \mathrm{MSE} = \frac{1}{N} \sum_{i=1}^{N} \left( x_i - \hat{x}_i \right)^2\]

&lt;p&gt;where $x_i$ are the ground truth 8-bit texel values scaled to $[0, 1]$, $\hat{x}_i$ are the decoded compressed values. Since the magnitude of PSNR can vary widely based on the source image, and the relevant quantity is just the relative PSNR between presets we present the values for a single material from the ambientCG PBR set, Tiles131. The same material is used for our header image in the introduction.&lt;/p&gt;

&lt;p&gt;We can adjust the number of training loop steps to generate quality presets. For &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;openntc&lt;/code&gt;, we use Low (30k), Medium (120k), High (240k) and Ultra (480k) to offer different compression time vs. quality tradeoffs.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/assets/openntc/psnr_chart.webp&quot;&gt;&lt;img src=&quot;/assets/openntc/psnr_chart.webp&quot; alt=&quot;PSNR chart&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Tiles131 (4096²)&lt;/th&gt;
      &lt;th&gt;Low (30k)&lt;/th&gt;
      &lt;th&gt;Medium (120k)&lt;/th&gt;
      &lt;th&gt;High (240k)&lt;/th&gt;
      &lt;th&gt;Ultra (480k)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;BCn&lt;/td&gt;
      &lt;td&gt;39.92 dB&lt;/td&gt;
      &lt;td&gt;39.92 dB&lt;/td&gt;
      &lt;td&gt;39.92 dB&lt;/td&gt;
      &lt;td&gt;39.92 dB&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;openntc 0.2 bpp&lt;/td&gt;
      &lt;td&gt;31.82 dB&lt;/td&gt;
      &lt;td&gt;33.35 dB&lt;/td&gt;
      &lt;td&gt;33.58 dB&lt;/td&gt;
      &lt;td&gt;33.91 dB&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;openntc 0.5 bpp&lt;/td&gt;
      &lt;td&gt;33.21 dB&lt;/td&gt;
      &lt;td&gt;35.46 dB&lt;/td&gt;
      &lt;td&gt;35.79 dB&lt;/td&gt;
      &lt;td&gt;36.66 dB&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;openntc 1.0 bpp&lt;/td&gt;
      &lt;td&gt;35.84 dB&lt;/td&gt;
      &lt;td&gt;38.90 dB&lt;/td&gt;
      &lt;td&gt;39.64 dB&lt;/td&gt;
      &lt;td&gt;40.25 dB&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;openntc 2.25 bpp&lt;/td&gt;
      &lt;td&gt;38.20 dB&lt;/td&gt;
      &lt;td&gt;41.98 dB&lt;/td&gt;
      &lt;td&gt;43.17 dB&lt;/td&gt;
      &lt;td&gt;43.87 dB&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;decoding-speed&quot;&gt;Decoding speed&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;/assets/openntc/sponza_split.webp&quot;&gt;&lt;img src=&quot;/assets/openntc/sponza_split.webp&quot; alt=&quot;Sponza split comparison&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, heres a full scene rendered entirely with openntc materials, the famous Sponza test scene [&lt;a href=&quot;#ref-18&quot;&gt;18&lt;/a&gt;]. This is a very simple deferred renderer with relatively simple lighting that is intended solely to measure the change in the performance of texture samples during the G-buffer write. All source materials use 4k texture sizes.&lt;/p&gt;

&lt;p&gt;Since texture sampling performance is the main source of performance increase, writing out the (UV, LOD, Material) index to an intermediate buffer and then sample NTC materials to form the G-buffer, similar to deferred texturing. Since NTC converts entire materials to a single sample, ideally we want just one sample per-pixel which can be achieved this way.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/assets/openntc/perf_chart.webp&quot;&gt;&lt;img src=&quot;/assets/openntc/perf_chart.webp&quot; alt=&quot;Performance chart&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a full scene, at 1080p, the cooperative vector path is the only realistic Inference-on-Sample path at frame rates acceptable to a game engine. Here is the full table of render timings for comparison.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Material path&lt;/th&gt;
      &lt;th&gt;Direct, DP4A&lt;/th&gt;
      &lt;th&gt;Direct, Coop&lt;/th&gt;
      &lt;th&gt;Deferred texturing, DP4A&lt;/th&gt;
      &lt;th&gt;Deferred texturing, Coop&lt;/th&gt;
      &lt;th&gt;Deferred texturing, Coop + wave partition&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Uncompressed textures (hardware filtering)&lt;/td&gt;
      &lt;td&gt;0.75 ms&lt;/td&gt;
      &lt;td&gt;–&lt;/td&gt;
      &lt;td&gt;0.84 ms&lt;/td&gt;
      &lt;td&gt;–&lt;/td&gt;
      &lt;td&gt;–&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;NTC 0.2 bpp&lt;/td&gt;
      &lt;td&gt;14.38 ms&lt;/td&gt;
      &lt;td&gt;4.80 ms&lt;/td&gt;
      &lt;td&gt;9.05 ms&lt;/td&gt;
      &lt;td&gt;1.74 ms&lt;/td&gt;
      &lt;td&gt;2.00 ms&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;NTC 0.5 bpp&lt;/td&gt;
      &lt;td&gt;16.81 ms&lt;/td&gt;
      &lt;td&gt;5.17 ms&lt;/td&gt;
      &lt;td&gt;10.20 ms&lt;/td&gt;
      &lt;td&gt;1.91 ms&lt;/td&gt;
      &lt;td&gt;2.20 ms&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;NTC 1.0 bpp&lt;/td&gt;
      &lt;td&gt;15.10 ms&lt;/td&gt;
      &lt;td&gt;5.02 ms&lt;/td&gt;
      &lt;td&gt;9.58 ms&lt;/td&gt;
      &lt;td&gt;1.89 ms&lt;/td&gt;
      &lt;td&gt;2.12 ms&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;NTC 2.25 bpp&lt;/td&gt;
      &lt;td&gt;17.42 ms&lt;/td&gt;
      &lt;td&gt;5.02 ms&lt;/td&gt;
      &lt;td&gt;10.64 ms&lt;/td&gt;
      &lt;td&gt;1.93 ms&lt;/td&gt;
      &lt;td&gt;2.17 ms&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;em&gt;All times refer to G-buffer generation times on an RTX 4080 at 1080p. Deferred texturing times include both the uv-pass and material sampling step.&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;tips-and-tricks&quot;&gt;Tips and Tricks&lt;/h2&gt;

&lt;p&gt;While reference material [&lt;a href=&quot;#ref-1&quot;&gt;1&lt;/a&gt;][&lt;a href=&quot;#ref-2&quot;&gt;2&lt;/a&gt;] was very helpful for this project, there are several things to keep in mind when using or implementing your own Neural Texture Compression engine that were not obvious to me:&lt;/p&gt;

&lt;h3 id=&quot;training-distribution&quot;&gt;Training distribution&lt;/h3&gt;
&lt;p&gt;When comparing reconstruction quality of a NTC scheme against other implementations, ensure that your measured PSNR numbers are as close to an apples-to-apples comparison as possible. If you are weighting PSNR on the higher resolution mips more heavily, you should be providing more training samples from the high resolution LODs. One of the trickiest bugs I resolved took place because during training I was drawing grids from each mip level such that the grid never overlapped the boundary. This had the subtle effect of reducing reconstruction quality at the edges of each texture, which was most noticable at lower resolution mips.&lt;/p&gt;

&lt;h3 id=&quot;multiple-materials&quot;&gt;Multiple materials&lt;/h3&gt;
&lt;p&gt;Cooperative Vectors are built to exploit wave-level cooperation, where optimal performance is typically achieved where every thread in a wave is performing multiplication with the same matrix at the same time. In our deferred texturing example in Sponza [&lt;a href=&quot;#ref-18&quot;&gt;18&lt;/a&gt;], this isn’t the case, and multiple threads in a wave will multiply with different matrices when they contain different materials.&lt;/p&gt;

&lt;p&gt;The original RTXNTC paper [&lt;a href=&quot;#ref-1&quot;&gt;1&lt;/a&gt;] suggests a wave-synchronization step where we iterate over different materials one at a time or avoid using cooperative vectors entirely in this case:&lt;/p&gt;

&lt;div class=&quot;language-hlsl highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;Outputs&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;runNetwork&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Inputs&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;uint&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;paramOffsets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// Check if all lanes are active.&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;WaveActiveCountBits&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;WaveGetLaneCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;uint&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mask&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;uint&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lane&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Iterate over unique network parameters in the SIMD group.&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mask&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;// Broadcast the parameter offset across SIMD lanes.&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;uint&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;offset&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;WaveReadLaneAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;paramOffsets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lane&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;bool&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;matchingLanes&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;offset&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;paramOffsets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;// Evaluate the MLP with matrix acceleration.&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;Outputs&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MLP&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;offset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;// Store the outputs for matching lanes.&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;storeOutputs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;matchingLanes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;// Clear the evaluated lanes.&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;mask&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;WaveActiveBallot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;matchingLanes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;lane&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;firstbitlow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Fallback without matrix acceleration.&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;However, the DirectX specification for this feature [&lt;a href=&quot;#ref-8&quot;&gt;8&lt;/a&gt;] does not enforce uniform control flow. In fact, omitting this wave synchronization improves performance by ~20% in our Sponza demo. This may change based on the average number of unique materials per wave.&lt;/p&gt;

&lt;h2 id=&quot;conclusions-and-benefits&quot;&gt;Conclusions and Benefits&lt;/h2&gt;

&lt;p&gt;Neural Texture Compression is extremely promising as a future compression method, and I expect to see it in use more as cooperative vector support [&lt;a href=&quot;#ref-8&quot;&gt;8&lt;/a&gt;][&lt;a href=&quot;#ref-9&quot;&gt;9&lt;/a&gt;] matures and tensor cores become more widely adopted. In the interim DP4A [&lt;a href=&quot;#ref-6&quot;&gt;6&lt;/a&gt;] serves as a stopgap way to get your feet wet with this technology. In particular, I see NTC textures having the potential to have a profound impact on game engines in the following ways:&lt;/p&gt;

&lt;h3 id=&quot;asset-streaming&quot;&gt;Asset streaming&lt;/h3&gt;
&lt;p&gt;Typically engines have systems to determine which mips to keep in memory, like Virtual Texturing or simple heuristics. It’s common to have the lowest resolution mip always resident in memory to avoid frame hitching to wait for a load, but this can cause the very common issue of texture pop-in when the better mip becomes available. An NTC compressed texture can deliver a much higher quality fallback option at a similar low memory footprint.&lt;/p&gt;

&lt;h3 id=&quot;scalability&quot;&gt;Scalability&lt;/h3&gt;
&lt;p&gt;Unlike BCn textures, which offer a small specific set of bits-per-pixel schemes, NTC is a lot more flexible. One can quickly tune the bits per feature, number of features per cell, or number of levels in the feature pyramid to offer a large degree of scalability in the compression ratio. Similarly, number of layers or sizes of intermediate layers of the decoder can be modified to affect inference speed. This means that we can choose an arbitrary scheme in the space of Compression Ratio vs Runtime Performance vs Quality that suits our needs, possibly even algorithmically. Trying to reduce VRAM use in a specific scene? Tune down the features per cell for materials by 10% and determine whether the matching quality reduction is acceptable. Having trouble making 60 FPS in a busy area of a level? Reduce decoder hidden states from 64 to 48 and re-evaluate.&lt;/p&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;p&gt;&lt;a name=&quot;ref-1&quot;&gt;&lt;/a&gt;[1] K. Vaidyanathan, M. Salvi, B. Wronski, T. Akenine-Möller, P. Ebelin, A. Lefohn, 2023. &lt;a href=&quot;https://research.nvidia.com/labs/rtr/neural_texture_compression/&quot;&gt;Random-Access Neural Compression of Material Textures&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a name=&quot;ref-2&quot;&gt;&lt;/a&gt;[2] NVIDIA, 2025. &lt;a href=&quot;https://github.com/NVIDIA-RTX/RTXNTC&quot;&gt;RTX Neural Texture Compression SDK (RTXNTC)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a name=&quot;ref-3&quot;&gt;&lt;/a&gt;[3] S. Fujieda, T. Harada, 2024. &lt;a href=&quot;https://gpuopen.com/download/2024_NeuralTextureBCCompression.pdf&quot;&gt;Neural Texture Block Compression&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a name=&quot;ref-4&quot;&gt;&lt;/a&gt;[4] L. Belcour, A. Benyoub, 2025. &lt;a href=&quot;https://arxiv.org/abs/2506.06040&quot;&gt;Hardware Accelerated Neural Block Texture Compression with Cooperative Vectors&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a name=&quot;ref-5&quot;&gt;&lt;/a&gt;[5] C. Weinreich, L. De Oliveira, A. Houdard, G. Nader, 2024. &lt;a href=&quot;https://gnader.github.io/publications/2024-neural-textures.html&quot;&gt;Real-Time Neural Materials using Block-Compressed Features&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a name=&quot;ref-6&quot;&gt;&lt;/a&gt;[6] Microsoft, 2019. &lt;a href=&quot;https://github.com/microsoft/DirectXShaderCompiler/wiki/Shader-Model-6.4&quot;&gt;HLSL Shader Model 6.4: Packed Dot Product Intrinsics&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a name=&quot;ref-7&quot;&gt;&lt;/a&gt;[7] Microsoft, 2021. &lt;a href=&quot;https://microsoft.github.io/DirectX-Specs/d3d/HLSL_SM_6_6_Pack_Unpack_Intrinsics.html&quot;&gt;HLSL Shader Model 6.6: Pack/Unpack Math Intrinsics&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a name=&quot;ref-8&quot;&gt;&lt;/a&gt;[8] Microsoft, 2025. &lt;a href=&quot;https://microsoft.github.io/hlsl-specs/proposals/0029-cooperative-vector/&quot;&gt;HLSL Specification 0029: Cooperative Vectors (Shader Model 6.9)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a name=&quot;ref-9&quot;&gt;&lt;/a&gt;[9] A. Patel (Microsoft), 2026. &lt;a href=&quot;https://devblogs.microsoft.com/directx/shader-model-6-10-agilitysdk-720-preview/&quot;&gt;Announcing Shader Model 6.10 Preview and AgilitySDK 720 Preview (LinAlg)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a name=&quot;ref-10&quot;&gt;&lt;/a&gt;[10] NVIDIA, 2026. &lt;a href=&quot;https://docs.nvidia.com/cuda/cuda-programming-guide/&quot;&gt;CUDA C++ Programming Guide: Warp Match and Warp Shuffle Functions&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a name=&quot;ref-11&quot;&gt;&lt;/a&gt;[11] D. Hendrycks, K. Gimpel, 2016. &lt;a href=&quot;https://arxiv.org/abs/1606.08415&quot;&gt;Gaussian Error Linear Units (GELUs)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a name=&quot;ref-12&quot;&gt;&lt;/a&gt;[12] C. E. Duchon, 1979. &lt;a href=&quot;https://doi.org/10.1175/1520-0450%281979%29018%3C1016:LFIOAT%3E2.0.CO;2&quot;&gt;Lanczos Filtering in One and Two Dimensions&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a name=&quot;ref-13&quot;&gt;&lt;/a&gt;[13] J. Jimenez, 2014. &lt;a href=&quot;https://advances.realtimerendering.com/s2014/&quot;&gt;Next Generation Post Processing in Call of Duty: Advanced Warfare&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a name=&quot;ref-14&quot;&gt;&lt;/a&gt;[14] L. Yang, S. Liu, M. Salvi, 2020. &lt;a href=&quot;https://research.nvidia.com/labs/rtr/publication/yang2020survey/&quot;&gt;A Survey of Temporal Antialiasing Techniques&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a name=&quot;ref-15&quot;&gt;&lt;/a&gt;[15] Microsoft, 2026. &lt;a href=&quot;https://github.com/microsoft/DirectXTex&quot;&gt;DirectXTex texture processing library&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a name=&quot;ref-16&quot;&gt;&lt;/a&gt;[16] N. Reed, 2012. &lt;a href=&quot;https://www.reedbeta.com/blog/understanding-bcn-texture-compression-formats/&quot;&gt;Understanding BCn Texture Compression Formats&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a name=&quot;ref-17&quot;&gt;&lt;/a&gt;[17] ambientCG, 2026. &lt;a href=&quot;https://ambientcg.com&quot;&gt;PBR materials&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a name=&quot;ref-18&quot;&gt;&lt;/a&gt;[18] F. Meinl, Intel, 2022. &lt;a href=&quot;https://www.intel.com/content/www/us/en/developer/topic-technology/graphics-research/samples.html&quot;&gt;GPU Research Samples&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a name=&quot;ref-19&quot;&gt;&lt;/a&gt;[19] Epic Games, 2026. &lt;a href=&quot;https://dev.epicgames.com/documentation/en-us/unreal-engine/texture-properties-in-unreal-engine&quot;&gt;Texture Properties in Unreal Engine&lt;/a&gt;&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Optimizing text rendering</title>
   <link href="https://somnathsarkar.io/2026/07/28/optimizing-text-rendering/"/>
   <updated>2026-07-28T00:00:00-07:00</updated>
   <id>https://somnathsarkar.io/2026/07/28/optimizing-text-rendering</id>
   <content type="html">&lt;p&gt;&lt;a href=&quot;/assets/lexkit/header.webp&quot;&gt;&lt;img src=&quot;/assets/lexkit/header.webp&quot; alt=&quot;Header&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While getting my text layout system, &lt;a href=&quot;https://github.com/somnathsarkar/lexkit&quot;&gt;lexkit&lt;/a&gt;, ready for a public release I wanted to make sure performance was as fast as possible. This is a breakdown of the major steps we took to optimize performance and lay out the entire Project Gutenberg text of &lt;em&gt;The Odyssey&lt;/em&gt; 6.5 times faster than the Unicode Consortium’s implementation ICU [&lt;a href=&quot;#ref-1&quot;&gt;1&lt;/a&gt;].&lt;/p&gt;

&lt;blockquote&gt;
  &lt;h3 id=&quot;table-of-contents&quot;&gt;Table of Contents&lt;/h3&gt;
  &lt;ol&gt;
    &lt;li&gt;&lt;a href=&quot;#sec-performance-and-profiling&quot;&gt;Performance and Profiling&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#sec-memory-allocation&quot;&gt;Memory Allocation&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#sec-api-design-and-caching&quot;&gt;API Design and Caching&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#sec-efficient-utf-8-decoding&quot;&gt;Efficient UTF-8 decoding&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#sec-fast-paths&quot;&gt;Fast Paths&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#sec-data-structures-the-two-step-table&quot;&gt;Data structures: The Two-Step table&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#sec-baking-unicode-specification&quot;&gt;Baking Unicode Specification&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#sec-simd-instructions&quot;&gt;SIMD instructions&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#sec-better-algorithms-for-bidi-reversals&quot;&gt;Better Algorithms for BiDi reversals&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#sec-multithreading&quot;&gt;Multithreading&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#sec-conclusions-and-future-work&quot;&gt;Conclusions and Future Work&lt;/a&gt;&lt;/li&gt;
  &lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a id=&quot;sec-performance-and-profiling&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;performance-and-profiling&quot;&gt;Performance and Profiling&lt;/h2&gt;

&lt;p&gt;Lexkit is a text layout framework capable of handling the following steps of the text rendering pipeline, divided into the stages we use to measure them:&lt;/p&gt;

&lt;h3 id=&quot;pre-processing&quot;&gt;Pre-processing&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;Reading a Unicode specification and transforming it into lookup tables.&lt;/li&gt;
  &lt;li&gt;Baking font data to a texture.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;per-text-operations&quot;&gt;Per-text operations&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;Decoding UTF-8 data to 32-bit codepoints.&lt;/li&gt;
  &lt;li&gt;Identifying and splitting paragraphs.&lt;/li&gt;
  &lt;li&gt;Performing Bidirectional (BiDi) text handling.&lt;/li&gt;
  &lt;li&gt;Shaping text: Converting codepoints to glyph indices.&lt;/li&gt;
  &lt;li&gt;Identifying break opportunities at grapheme, word and line levels.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;per-layout-operations&quot;&gt;Per-layout operations&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;Fitting text into boxes with fixed width, performing hyphenation as appropriate.&lt;/li&gt;
  &lt;li&gt;Outputting vertex buffers for rendering.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href=&quot;/assets/lexkit/phase_pies.webp&quot;&gt;&lt;img src=&quot;/assets/lexkit/phase_pies.webp&quot; alt=&quot;Pie Chart&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We are primarily concerned with the last two categories, since the pre-processing steps can be baked offline and don’t incur a runtime cost. We measure this against the de-facto reference Unicode Implementation, ICU [&lt;a href=&quot;#ref-1&quot;&gt;1&lt;/a&gt;]. ICU is concerned with performing Unicode operations like BiDi and text breaking and doesn’t have any shaping utilities. To compare them, we use HarfBuzz functions to complete these stages of the pipeline. HarfBuzz [&lt;a href=&quot;#ref-14&quot;&gt;14&lt;/a&gt;] is the industry standard for text shaping and is used as part of lexkit as well, so this is a fair comparison.&lt;/p&gt;

&lt;h3 id=&quot;measurement-setup&quot;&gt;Measurement setup&lt;/h3&gt;

&lt;p&gt;To measure performance, we use both CPU profiling tools and instrumenting our code. The Intel VTune Profiler [&lt;a href=&quot;#ref-12&quot;&gt;12&lt;/a&gt;] is a common tool that we’ll use to measure CPU utilization, memory bandwidth, L1/L2 cache hit rates and hotspots. We will use the Win32 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;QueryPerformanceCounter&lt;/code&gt; group of functions to compute timestamps and measure the execution times of individual functions.&lt;/p&gt;

&lt;p&gt;We measure performance on &lt;strong&gt;example03&lt;/strong&gt; in the lexkit examples folder (&lt;a href=&quot;https://github.com/somnathsarkar/lexkit/blob/master/examples/example03.c&quot;&gt;Source&lt;/a&gt;), which lays out the entire text of The Odyssey from Project Gutenberg [&lt;a href=&quot;#ref-15&quot;&gt;15&lt;/a&gt;] (0.75 MB) at real-time rates. It uses our OpenGL renderer (&lt;a href=&quot;https://github.com/somnathsarkar/lexkit/blob/master/examples/example_opengl.c&quot;&gt;Source&lt;/a&gt;) on a 64-bit Windows 11 machine using an Intel i7-12700K CPU and NVIDIA RTX 4080 GPU.&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;sec-memory-allocation&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;memory-allocation&quot;&gt;Memory Allocation&lt;/h2&gt;

&lt;p&gt;It is extremely difficult to handle arbitrary fonts and lay them out into dynamic dimensions without some degree of runtime memory allocation. Our best bet is to use a performant allocator and avoid using OS utilities like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;VirtualAlloc&lt;/code&gt; for every single minor allocation. For lexkit, we have additional constraints: the allocator should behave well with multiple threads and should accommodate allocations and deallocations at lifetimes that correspond to text rendering.&lt;/p&gt;

&lt;p&gt;This is a good choice for a linear allocator, specifically an Arena [&lt;a href=&quot;#ref-9&quot;&gt;9&lt;/a&gt;]. Arena allocators work by grabbing a fixed amount of memory, then allocating and freeing blocks using stack-based lifetimes. In other words allocations are last-in, first-out (LIFO). Arenas expose allocation via the following API:&lt;/p&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Arena API&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;LkArena&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;lkArenaCreate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;lkArenaDestroy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LkArena&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arena&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;lkArenaPush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LkArena&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arena&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint64_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sz&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint64_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aln&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;         &lt;span class=&quot;c1&quot;&gt;// Zero-initialized&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;lkArenaPushNZ&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LkArena&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arena&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint64_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sz&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint64_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aln&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;       &lt;span class=&quot;c1&quot;&gt;// Uninitialized&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;uint64_t&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;lkArenaGetPos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LkArena&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arena&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;lkArenaRestore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LkArena&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arena&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint64_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;                    &lt;span class=&quot;c1&quot;&gt;// Free everything pushed since pos&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Helper macros&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;#define APush(arena, tp) (tp*)lkArenaPush((arena), sizeof(tp), _Alignof(tp))
#define APushArray(arena, tp, count) (tp*)lkArenaPushArray((arena), sizeof(tp), _Alignof(tp), (count))
#define APushNZ(arena, tp) (tp*)lkArenaPushNZ((arena), sizeof(tp), _Alignof(tp))
#define APushArrayNZ(arena, tp, count) (tp*)lkArenaPushArrayNZ((arena), sizeof(tp), _Alignof(tp), (count))
&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// Typical usage: scratch is an LkArena&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;uint64_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;frame_pos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lkArenaGetPos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;scratch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;LkLine&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lines&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;APushArrayNZ&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;scratch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LkLine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;max_lines&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;int32_t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;codepoint_orders&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;APushArrayNZ&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;scratch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int32_t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;line_codepoint_count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// Use variables in scratch&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;lkArenaRestore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;scratch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;frame_pos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Managing memory ourselves using arenas is far more performant than relying on the CRT. For the below comparison, we replace all arena allocations with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;malloc&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;realloc&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;free&lt;/code&gt; CRT calls; the performance benefit is clear. Pure C libraries also typically use the convention that zero-initialized memory is a valid initial state for a struct instance since we don’t have access to constructors. However, if it can be avoided, it helps to clearly denote which objects require zero-initialization and which don’t. Expressly providing allocations that don’t set memory to zero instead of guaranteeing it gave us a small performance win.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/assets/lexkit/alloc_bars.webp&quot;&gt;&lt;img src=&quot;/assets/lexkit/alloc_bars.webp&quot; alt=&quot;Allocator variants&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We make the following modifications to arenas for our use in a text layout system.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;One issue with the simplest form of arenas is that we have to preallocate the block of memory ahead of time or use a complex scheme every time we want to grow the arena. This may involve either reallocating the entire block of memory, or using a linked list of blocks to accommodate new data. A much easier approach is to use Win32’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;VirtualAlloc&lt;/code&gt; [&lt;a href=&quot;#ref-10&quot;&gt;10&lt;/a&gt;] to reserve a large block of memory (say 16 GB) up front, and only commit when needed. This exploits the ability of MMUs to supply a large virtual address space up front which is mapped to actual memory only once needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;#define LK_ARENA_RESERVE (1024llu * 1024llu * 1024llu * 16)   // 16 GB
#define LK_ARENA_CHUNK (1024llu * 1024llu)                    // 1 MB
&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;Reserve&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;uint64_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;VirtualAlloc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MEM_RESERVE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PAGE_NOACCESS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bool&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;Commit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ptr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint64_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;VirtualAlloc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ptr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MEM_COMMIT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PAGE_READWRITE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;LkArena&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;lkArenaCreate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;LkArena&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arena&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;calloc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;sizeof&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LkArena&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;arena&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;reserved&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LK_ARENA_RESERVE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;arena&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;committed&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;arena&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Reserve&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LK_ARENA_RESERVE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arena&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;ArenaCommit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LkArena&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arena&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint64_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos_required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;uint64_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos_target&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LK_ARENA_CHUNK&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos_required&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LK_ARENA_CHUNK&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LK_ARENA_CHUNK&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;Commit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;char&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arena&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arena&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;committed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos_target&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arena&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;committed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;arena&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;committed&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos_target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;ArenaPushRaw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LkArena&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arena&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint64_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sz&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint64_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aln&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;uint64_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos_aln&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arena&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aln&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aln&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aln&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;uint64_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos_target&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos_aln&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sz&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos_target&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arena&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;committed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;ArenaCommit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arena&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos_target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;arena&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos_target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;char&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arena&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos_aln&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;Text rendering involves multiple disjoint data lifetimes:
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Fonts&lt;/strong&gt;: data that belongs to a specific font family and styles, e.g. a font atlas, per-glyph dimensions, metadata, etc.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Text&lt;/strong&gt;: data that belongs to a piece of text, e.g. line break candidate locations, shaped glyph indices, etc.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Layout&lt;/strong&gt;: data that belongs to a piece of text that has been organized to be rendered on screen, e.g. actual line break locations.&lt;/li&gt;
    &lt;/ul&gt;

    &lt;p&gt;These lifetimes can be individually managed by separate arenas. For example, we create an arena for a specific piece of text, then as we move through the text rendering pipeline, all allocations that belong to that lifetime (break candidates, BiDi run information, etc.) will simply be pushed onto the arena.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;Following on from that, in multi-threading mode each worker thread has a thread-specific Arena. This allows us to avoid synchronization along the allocator path. To implement this, we have an array of arenas as part of our lexkit context, which is indexed into with the worker thread id.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// One arena per worker thread + one for the main thread&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;lkCreateContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LkUnicodeData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ud&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_workers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LkAllocator&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;alloc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LkContext&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;o_ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;o_ctx&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;queue&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lkJobQueueCreate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;alloc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_workers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;o_ctx&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;worker_scratch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;AAllocArray&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;o_ctx&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;alloc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LkArena&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_workers&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_workers&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;o_ctx&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;worker_scratch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lkArenaCreateFrom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;o_ctx&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;alloc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Jobs execute on worker threads and index into the arena array with their id&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;ShapeJob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;worker_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;ShapeJobData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sdata&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;LkArena&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;scratch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sdata&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;worker_scratch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;worker_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;LkGlyph&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;run_glyphs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;APushArrayNZ&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;scratch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LkGlyph&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;glyph_count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a id=&quot;sec-api-design-and-caching&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;api-design-and-caching&quot;&gt;API Design and Caching&lt;/h2&gt;

&lt;p&gt;It helps to simply do less work rather than doing the same work faster. A text rendering system may have to operate under the following scenarios:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Rendering entirely pre-determined text in a pre-determined layout, e.g. static text in a game UI, closed captions, etc.&lt;/li&gt;
  &lt;li&gt;Rendering entirely pre-determined text in a layout that changes per-frame, e.g. static text in a resizable window.&lt;/li&gt;
  &lt;li&gt;Rendering text that changes per-frame in a layout that may or may not change per-frame, e.g. editable text in an editor.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Lexkit doesn’t cache any data by default, but exposes an API that allows applications to manage their own caches:&lt;/p&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Context: Cache this as long as we make calls to lexkit.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;LkContext&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;lkCreateContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;g_lk_unicode_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_workers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Font: Cache this as long as font remains relevant&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;LkFont&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;font&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;lkCreateFont&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;C:/Windows/Fonts/arial.ttf&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;font&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Per-text operations: UTF-8 decode, BiDi, shaping, break computation, etc.&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;//  Cache this object if the text remains unchanged.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;LkText&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;lkCreateText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;font&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;utf8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;utf8_len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Per-layout operations: line fitting, reordering, positioning, etc.&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;//  Cache this object if the text AND rect it&apos;s laid out into remains unchanged.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;LkVertexDescriptor_Text&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;quads&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MAX_QUADS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;int32_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;quad_count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;lkLayoutText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;font&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;scroll_y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MAX_QUADS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;quads&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quad_count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a id=&quot;sec-efficient-utf-8-decoding&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;efficient-utf-8-decoding&quot;&gt;Efficient UTF-8 decoding&lt;/h2&gt;

&lt;p&gt;Lexkit accepts text in the UTF-8 encoding, and thankfully the process of decoding UTF-8 to 32-bit Unicode codepoints is well-covered with a variety of approaches [&lt;a href=&quot;#ref-8&quot;&gt;8&lt;/a&gt;]. We use the simple lookup table approach originally from Bjoern Höhrmann [&lt;a href=&quot;#ref-7&quot;&gt;7&lt;/a&gt;].&lt;/p&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// &quot;Flexible and Economical UTF-8 Decoder&quot; by Bjoern Hoehrmann&lt;/span&gt;

&lt;span class=&quot;cp&quot;&gt;#define UTF8_ACCEPT 0
#define UTF8_REJECT 1
&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint8_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;utf8d&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 00..1f&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;mh&quot;&gt;0xb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// f0..ff&lt;/span&gt;
  &lt;span class=&quot;mh&quot;&gt;0x0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0x1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// s0..s0&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// s7..s8&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;inline&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint32_t&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;Utf8DecodeStep&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;uint32_t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint32_t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;codep&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint32_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;uint32_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;utf8d&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;

  &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;codep&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UTF8_ACCEPT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;byte&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x3fu&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;codep&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mh&quot;&gt;0xff&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;utf8d&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;256&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;16&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Additionally, we have a fast path for ASCII characters that can decode 8 codepoints at the same time, shown below. These two optimizations together bring &lt;em&gt;The Odyssey&lt;/em&gt; decode times down from &lt;strong&gt;19 ms&lt;/strong&gt; to &lt;strong&gt;0.8 ms&lt;/strong&gt;.&lt;/p&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint64_t&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;Utf8Decode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint8_t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint64_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint32_t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;o_codepoints&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UTF8_ACCEPT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;// Fast path for ASCII characters&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;uint64_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;memcpy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;chunk&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x8080808080808080llu&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;o_codepoints&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;k&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;k&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;k&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;o_codepoints&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;k&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;k&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;start&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Lookup table that supports longer codepoints&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;Utf8DecodeStep&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a id=&quot;sec-fast-paths&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;fast-paths&quot;&gt;Fast Paths&lt;/h2&gt;

&lt;p&gt;On the subject of fast paths, it is extremely beneficial to identify the most common case for each stage of the pipeline and have a specialized code path to deal with it. While this can be dependent on the language, locale and script you expect to deal with most commonly, at the absolute minimum including special code paths for the simplest case is almost always a huge win. All of these results are measured using single-threaded mode.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Pipeline Stage&lt;/th&gt;
      &lt;th&gt;Fast Path&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Before&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;After&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;UTF-8 Decoding&lt;/td&gt;
      &lt;td&gt;Load 8 consecutive bytes in a 64-bit integer, test for ASCII and copy over if test passed&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;19 ms&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.8 ms&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;BiDi Classes&lt;/td&gt;
      &lt;td&gt;Look up bracket pairs only for codepoints of class &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Other Neutral&lt;/code&gt;. Every other class is never a bracket.&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;31.9 ms&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;5.0 ms&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;BiDi Level Runs&lt;/td&gt;
      &lt;td&gt;Skip the majority of rules if the paragraph contains no RTL or numeric characters&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;14.5 ms&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;2.5 ms&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Break Analysis&lt;/td&gt;
      &lt;td&gt;Between two ASCII letters there is never a line, word or grapheme break.&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;24.2 ms&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;20.7 ms&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Reordering &amp;amp; Positioning&lt;/td&gt;
      &lt;td&gt;Emit quads only for lines inside the viewport and stop at the first line below it&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;3.1 ms per frame&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.02 ms per frame&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;a id=&quot;sec-data-structures-the-two-step-table&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;data-structures-the-two-step-table&quot;&gt;Data structures: The Two-Step table&lt;/h2&gt;

&lt;p&gt;Most Unicode algorithms (Line breaking [&lt;a href=&quot;#ref-5&quot;&gt;5&lt;/a&gt;] [&lt;a href=&quot;#ref-6&quot;&gt;6&lt;/a&gt;], BiDi handling [&lt;a href=&quot;#ref-4&quot;&gt;4&lt;/a&gt;], etc.) involve classifying codepoints into Unicode classes. This is given by specification files [&lt;a href=&quot;#ref-3&quot;&gt;3&lt;/a&gt;] that map ranges of codepoints to classes. Each specification file contains these mappings for a specific property, for example here’s what some of these files look like:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# LineBreak-16.0.0.txt
# @missing: 0000..10FFFF; XX

0020           ; SP # Zs         SPACE
0028           ; OP # Ps         LEFT PARENTHESIS
0029           ; CP # Pe         RIGHT PARENTHESIS
002D           ; HY # Pd         HYPHEN-MINUS
0041..005A     ; AL # Lu    [26] LATIN CAPITAL LETTER A..LATIN CAPITAL LETTER Z
...
05D0..05EA     ; HL # Lo    [27] HEBREW LETTER ALEF..HEBREW LETTER TAV
...
4E00..9FFF     ; ID # Lo [20992] CJK UNIFIED IDEOGRAPH-4E00..CJK UNIFIED IDEOGRAPH-9FFF
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Of course, looking up all these codepoints naively results in a totally unacceptable runtime. The typical strategy is to compress the mapping into a data structure called a Two-Step table [&lt;a href=&quot;#ref-2&quot;&gt;2&lt;/a&gt;] [&lt;a href=&quot;#ref-16&quot;&gt;16&lt;/a&gt;]. To construct a Two-Step table, we first divide the overall codepoint range, $U = 1,114,112$ into blocks of size $N=256$ resulting in $K=\lceil U/N\rceil =4352 $ blocks. The specification contains a mapping to Unicode classes for these $N$ consecutive codepoints. If we look at all $K$ sequences of $N$ consecutive mappings, we note that there are $M$ unique sequences with $M \ll K$. This is because there are large sequences of codepoints that belong to the same class.&lt;/p&gt;

&lt;p&gt;Two-Step tables exploit this property. A Two-Step table consists of two lookup tables. The upper level table translates codepoints into indices of the second table. The second table contains $M$ unique sequences of mappings described above. For Unicode classes, this is far more runtime and memory efficient than an off-the-shelf hashmap.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/assets/lexkit/twostep_table.webp&quot;&gt;&lt;img src=&quot;/assets/lexkit/twostep_table.webp&quot; alt=&quot;Two-Step table&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can see the impact of Two-Step tables on lookup performance for our Odyssey example by cumulatively switching each individual property from a linear lookup to a Two-Step table, as the chart below shows:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/assets/lexkit/twostep_bars.webp&quot;&gt;&lt;img src=&quot;/assets/lexkit/twostep_bars.webp&quot; alt=&quot;Two-Step tables per property&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;sec-baking-unicode-specification&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;baking-unicode-specification&quot;&gt;Baking Unicode Specification&lt;/h2&gt;

&lt;p&gt;Lexkit exposes the ability to load arbitrary specification files, essentially future-proofing against new Unicode versions. This allows us to build the Two-Step tables for any version of Unicode at runtime.&lt;/p&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;LkUnicodeSpecInfo&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str_path_lb&lt;/span&gt;     &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;ucd/LineBreak.txt&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str_path_wb&lt;/span&gt;     &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;ucd/auxiliary/WordBreakProperty.txt&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str_path_gb&lt;/span&gt;     &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;ucd/auxiliary/GraphemeBreakProperty.txt&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str_path_gc&lt;/span&gt;     &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;ucd/UnicodeData.txt&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str_path_eaw&lt;/span&gt;    &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;ucd/EastAsianWidth.txt&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str_path_incb&lt;/span&gt;   &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;ucd/DerivedCoreProperties.txt&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str_path_ep&lt;/span&gt;     &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;ucd/emoji/emoji-data.txt&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str_path_bidi&lt;/span&gt;   &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;ucd/extracted/DerivedBidiClass.txt&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str_path_bidipb&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;ucd/BidiBrackets.txt&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;LkUnicodeData&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ud&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lkTryLoadUnicodeDataFromSpec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arena&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ud&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;LkContext&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;lkCreateContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ud&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_workers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;NULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Realistically though, it’s preferable to select the latest Unicode version and build against that. To avoid paying the Two-Step construction time at startup, we bake all the data to a header. When I started work on lexkit, this was Unicode 16, so adding it as a header &lt;a href=&quot;https://github.com/somnathsarkar/lexkit/blob/a9bf1185b4bac32eb6994d20a34cd3a906f0aaa9/lexkit/unicode_data_16.h&quot;&gt;here&lt;/a&gt; skips this cost entirely while increasing the source code size by ~650 KB, and the linked binary by 1 MB.&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;sec-simd-instructions&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;simd-instructions&quot;&gt;SIMD instructions&lt;/h2&gt;

&lt;p&gt;Using the microarchitecture exploration report from Intel VTune, we observed that one of the hotspots for our Odyssey example was in performing Two-Step table lookups over the text data. VTune indicates that the retirement rate is high:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/assets/lexkit/vtune_twostep_lookup.webp&quot;&gt;&lt;img src=&quot;/assets/lexkit/vtune_twostep_lookup.webp&quot; alt=&quot;VTune LkTwoStepLookup&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;which indicates that reducing the number of instructions is a natural way to proceed. These lookups are independent of each other and so can be easily split up to multithreaded jobs. To improve single-threaded performance (which also helps with multithreaded performance), we can use SIMD instructions instead. Here’s an example with AVX2 [&lt;a href=&quot;#ref-11&quot;&gt;11&lt;/a&gt;]:&lt;/p&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;#define BLOCK_P2 8
#define BLOCK_SIZE (1 &amp;lt;&amp;lt; BLOCK_P2)
#define BLOCK_MASK ((1 &amp;lt;&amp;lt; BLOCK_P2) - 1)
&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;int32_t&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;LkTwoStepLookup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LkTwoStep&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint32_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;uint32_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;block0_i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;BLOCK_SIZE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;uint32_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;block1_i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ts&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block0_i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;BLOCK_SIZE&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;BLOCK_SIZE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ts&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block1_i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;__m128i&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;LkTwoStepLookupAvx2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LkTwoStep&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__m128i&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__m128i&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;block_mask&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_mm_set1_epi32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;BLOCK_MASK&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;__m128i&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;block0_i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_mm_srli_epi32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;BLOCK_P2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;__m128i&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;block1_idx&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_mm_i32gather_epi32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ts&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;block0_i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;__m128i&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;block1_start&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_mm_slli_epi32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block1_idx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;BLOCK_P2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;__m128i&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;block1_offset&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_mm_and_epi32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;block_mask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;__m128i&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;block1_i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_mm_add_epi32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block1_start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;block1_offset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;__m128i&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lk_result&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_mm_i32gather_epi32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ts&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;block1_i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lk_result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;GetGlyphAtIndexAvx2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint32_t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;codepoints&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int32_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;len_codepoints&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int32_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;idx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LkUnicodeData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ud&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Glyph&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;o_glyphs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// Get codepoint&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;__m128i&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;idx_vec&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_mm_loadu_epi32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;idx_arr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;__m128i&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;codepoint_vec&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_mm_i32gather_epi32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;codepoints&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;idx_vec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;__m128i&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lbc_vec&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LkTwoStepLookupAvx2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ud&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ts_lb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;codepoint_vec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;__m128i&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;wbc_vec&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LkTwoStepLookupAvx2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ud&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ts_wb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;codepoint_vec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;__m128i&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gbc_vec&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LkTwoStepLookupAvx2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ud&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ts_gb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;codepoint_vec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;__m128i&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gc_vec&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LkTwoStepLookupAvx2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ud&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ts_gc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;codepoint_vec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;__m128i&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;eaw_vec&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LkTwoStepLookupAvx2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ud&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ts_eaw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;codepoint_vec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;__m128i&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;incb_vec&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LkTwoStepLookupAvx2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ud&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ts_incb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;codepoint_vec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;__m128i&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;extended_pictographic_vec&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LkTwoStepLookupAvx2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ud&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ts_ep&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;codepoint_vec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This improves performance significantly, reducing break computations in single-threaded mode by &lt;strong&gt;12.7 ms&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;sec-better-algorithms-for-bidi-reversals&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;better-algorithms-for-bidi-reversals&quot;&gt;Better Algorithms for BiDi reversals&lt;/h2&gt;

&lt;p&gt;The BiDi reordering step of the pipeline involves successively reversing large sections of text. See my description in the previous article here &lt;a href=&quot;/2025/04/30/text-rendering-pipeline/&quot;&gt;A trip through the text rendering pipeline&lt;/a&gt;. The Unicode standard [&lt;a href=&quot;#ref-4&quot;&gt;4&lt;/a&gt;] allows for $K=125$ maximum levels of BiDi nestedness, so this is $O(NK)$ by default, far above what’s ideal in a real-time setting. By preprocessing the BiDi levels, we can get a far faster algorithm where each codepoint is moved at most once:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/assets/lexkit/bidi_reversal_tree.webp&quot;&gt;&lt;img src=&quot;/assets/lexkit/bidi_reversal_tree.webp&quot; alt=&quot;BiDi reordering&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This has a powerful effect on The Odyssey example’s performance, reducing it by 32 ms per frame, or 4.3 times faster than the naive BiDi reordering stage.&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;sec-multithreading&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;multithreading&quot;&gt;Multithreading&lt;/h2&gt;

&lt;p&gt;As we explained in the previous article, text layout can be divided into a number of hierarchical steps: Paragraphs, BiDi runs, lines, codepoints, etc. As a result, converting optimized single-threaded code into multithreaded code quickly becomes a no-brainer. By default, lexkit operates in single-threaded mode; however, you can specify the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;num_workers&lt;/code&gt; parameter at context creation time to create a multithreaded context.&lt;/p&gt;

&lt;p&gt;We use a lockless job queue in lexkit. This means that there are no mutexes or locks used to synchronize threads, in favor of a simplified architecture that only uses atomics and semaphores. The benefit of this format is that it’s guaranteed to make progress without the possibility of deadlocks. Here’s what the job API looks like:&lt;/p&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;typedef&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LkJobFunc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;worker_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;typedef&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;LkJobFunc&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;func&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LkJob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;LkJobQueue&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;lkJobQueueCreate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LkAllocator&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;alloc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_workers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;lkJobQueuePush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LkJobQueue&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;queue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LkJob&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;job&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;lkJobQueueWait&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LkJobQueue&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;queue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;lkJobQueueDestroy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LkJobQueue&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;queue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;For example, this is how BiDi classes are computed for every codepoint:&lt;/p&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;typedef&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sz&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LkUnicodeData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ud&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint32_t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;codepoints&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;BidiUnit&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;o_units&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ComputeBidiUnitData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;ComputeBidiUnitJob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;worker_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;ComputeBidiUnitData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cdata&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cdata&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cdata&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cdata&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sz&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;cdata&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;o_units&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;BidiUnitCreate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cdata&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;codepoints&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cdata&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ud&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;lkComputeBidiUnits&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;LkContext&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;LkArena&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arena&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint32_t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;codepoints&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;int32_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;len_codepoints&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;BidiUnit&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;**&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;o_units&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;int32_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;work_chunk&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;len_codepoints&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;queue&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num_workers&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;work_chunk&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;16384&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;work_chunk&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;16384&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;int32_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_chunks&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;len_codepoints&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;work_chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;len_codepoints&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;work_chunk&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;ComputeBidiUnitData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;job_data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;APushArrayNZ&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;scratch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ComputeBidiUnitData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_chunks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;chunk_i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int32_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;len_codepoints&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;work_chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;job_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;chunk_i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;job_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;chunk_i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sz&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;work_chunk&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;len_codepoints&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;len_codepoints&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;work_chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;job_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;chunk_i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ud&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ud&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;job_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;chunk_i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;codepoints&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;codepoints&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;job_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;chunk_i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;o_units&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;o_units&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;LkJob&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;job&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;job&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ComputeBidiUnitJob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;job&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;job_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;chunk_i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;chunk_i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;lkJobQueuePush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;queue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;job&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;lkJobQueueWait&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;queue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Splitting up the work among jobs is simple. For trivially parallel tasks like looking up Unicode classes from codepoints, this is a simple matter of dividing the entire text corpus into blocks then submitting the ranges into the job queue. For tasks that involve variable output, such as converting a list of codepoints into BiDi runs, we need two passes. The first outputs the results for each job to a worker-local arena. Then we perform a prefix sum on the main thread and dispatch the second stage that copies the results to a shared central array.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/assets/lexkit/two_pass_dispatch.webp&quot;&gt;&lt;img src=&quot;/assets/lexkit/two_pass_dispatch.webp&quot; alt=&quot;Two Pass Dispatch&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Of course, multithreading represents the biggest potential for performance improvement. Our single-threaded performance is already superior to Unicode’s reference implementation which is entirely single-threaded by design. At &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;num_workers &amp;gt;= 1&lt;/code&gt;, we handily beat them:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/assets/lexkit/workers_bars.webp&quot;&gt;&lt;img src=&quot;/assets/lexkit/workers_bars.webp&quot; alt=&quot;Setup time vs num_workers&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We notice that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;num_workers&lt;/code&gt; over 8 does not help with performance further on this text corpus for our machine. The Intel i7-12700K has 8 Performance cores and 4 Efficiency cores, which support 20 threads in total. At each multithreaded stage, the total input data is divided into chunks which are assigned to jobs. The number of workers and chunks is related by the formula: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chunk_size = max(len / ((num_workers + 1) * 4), 16384)&lt;/code&gt;. As chunk size increases, the number of jobs decreases and work done as part of an individual job increases. There’s a tradeoff between the overhead associated with a job and the parallel speedup offered by multiple workers [&lt;a href=&quot;#ref-13&quot;&gt;13&lt;/a&gt;], so increasing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;num_workers&lt;/code&gt; beyond a certain threshold will likely lead to performance degradation rather than improvement. Our formula for chunk size limits the maximum number of jobs so instead of degradation we see stagnation at higher worker counts.&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;sec-conclusions-and-future-work&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;conclusions-and-future-work&quot;&gt;Conclusions and Future Work&lt;/h2&gt;

&lt;p&gt;This covers the major steps along the way of getting lexkit to where it is today, but there were many smaller steps that helped bring runtimes down and many things that could still be improved. I encourage readers to take a look at the project on &lt;a href=&quot;https://github.com/somnathsarkar/lexkit&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here are some things that I would like to look at in the future, given time:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;SIMD instructions could be extended to far more areas than just Two-Step table lookup. It’s likely that Line Breaking and the Bidirectional Text Algorithm could also be accelerated by vectorized instructions. Currently codepoints and their associated data are stored in an Array-of-Structs (AoS) format. It’s likely that moving to a Structure-of-Arrays format would be necessary for further progress in this area.&lt;/li&gt;
  &lt;li&gt;This optimization process was entirely contained to the CPU since the GPU side of rendering quads offers little opportunity for significant performance wins. However, it’s clear that large sections of the text rendering pipeline could be ported to compute shaders. As far as I know this isn’t done by any major text layout library and would be interesting to pursue.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;p&gt;&lt;a id=&quot;ref-1&quot;&gt;&lt;/a&gt;[1] Unicode Consortium. &lt;a href=&quot;https://icu.unicode.org/&quot;&gt;ICU - International Components for Unicode&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;ref-2&quot;&gt;&lt;/a&gt;[2] Unicode Consortium. &lt;a href=&quot;https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-5/&quot;&gt;The Unicode Standard, Core Specification, Chapter 5: Implementation Guidelines&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;ref-3&quot;&gt;&lt;/a&gt;[3] Unicode Consortium. &lt;a href=&quot;https://www.unicode.org/reports/tr44/&quot;&gt;Unicode Standard Annex #44: Unicode Character Database&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;ref-4&quot;&gt;&lt;/a&gt;[4] Unicode Consortium. &lt;a href=&quot;https://www.unicode.org/reports/tr9/&quot;&gt;Unicode Standard Annex #9: Unicode Bidirectional Algorithm&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;ref-5&quot;&gt;&lt;/a&gt;[5] Unicode Consortium. &lt;a href=&quot;https://www.unicode.org/reports/tr14/&quot;&gt;Unicode Standard Annex #14: Unicode Line Breaking Algorithm&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;ref-6&quot;&gt;&lt;/a&gt;[6] Unicode Consortium. &lt;a href=&quot;https://www.unicode.org/reports/tr29/&quot;&gt;Unicode Standard Annex #29: Unicode Text Segmentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;ref-7&quot;&gt;&lt;/a&gt;[7] Bjoern Hoehrmann, 2010. &lt;a href=&quot;https://bjoern.hoehrmann.de/utf-8/decoder/dfa/&quot;&gt;Flexible and Economical UTF-8 Decoder&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;ref-8&quot;&gt;&lt;/a&gt;[8] John Keiser and Daniel Lemire, 2021. &lt;a href=&quot;https://arxiv.org/abs/2010.03090&quot;&gt;Validating UTF-8 In Less Than One Instruction Per Byte&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;ref-9&quot;&gt;&lt;/a&gt;[9] Ryan Fleury, 2022. &lt;a href=&quot;https://www.rfleury.com/p/untangling-lifetimes-the-arena-allocator&quot;&gt;Untangling Lifetimes: The Arena Allocator&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;ref-10&quot;&gt;&lt;/a&gt;[10] Microsoft. &lt;a href=&quot;https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc&quot;&gt;VirtualAlloc function (memoryapi.h)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;ref-11&quot;&gt;&lt;/a&gt;[11] Intel. &lt;a href=&quot;https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html&quot;&gt;Intel Intrinsics Guide&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;ref-12&quot;&gt;&lt;/a&gt;[12] Intel. &lt;a href=&quot;https://www.intel.com/content/www/us/en/docs/vtune-profiler/user-guide/current/overview.html&quot;&gt;Intel VTune Profiler User Guide&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;ref-13&quot;&gt;&lt;/a&gt;[13] Gene M. Amdahl, 1967. &lt;a href=&quot;https://doi.org/10.1145/1465482.1465560&quot;&gt;Validity of the Single Processor Approach to Achieving Large Scale Computing Capabilities&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;ref-14&quot;&gt;&lt;/a&gt;[14] &lt;a href=&quot;https://harfbuzz.github.io/&quot;&gt;HarfBuzz Manual&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;ref-15&quot;&gt;&lt;/a&gt;[15] Homer, translated by Alexander Pope. &lt;a href=&quot;https://www.gutenberg.org/ebooks/3160&quot;&gt;The Odyssey&lt;/a&gt;, Project Gutenberg eBook #3160&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;ref-16&quot;&gt;&lt;/a&gt;[16] Peter Kankowski, 2006. &lt;a href=&quot;https://www.strchr.com/multi-stage_tables&quot;&gt;Two-stage tables for storing Unicode character properties&lt;/a&gt;&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>A trip through the text rendering pipeline</title>
   <link href="https://somnathsarkar.io/2025/04/30/text-rendering-pipeline/"/>
   <updated>2025-04-30T00:00:00-07:00</updated>
   <id>https://somnathsarkar.io/2025/04/30/text-rendering-pipeline</id>
   <content type="html">&lt;p&gt;&lt;a href=&quot;/assets/lexkit/pipeline.webp&quot;&gt;&lt;img src=&quot;/assets/lexkit/pipeline.webp&quot; alt=&quot;Pipeline&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Text rendering is a major pain point while developing UIs or editors for graphics applications. Moving beyond simple monospaced, single-line ASCII text can be incredibly complicated. This article attempts to document the sequence of steps to go from an array of Unicode text data to letter shapes on screen as simply as possible. I’ll use &lt;a href=&quot;https://github.com/somnathsarkar/lexkit&quot;&gt;lexkit&lt;/a&gt;, a text layout library I wrote, as a point of reference throughout. We’ll cover different encoding schemes, Bidirectional (BiDi) text, Unicode-compliant line and word breaking algorithms, hyphenation schemes and more.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;h3 id=&quot;table-of-contents&quot;&gt;Table of Contents&lt;/h3&gt;
  &lt;ol&gt;
    &lt;li&gt;&lt;a href=&quot;#sec-rendering-text&quot;&gt;Rendering Text&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#sec-text-encodings&quot;&gt;Text Encodings&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#sec-paragraph-splitting&quot;&gt;Paragraph Splitting&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#sec-bidirectional-text&quot;&gt;Bidirectional Text&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#sec-text-shaping&quot;&gt;Text Shaping&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#sec-line-word-and-grapheme-breaks&quot;&gt;Line, Word and Grapheme Breaks&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#sec-hyphenation&quot;&gt;Hyphenation&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#sec-text-layout&quot;&gt;Text Layout&lt;/a&gt;&lt;/li&gt;
  &lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a id=&quot;sec-rendering-text&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;rendering-text&quot;&gt;Rendering Text&lt;/h2&gt;

&lt;p&gt;Fonts can be thought of as collections of &lt;strong&gt;Glyphs&lt;/strong&gt;: symbols that represent the letters, numbers and other symbols in the font’s typeface. Glyphs are typically described as vectors, i.e. lists of resizable shapes and lines that can losslessly scale to arbitrary sizes. They also contain important metrics about the dimensions they occupy on screen, key of which is the &lt;em&gt;x-advance&lt;/em&gt; or amount by which a cursor should move forward after displaying the glyph at its current location. [&lt;a href=&quot;#ref-1&quot;&gt;1&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;The problem we are going to solve in this post is of the following form. We have as input:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;A buffer containing text data.&lt;/li&gt;
  &lt;li&gt;A font, style (bold, italic, etc.) and size we want to use.&lt;/li&gt;
  &lt;li&gt;The language the text is written in, which a few later stages depend on.&lt;/li&gt;
  &lt;li&gt;A window or subregion of a window we want to render that text into.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The desired output of this text layout system is a list of glyph indices along with the coordinates, height and width they occupy on screen.&lt;/p&gt;

&lt;p&gt;There are three primary ways to draw a glyph to screen:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;We can preprocess the font to a texture that contains all glyphs in the font sizes and styles we desire. This texture is typically called a font atlas, and resembles other texture atlases common in graphics programming, e.g. the shadow map atlas. Then, we can translate a glyph index to the texture space rectangle (uv-extent of the glyph in the atlas), and blit it to the screen-space rect outputted by our text layout system. This is typically referred to as &lt;em&gt;bitmap text rendering&lt;/em&gt;.&lt;/li&gt;
  &lt;li&gt;We can store a &lt;em&gt;signed distance field&lt;/em&gt; in the atlas instead of the rasterized glyph. In this scenario, each texel in the atlas stores how far it lies from the nearest glyph edge, and whether it falls inside or outside the glyph by its sign. This can be rendered by applying a threshold to this distance in a shader, which lets an atlas serve multiple font sizes. [&lt;a href=&quot;#ref-2&quot;&gt;2&lt;/a&gt;]&lt;/li&gt;
  &lt;li&gt;Finally, we can just render the text to screen directly by drawing the vector shapes in the size and position described by the text layout system. The Slug algorithm is a popular choice for this method. [&lt;a href=&quot;#ref-3&quot;&gt;3&lt;/a&gt;]&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a id=&quot;sec-text-encodings&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;text-encodings&quot;&gt;Text Encodings&lt;/h2&gt;

&lt;p&gt;ASCII text is easily represented in C as an array of 8-bit integers or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;char&lt;/code&gt;s. But what happens when we need to describe characters that don’t belong to the English language, emoji or other complex symbols? Unicode 16.0 defines over 150,000 unique characters, far beyond the maximum that can be represented by 8 bits. To represent this large space of potential symbols or &lt;strong&gt;codepoints&lt;/strong&gt; in a byte-efficient manner we need an encoding scheme.&lt;/p&gt;

&lt;p&gt;A simple way to extend out of ASCII into codepoints that require more than 8 bits is through the use of a variable-size encoding scheme, where a single codepoint is represented by one or more &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;char&lt;/code&gt;s. We split the bit sequence between multiple bytes and use a special header in the first byte indicating how long the sequence is. Finally we mark up the trailing bytes with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;10&lt;/code&gt; prefix so that we know which bytes are part of the same codepoint. This is called &lt;em&gt;UTF-8&lt;/em&gt;.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Codepoint range&lt;/th&gt;
      &lt;th&gt;Bytes&lt;/th&gt;
      &lt;th&gt;Encoding&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;U+0000 - U+007F&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0xxxxxxx&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;U+0080 - U+07FF&lt;/td&gt;
      &lt;td&gt;2&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;110xxxxx&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;10xxxxxx&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;U+0800 - U+FFFF&lt;/td&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1110xxxx&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;10xxxxxx&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;10xxxxxx&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;U+10000 - U+10FFFF&lt;/td&gt;
      &lt;td&gt;4&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;11110xxx&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;10xxxxxx&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;10xxxxxx&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;10xxxxxx&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;We’re going to proceed with the assumption that we’re working with text encoded using &lt;em&gt;UTF-8&lt;/em&gt;, which is by far the most common encoding scheme out there these days. It’s crucial to be aware of what input scheme you’re working with however, there are other common encodings out there and assuming the wrong encoding scheme can lead to unexpected results. For example, the Win32 API decided to use a 16-bit scheme decades ago, and has been stuck with that ever since. You can learn more about text encodings at [&lt;a href=&quot;#ref-4&quot;&gt;4&lt;/a&gt;]. Luckily, most OSes and text libraries provide API functions to translate between encodings.&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;sec-paragraph-splitting&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;paragraph-splitting&quot;&gt;Paragraph Splitting&lt;/h2&gt;

&lt;p&gt;Laying out a body of text can be thought of as a hierarchy where the top is your entire piece of encoded bytes and the bottom are the individual glyphs that make it up. Sequentially, we split the body into small chunks, each of which can be split into smaller chunks independently. The first level of this hierarchy is the paragraph level. While it may seem obvious that paragraphs can be split via typical line breaking characters like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CR&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LF&lt;/code&gt;, Unicode specifies a whole class of codepoints that break paragraphs, aptly named the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Paragraph_Separator&lt;/code&gt; class. [&lt;a href=&quot;#ref-5&quot;&gt;5&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;sec-bidirectional-text&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;bidirectional-text&quot;&gt;Bidirectional Text&lt;/h2&gt;

&lt;p&gt;English, Spanish, French and most other Western languages flow from Left-to-Right (LTR). This means that successive glyphs typically flow in that direction for text segments that are part of these languages. In contrast, languages like Arabic and Hebrew flow from Right-to-Left (RTL). Handling both directions, possibly mixed in a single text corpus, is called BiDi handling.&lt;/p&gt;

&lt;p&gt;BiDi handling begins by assigning each codepoint a &lt;strong&gt;BiDi class&lt;/strong&gt;: a Unicode property describing how that codepoint behaves directionally. [&lt;a href=&quot;#ref-6&quot;&gt;6&lt;/a&gt;] Codepoints can be strongly LTR, strongly RTL, a neutral character like a space or a bracket, and so on. The Unicode Bidirectional Algorithm [&lt;a href=&quot;#ref-5&quot;&gt;5&lt;/a&gt;] resolves these classes into an &lt;strong&gt;embedding level&lt;/strong&gt; for every codepoint, an integer where even values run LTR and odd values run RTL. The paragraph then decomposes into &lt;strong&gt;BiDi runs&lt;/strong&gt;: maximal sequences of consecutive codepoints that share a level.&lt;/p&gt;

&lt;p&gt;Why isn’t this as simple as assigning each codepoint a direction and reordering them as such? Let’s look at a tricky sentence where an English phrase is quoted within a Hebrew sentence, and there is another RTL sequence within the English phrase.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/assets/lexkit/bidi_ordering.webp&quot;&gt;&lt;img src=&quot;/assets/lexkit/bidi_ordering.webp&quot; alt=&quot;BiDi ordering&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The naive approach ends up placing them in the wrong position. Correct reordering works from the deepest nesting level outward and is described in the Unicode Bidirectional Algorithm [&lt;a href=&quot;#ref-5&quot;&gt;5&lt;/a&gt;], under “Reordering Resolved Levels”. At each step we reverse every contiguous span of codepoints at that BiDi level or deeper, so a BiDi run at level &lt;em&gt;i&lt;/em&gt; will be reversed &lt;em&gt;i&lt;/em&gt; times.&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;sec-text-shaping&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;text-shaping&quot;&gt;Text Shaping&lt;/h2&gt;

&lt;p&gt;Text shaping refers to the process of converting a list of codepoints to a list of glyphs. This is a relatively complicated operation since the mapping runs in both directions: multiple codepoints may collapse into a single glyph (combining characters, ligatures, emoji sequences, etc.), and a single codepoint may expand into several glyphs. Additionally fonts may contain multiple glyph options for a single codepoint and the shaping engine needs to figure out which one from context. A font file’s Character to Glyph Index mapping table (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cmap&lt;/code&gt;) supplies a default glyph for each codepoint. After that, two additional tables are used to perform contextual decisions. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GSUB&lt;/code&gt; substitutes glyphs based on context (e.g. substituting consecutive glyphs with a ligature), and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GPOS&lt;/code&gt; adjusts their positions relative to one another (e.g. for kerning). [&lt;a href=&quot;#ref-7&quot;&gt;7&lt;/a&gt;] &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HarfBuzz&lt;/code&gt; is generally recognized as the standard for text shaping engines and we use it for the shaping stage in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lexkit&lt;/code&gt;. [&lt;a href=&quot;#ref-8&quot;&gt;8&lt;/a&gt;] We run it over each BiDi run individually since &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HarfBuzz&lt;/code&gt; exclusively handles shaping and isn’t able to perform other tasks in the pipeline such as BiDi handling or Line breaking.&lt;/p&gt;

&lt;p&gt;This article is mainly concerned with rendering a single text block with a single font and style that is known ahead of time, which is more relevant for game UIs and graphics demos. It’s worth mentioning that applications like Web browsers and rich text editors have another level of hierarchy that we need to consider before shaping. Shaping engines work on a single &lt;em&gt;script&lt;/em&gt; at a time, so a run spanning more than one script has to be split further. This means, for example, that English (Latin script) and Russian (Cyrillic script) can’t be part of the same shaping run despite both being left-to-right languages.&lt;/p&gt;

&lt;p&gt;Additionally, shaping engines only work on a single font at a time, which means rich text editors must segment their text input according to font family, style and size. A robust application should also switch between fonts when the initial font is missing glyphs for some of the codepoints in the run, using a fallback font for that section of text. This additional decomposition before shaping is called &lt;strong&gt;itemization&lt;/strong&gt; and is sufficiently complex to demand an article on its own. For our UI purposes, we can generally accept that we are dealing with a fixed number of fonts and styles and can assume that the text input is well-behaved, but for use cases where this isn’t true [&lt;a href=&quot;#ref-9&quot;&gt;9&lt;/a&gt;] is a good starting point for itemization.&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;sec-line-word-and-grapheme-breaks&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;line-word-and-grapheme-breaks&quot;&gt;Line, Word and Grapheme Breaks&lt;/h2&gt;

&lt;p&gt;Once we have our glyphs from the text shaping stage, we can work out how much space a piece of text occupies.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/assets/lexkit/font_metrics.webp&quot;&gt;&lt;img src=&quot;/assets/lexkit/font_metrics.webp&quot; alt=&quot;Font Metrics&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The shaping engine returns an &lt;em&gt;x-advance&lt;/em&gt; and an offset for each glyph, which we accumulate to find how far the cursor travels along the line. The font atlas stores the dimensions of each glyph’s rasterized bitmap, describing the rendered symbol that actually appears on screen, which can extend past the glyph’s advance. Vertically, glyphs can be thought of as lying on a baseline, with a maximum extent above (the &lt;em&gt;ascent&lt;/em&gt; of the font) and below (&lt;em&gt;descent&lt;/em&gt;) which are specified in the font file and can be recorded as part of the Font Atlas. Additionally, a recommended distance between successive baselines is also recorded. Together, these metrics tell us the width and height a piece of text would occupy as a single line. Very useful in a UI setting where we might want to center it within another element, or grow a container to the size of text.&lt;/p&gt;

&lt;p&gt;However, the most common way to render text is to a fixed-width container or window. In this case, we must automatically determine where to move on to the next line. This is called a &lt;strong&gt;Line break&lt;/strong&gt; [&lt;a href=&quot;#ref-10&quot;&gt;10&lt;/a&gt;]. Following our hierarchical model of text rendering described earlier, we can think of each paragraph being decomposed into lines.&lt;/p&gt;

&lt;p&gt;Unfortunately, this is not as easy as writing out glyphs until we exceed the line size for many reasons:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;It is difficult to read text that is broken up at a glyph level.&lt;/li&gt;
  &lt;li&gt;Certain glyphs only read correctly when present consecutively, so they must not be broken between lines.&lt;/li&gt;
  &lt;li&gt;It’s common to insert a hyphen to indicate a word is broken up between two lines, so we need to reserve space for the hyphen if we choose to break up a word.&lt;/li&gt;
  &lt;li&gt;The cursor must change from LTR to RTL depending on the glyphs being rendered.&lt;/li&gt;
  &lt;li&gt;There are certain glyphs that enforce a line break immediately after them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;strong&gt;Unicode Line Breaking Algorithm&lt;/strong&gt; [&lt;a href=&quot;#ref-10&quot;&gt;10&lt;/a&gt;] describes this process. The input to this algorithm is a single paragraph of codepoints and the output is a list of positions where a line break is required, optional or prohibited. It does this by mapping every codepoint in Unicode to a &lt;strong&gt;Line Breaking Class&lt;/strong&gt;, then defines a set of rules about which kinds of breaks are permitted between which groups of classes. We will describe an efficient way of implementing this algorithm in a future optimization article.&lt;/p&gt;

&lt;p&gt;We can then group glyphs into lines as follows:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Use the Unicode Line Breaking Algorithm to determine a set of valid line break positions for the paragraph.&lt;/li&gt;
  &lt;li&gt;Accumulate consecutive glyphs, adding up their &lt;em&gt;x-advance&lt;/em&gt;s until the sum exceeds the line width.&lt;/li&gt;
  &lt;li&gt;Wind back to the previous valid line break position, and break there.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a &lt;em&gt;greedy&lt;/em&gt; algorithm: every line is filled as far as it can be before we move on. To improve the evenness of line lengths, line breaking can additionally be performed on an entire paragraph at once, where candidate break locations are scored based on the gaps left behind. The Knuth-Plass line breaking algorithm used in TeX is the best example of this. [&lt;a href=&quot;#ref-11&quot;&gt;11&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;Lines can be further broken down into words, and words can be similarly broken down into sequences of one or more codepoints called &lt;strong&gt;grapheme clusters&lt;/strong&gt; [&lt;a href=&quot;#ref-12&quot;&gt;12&lt;/a&gt;].&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/assets/lexkit/break_hierarchy.webp&quot;&gt;&lt;img src=&quot;/assets/lexkit/break_hierarchy.webp&quot; alt=&quot;Break hierarchy&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While grapheme clusters may look like a single character in most cases, they do not always map to a single codepoint. For example, an accented letter may be composed of a base letter codepoint followed by a combining mark codepoint representing the accent. Flag symbols are represented by pairs of regional indicator codepoints and complex emoji can be divided into multiple codepoints connected by special codepoints called zero width joiners.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/assets/lexkit/grapheme_codepoints.webp&quot;&gt;&lt;img src=&quot;/assets/lexkit/grapheme_codepoints.webp&quot; alt=&quot;Graphemes and codepoints&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It makes no sense to break these codepoints up for line breaking or selection purposes. Grapheme clusters represent the lowest logical level of separation. Each of the above examples is a single grapheme cluster spanning several codepoints. The boundaries described by grapheme breaks are what a text editor uses to decide how far the cursor travels when you press an arrow key, or how much text a single backspace removes. Similar to line breaks, grapheme break locations are described by classifying codepoints into &lt;strong&gt;Grapheme Break Classes&lt;/strong&gt; and specifying rules for legal breaks between them. [&lt;a href=&quot;#ref-12&quot;&gt;12&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;Word breaks should be more intuitive. Pressing Ctrl + an arrow key advances the cursor forward or backward across word boundaries. In English, words are typically separated by spaces or other punctuation. Unicode’s Word Break Specification [&lt;a href=&quot;#ref-12&quot;&gt;12&lt;/a&gt;] again describes legal word break locations using a set of rules between &lt;strong&gt;Word Break Classes&lt;/strong&gt;, where every Unicode codepoint maps to a class. If hyphenation is disabled, line break locations are also word break locations.&lt;/p&gt;

&lt;p&gt;You may notice we described both line breaking and BiDi as operating on paragraphs instead of at two different levels in the hierarchy. This is because a single line can contain more than one directionality of text. So when accumulating &lt;em&gt;x-advance&lt;/em&gt;s for a line, we can do so freely across BiDi runs.&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;sec-hyphenation&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;hyphenation&quot;&gt;Hyphenation&lt;/h2&gt;

&lt;p&gt;What if we need to line break inside a word? In typical text layouts, this split is accompanied by a hyphen, and choosing an optimal break position within a word is called &lt;strong&gt;Hyphenation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This can be a language-specific problem. TeX uses a dictionary of patterns per language to score every position in a word using &lt;em&gt;Liang’s Algorithm&lt;/em&gt;. [&lt;a href=&quot;#ref-13&quot;&gt;13&lt;/a&gt;] This requires knowledge of the language used per section of text corpus as well as reliance on a fixed dictionary.&lt;/p&gt;

&lt;p&gt;We use a simpler, greedy algorithm for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lexkit&lt;/code&gt;. Since grapheme breaks already indicate which positions a word can be broken at without breaking things like emoji sequences, we call any break at a grapheme cluster boundary legal. Then we sweep over grapheme boundaries such that both the first half of the word and a hyphen fit on the original line, accounting for the kerning advance between the last glyph and the hyphen itself.&lt;/p&gt;

&lt;p&gt;This approach needs no language-specific data at all, at the cost of breaking words at slightly less legible positions. For example, TeX implementations may favor breaking the word &lt;em&gt;talking&lt;/em&gt; as &lt;em&gt;talk-&lt;/em&gt; + &lt;em&gt;ing&lt;/em&gt;, while our algorithm wouldn’t favor any specific position for that word.&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;sec-text-layout&quot;&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;text-layout&quot;&gt;Text Layout&lt;/h2&gt;

&lt;p&gt;Finally, we have a list of lines composed of glyphs, with each glyph belonging to a BiDi run. It’s here that we can apply the BiDi reordering algorithm described in the Bidirectional Text section, reorganizing the glyphs of a line into a left-to-right ordering regardless of the actual BiDi orientation.&lt;/p&gt;

&lt;p&gt;Now we have the list of glyphs in visual order for this line and can lay them out. The cursor starts at the left edge of the line and moves forward by each glyph’s &lt;em&gt;x-advance&lt;/em&gt;. Glyph metrics from the shaping stage specify the &lt;em&gt;offset&lt;/em&gt; at which the glyph’s bitmap should be positioned relative to that cursor. Vertically, the first baseline of a text corpus sits one &lt;em&gt;ascent&lt;/em&gt; below the top of our container, and each successive line moves the baseline down by the recommended distance between baselines. A glyph is then placed relative to its baseline using the extents recorded in the atlas. This gives us screen-space rects for every glyph, parameterized by &lt;em&gt;x&lt;/em&gt;, &lt;em&gt;y&lt;/em&gt;, &lt;em&gt;height&lt;/em&gt; and &lt;em&gt;width&lt;/em&gt;. From the font atlas, we retrieve a similar rect in UV space. Now text rendering reduces to a simple alpha-blended blit operation, highly efficient on the GPU.&lt;/p&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;p&gt;&lt;a id=&quot;ref-1&quot;&gt;&lt;/a&gt;[1] &lt;a href=&quot;https://freetype.org/freetype2/docs/glyphs/&quot;&gt;FreeType Glyph Conventions&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;ref-2&quot;&gt;&lt;/a&gt;[2] Chris Green, 2007. &lt;a href=&quot;https://steamcdn-a.akamaihd.net/apps/valve/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf&quot;&gt;Improved Alpha-Tested Magnification for Vector Textures and Special Effects&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;ref-3&quot;&gt;&lt;/a&gt;[3] Eric Lengyel, 2017. &lt;a href=&quot;https://jcgt.org/published/0006/02/02/&quot;&gt;GPU-Centered Font Rendering Directly from Glyph Outlines&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;ref-4&quot;&gt;&lt;/a&gt;[4] Joel Spolsky, 2003. &lt;a href=&quot;https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/&quot;&gt;The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;ref-5&quot;&gt;&lt;/a&gt;[5] Unicode Consortium. &lt;a href=&quot;https://www.unicode.org/reports/tr9/&quot;&gt;Unicode Standard Annex #9: Unicode Bidirectional Algorithm&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;ref-6&quot;&gt;&lt;/a&gt;[6] Unicode Consortium. &lt;a href=&quot;https://www.unicode.org/reports/tr44/&quot;&gt;Unicode Standard Annex #44: Unicode Character Database&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;ref-7&quot;&gt;&lt;/a&gt;[7] Microsoft. &lt;a href=&quot;https://learn.microsoft.com/en-us/typography/opentype/spec/&quot;&gt;OpenType Specification&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;ref-8&quot;&gt;&lt;/a&gt;[8] &lt;a href=&quot;https://harfbuzz.github.io/&quot;&gt;HarfBuzz Manual&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;ref-9&quot;&gt;&lt;/a&gt;[9] Raph Levien, 2020. &lt;a href=&quot;https://raphlinus.github.io/text/2020/10/26/text-layout.html&quot;&gt;Text layout is a loose hierarchy of segmentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;ref-10&quot;&gt;&lt;/a&gt;[10] Unicode Consortium. &lt;a href=&quot;https://www.unicode.org/reports/tr14/&quot;&gt;Unicode Standard Annex #14: Unicode Line Breaking Algorithm&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;ref-11&quot;&gt;&lt;/a&gt;[11] Donald E. Knuth and Michael F. Plass, 1981. &lt;a href=&quot;https://doi.org/10.1002/spe.4380111102&quot;&gt;Breaking Paragraphs into Lines&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;ref-12&quot;&gt;&lt;/a&gt;[12] Unicode Consortium. &lt;a href=&quot;https://www.unicode.org/reports/tr29/&quot;&gt;Unicode Standard Annex #29: Unicode Text Segmentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id=&quot;ref-13&quot;&gt;&lt;/a&gt;[13] Franklin Mark Liang, 1983. &lt;a href=&quot;https://tug.org/docs/liang/&quot;&gt;Word Hy-phen-a-tion by Com-put-er&lt;/a&gt;&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Catalyst</title>
   <link href="https://somnathsarkar.io/2022/06/15/catalyst/"/>
   <updated>2022-06-15T00:00:00-07:00</updated>
   <id>https://somnathsarkar.io/2022/06/15/catalyst</id>
   <content type="html">&lt;h3 id=&quot;a-real-time-rendering-engine-built-in-vulkan-and-modern-c&quot;&gt;A real-time rendering engine built in Vulkan and Modern C++&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Example Scene&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Scene open in Editor&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&lt;a href=&quot;/assets/catalyst/engine.png&quot;&gt;&lt;img src=&quot;/assets/catalyst/engine.png&quot; alt=&quot;Engine Screenshot&quot; /&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&lt;a href=&quot;/assets//catalyst/editor.png&quot;&gt;&lt;img src=&quot;/assets/catalyst/editor.png&quot; alt=&quot;Editor Screenshot&quot; /&gt;&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The next-generation of graphics APIs, Vulkan and DirectX 12, are quickly becoming the new foundation for games on PC. I wanted to build a rendering engine that allows me to easily explore and experiment with some of the most important features of modern real-time graphics systems found in games today. The result of this work was Catalyst - a physically-based real-time renderer built in Vulkan and modern C++. This post will demonstrate what goes into rendering a simple scene using Catalyst with all effects turned on, with comments on the implementation details in Vulkan. For the project source code and full list of graphics and editor features, a public repository is available &lt;a href=&quot;https://github.com/somnathsarkar/catalyst&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;h3 id=&quot;table-of-contents&quot;&gt;Table of Contents&lt;/h3&gt;
  &lt;ol&gt;
    &lt;li&gt;&lt;a href=&quot;#overview&quot;&gt;Overview&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#shadow-mapping&quot;&gt;Shadow mapping&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#z-prepass&quot;&gt;Z-Prepass&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#screen-space-ambient-occlusion-ssao&quot;&gt;Screen-Space Ambient Occlusion&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#screen-space-reflections-ssr&quot;&gt;Screen-Space Reflections&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#skybox&quot;&gt;Skybox&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#main-render-pass&quot;&gt;Main render pass&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#hdrtone-mapping&quot;&gt;HDR/Tone mapping&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#debug-draw&quot;&gt;Debug Draw&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#editor&quot;&gt;Editor&lt;/a&gt;&lt;/li&gt;
  &lt;/ol&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;overview&quot;&gt;Overview&lt;/h2&gt;
&lt;p&gt;Catalyst is a fully-forward renderer that uses bindless resources. This is a flowchart of a single frame:
&lt;a href=&quot;/assets/catalyst/flowchart.png&quot;&gt;&lt;img src=&quot;/assets/catalyst/flowchart.png&quot; alt=&quot;Graphics Flowchart&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;shadow-mapping&quot;&gt;Shadow mapping&lt;/h2&gt;

&lt;p&gt;For every directional light, a shadowmap is generated. The Catalyst editor has options to tweak the width and height of the directional lights effect, ideally this should be as small as possible to make maximum use of the Shadow Map’s limited resolution. There are a number of additional parameters relating to shadows that are exposed to the user via Global Settings. Shadowmap bias can be increased or decreased according to the scene, and the size of the PCF smoothing kernel can be modified for performance as well. The shadowmap render pass is a simple depth-only pass that applies all vertex transformations and renders to a single depth/stencil texture. The entire scene is rendered from the lights point of view. For directional lights this involves an orthographic projection.&lt;/p&gt;

&lt;table&gt;
&lt;tr nowrap=&quot;nowrap&quot;&gt;&lt;td&gt;&lt;a href=&quot;/assets/catalyst/shadowmap0.png&quot;&gt;&lt;img src=&quot;/assets/catalyst/shadowmap0.png&quot; /&gt;&lt;/a&gt;&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;/assets/catalyst/shadowmap1.png&quot;&gt;&lt;img src=&quot;/assets/catalyst/shadowmap1.png&quot; /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot;&gt;&lt;center&gt;&lt;em&gt;Two shadowmaps from directional lights in the scene&lt;/em&gt;&lt;/center&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;

&lt;h2 id=&quot;z-prepass&quot;&gt;Z-Prepass&lt;/h2&gt;
&lt;p&gt;Similar to the shadowmap pass, the Z-Prepass is a depth only render pass. This time the entire scene is rendered from the main camera’s perspective. Also, to enable MSAA for the main render pass, the depth image that we write here is multisampled. A single sample texture is generated at the end of this pass by resolving this depth image. The single sample texture is passed as input to the SSAO and SSR passes, while the multisampled texture is forwarded directly to the main render pass.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;&lt;a href=&quot;/assets/catalyst/zprepass.png&quot;&gt;&lt;img src=&quot;/assets/catalyst/zprepass.png&quot; alt=&quot;Z Prepass&quot; /&gt;&lt;/a&gt;&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&lt;em&gt;Resolved z-prepass depth texture&lt;/em&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;screen-space-ambient-occlusion-ssao&quot;&gt;Screen-Space Ambient Occlusion (SSAO)&lt;/h2&gt;

&lt;p&gt;SSAO is implemented in a relatively straightforward manner, but there are some additional considerations for a fully-forward renderer. At the start of the scene a noise texture is generated to provide a random permutation for the SSAO kernel. The kernel consists of a variable number of rays in the hemisphere of the surface, with most rays concentrated close to the normal. Unlike a deferred renderer, we do not have a normal G-buffer to refer to and thus the normals must be reconstructed from the depthmap generated by the Z-Prepass. This can cause artifacts at the borders of objects, an example of the normal map generated by such an approach is shown below.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;&lt;a href=&quot;/assets/catalyst/recondepth.png&quot;&gt;&lt;img src=&quot;/assets/catalyst/recondepth.png&quot; alt=&quot;Example Depth&quot; /&gt;&lt;/a&gt;&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;&lt;a href=&quot;/assets/catalyst/reconnormal.png&quot;&gt;&lt;img src=&quot;/assets/catalyst/reconnormal.png&quot; alt=&quot;Reconstructed Normal&quot; /&gt;&lt;/a&gt;&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;&lt;a href=&quot;/assets/catalyst/reconssao.png&quot;&gt;&lt;img src=&quot;/assets/catalyst/reconssao.png&quot; alt=&quot;SSAO Map&quot; /&gt;&lt;/a&gt;&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&lt;em&gt;Example depth buffer input&lt;/em&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&lt;em&gt;Reconstructed normal data, note discontinuities at edges&lt;/em&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&lt;em&gt;Resulting SSAO Map&lt;/em&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Overall, SSAO is a nice way to improve the scene for a relatively cheap cost. Reducing the number of rays from 64 to 16 reduces frame time significantly without degrading the final result too heavily. The SSAO map is generated in half-resolution and composited during the main render pass. Click to enlarge the below images to compare results easier.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;&lt;a href=&quot;/assets/catalyst/exp1.png&quot;&gt;&lt;img src=&quot;/assets/catalyst/exp1.png&quot; alt=&quot;SSAO On&quot; /&gt;&lt;/a&gt;&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;&lt;a href=&quot;/assets/catalyst/ssaooff.png&quot;&gt;&lt;img src=&quot;/assets/catalyst/ssaooff.png&quot; alt=&quot;SSAO Off&quot; /&gt;&lt;/a&gt;&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&lt;em&gt;SSAO On&lt;/em&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&lt;em&gt;SSAO Off&lt;/em&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;screen-space-reflections-ssr&quot;&gt;Screen-Space Reflections (SSR)&lt;/h2&gt;
&lt;p&gt;The inputs to this stage include the resolved depth image from the Z-prepass and the resolved previous frame. The previous frame used here is just after the main render pass and before the HDR/Tone-mapping stage. Since this is a fully forward renderer, we don’t have access to a specular G-buffer, so the previous frame is our best bet for reflection data. Getting SSR to look good depends on a number of scene-dependant parameters. Thus, many options are presented to the user to tweak their appearance. Similarly to SSAO, normals are reconstructed from the depth image and are used to find the reflection vector along the view direction. Raymarching is performed until a matching object is found in the previous frame.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;&lt;a href=&quot;/assets/catalyst/ssr.png&quot;&gt;&lt;img src=&quot;/assets/catalyst/ssr.png&quot; alt=&quot;SSR Map&quot; /&gt;&lt;/a&gt;&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&lt;em&gt;SSR Map&lt;/em&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;SSR is tricky to get right, and is the most expensive step of the pipeline as of now due to heavy branching. If a reflection ray is not found, the environment cubemap is used as a fallback for reflection information.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;&lt;a href=&quot;/assets/catalyst/exp1.png&quot;&gt;&lt;img src=&quot;/assets/catalyst/exp1.png&quot; alt=&quot;SSR On&quot; /&gt;&lt;/a&gt;&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;&lt;a href=&quot;/assets/catalyst/ssroff.png&quot;&gt;&lt;img src=&quot;/assets/catalyst/ssroff.png&quot; alt=&quot;SSR Off&quot; /&gt;&lt;/a&gt;&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&lt;em&gt;SSR On&lt;/em&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&lt;em&gt;SSR Off&lt;/em&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;skybox&quot;&gt;Skybox&lt;/h2&gt;
&lt;p&gt;Environmental mapping is performed using a skybox with two cubemaps. One is used to provide the specular lighting component to scene geometry, while the other is an irradiance map used for the diffuse lighting component. There are several tools available to generate these maps. One skybox is included with the editor for convenience. The first draw of every main renderpass involves drawing the skybox to the empty frame.&lt;/p&gt;

&lt;h2 id=&quot;main-render-pass&quot;&gt;Main render pass&lt;/h2&gt;
&lt;p&gt;This is where everything comes together. SSAO and SSR maps and all shadow maps are inputs to this stage as textures. All material parameters and render settings are exposed in a uniform buffer, and the multisampled depthmap is attached for depth testing with the compare operation set to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;VK_COMPARE_OP_EQUAL&lt;/code&gt;. The output of this stage is a multisampled High Dynamic Range (HDR) color attachment which is resolved to a single sample texture for use in the HDR pass, compute shaders and the next frames’ SSR calculation.&lt;/p&gt;

&lt;table&gt;
&lt;tr nowrap=&quot;nowrap&quot;&gt;&lt;td&gt;&lt;a href=&quot;/assets/catalyst/skybox.png&quot;&gt;&lt;img src=&quot;/assets/catalyst/skybox.png&quot; /&gt;&lt;/a&gt;&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;/assets/catalyst/skybox.png&quot;&gt;&lt;img src=&quot;/assets/catalyst/mainpass.png&quot; /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot;&gt;&lt;center&gt;&lt;em&gt;HDR buffer before and after scene geometry is rendered. To display these images, the HDR values across each image were normalized independently.&lt;/em&gt;&lt;/center&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;

&lt;h2 id=&quot;hdrtone-mapping&quot;&gt;HDR/Tone mapping&lt;/h2&gt;
&lt;p&gt;First a series of compute shaders is used to calculate the geometric average of the illuminance for the frame. The frame is reduced by a factor of the subgroup size each iteration. Vulkan’s subgroup extension (now part of the core Vulkan specification) is very handy for performing these reduce operations. The end result is the log-average illuminance which is fed into the Tonemapping pass as a Uniform Buffer. The tonemapping pass takes the resolved HDR output of the main render pass and maps it to a Low Dynamic Resolution (LDR) color attachment suitable for presentation. The geometric mean of the illuminance, together with a user-modifiable exposure adjustment is used to perform this mapping. The effects of the exposure setting on the final result is shown below.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;&lt;a href=&quot;/assets/catalyst/exp0.png&quot;&gt;&lt;img src=&quot;/assets/catalyst/exp0.png&quot; alt=&quot;Exposure 0.5&quot; /&gt;&lt;/a&gt;&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;&lt;a href=&quot;/assets/catalyst/exp1.png&quot;&gt;&lt;img src=&quot;/assets/catalyst/exp1.png&quot; alt=&quot;Exposure 1.0&quot; /&gt;&lt;/a&gt;&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;&lt;a href=&quot;/assets/catalyst/exp2.png&quot;&gt;&lt;img src=&quot;/assets/catalyst/exp2.png&quot; alt=&quot;Exposure 2.0&quot; /&gt;&lt;/a&gt;&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;&lt;a href=&quot;/assets/catalyst/exp4.png&quot;&gt;&lt;img src=&quot;/assets/catalyst/exp4.png&quot; alt=&quot;Exposure 4.0&quot; /&gt;&lt;/a&gt;&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;&lt;a href=&quot;/assets/catalyst/exp8.png&quot;&gt;&lt;img src=&quot;/assets/catalyst/exp8.png&quot; alt=&quot;Exposure 8.0&quot; /&gt;&lt;/a&gt;&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Exp. = 0.5&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Exp. = 1.0&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Exp. = 2.0&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Exp. = 4.0&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Exp. = 8.0&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;debug-draw&quot;&gt;Debug Draw&lt;/h2&gt;
&lt;p&gt;It can be helpful to draw 3D structures like bounding boxes or billboards to aid debugging during development. The Catalyst editor makes use of this feature to render icons for scene objects with no geometry such as Directional Lights, and to indicate which meshes the user has selected in the scene hierarchy by displaying a bounding box that holds all of them. This stage writes directly to the resolved color output attachment from the main render pass. This means that output from this stage is not used to compute tonemapping information or generate SSR maps for future stages, which is ideal. The downside of this approach is that MSAA is not available for this stage, which is less important for a feature that is disabled when building for release mode.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;&lt;a href=&quot;/assets/catalyst/aabb.png&quot;&gt;&lt;img src=&quot;/assets/catalyst/aabb.png&quot; alt=&quot;AABB&quot; /&gt;&lt;/a&gt;&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;&lt;a href=&quot;/assets/catalyst/billboard.png&quot;&gt;&lt;img src=&quot;/assets/catalyst/billboard.png&quot; alt=&quot;Billboard&quot; /&gt;&lt;/a&gt;&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&lt;em&gt;An AABB is drawn around selected meshes in the editor&lt;/em&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&lt;em&gt;Screen-aligned billboards are used to indicate selected directional lights&lt;/em&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;editor&quot;&gt;Editor&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;&lt;a href=&quot;/assets/catalyst/editor-materials.png&quot;&gt;&lt;img src=&quot;/assets/catalyst/editor-materials.png&quot; alt=&quot;Editor Materials&quot; /&gt;&lt;/a&gt;&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&lt;em&gt;Modifying material properties in the Catalyst editor&lt;/em&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The Catalyst repository on Github includes a user-friendly editor built on Qt 6 that allows you to import models and textures, edit material parameters and arrange meshes in a scene hierarchy. It comes with a number of sample assets to easily experiment with many of the engines features. Qt is not necessary to build the rendering engine itself. If you’d like to try it out, the repository is available &lt;a href=&quot;https://github.com/somnathsarkar/catalyst&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
</content>
 </entry>
 

</feed>
