<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Igalia on Christian Gmeiner</title>
    <link>https://christian-gmeiner.info/tags/igalia/</link>
    <description>Recent content in Igalia on Christian Gmeiner</description>
    <image>
      <title>Christian Gmeiner</title>
      <url>https://christian-gmeiner.info/papermod-cover.png</url>
      <link>https://christian-gmeiner.info/papermod-cover.png</link>
    </image>
    <generator>Hugo -- 0.159.0</generator>
    <language>en-us</language>
    <lastBuildDate>Fri, 19 Jun 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://christian-gmeiner.info/tags/igalia/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>The Diagonal Seam</title>
      <link>https://christian-gmeiner.info/2026-06-19-the-diagonal-seam/</link>
      <pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate>
      <guid>https://christian-gmeiner.info/2026-06-19-the-diagonal-seam/</guid>
      <description>&lt;p&gt;Two more dEQP tests down:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;dEQP-GLES3.functional.fbo.blit.rect.nearest_consistency_mag -- Pass
dEQP-GLES3.functional.fbo.blit.rect.nearest_consistency_min -- Pass
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This one was a fun geometry puzzle.&lt;/p&gt;
&lt;h2 id=&#34;the-problem&#34;&gt;The problem&lt;/h2&gt;
&lt;p&gt;Mesa&amp;rsquo;s &lt;code&gt;u_blitter&lt;/code&gt; is the utility that drivers use for framebuffer blits &amp;ndash; copying pixel data between surfaces, optionally scaling and filtering. It works by drawing a textured quad: set up the source as a texture, the destination as a render target, and draw a rectangle with the appropriate texture coordinates. Simple.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Two more dEQP tests down:</p>
<pre tabindex="0"><code>dEQP-GLES3.functional.fbo.blit.rect.nearest_consistency_mag -- Pass
dEQP-GLES3.functional.fbo.blit.rect.nearest_consistency_min -- Pass
</code></pre><p>This one was a fun geometry puzzle.</p>
<h2 id="the-problem">The problem</h2>
<p>Mesa&rsquo;s <code>u_blitter</code> is the utility that drivers use for framebuffer blits &ndash; copying pixel data between surfaces, optionally scaling and filtering. It works by drawing a textured quad: set up the source as a texture, the destination as a render target, and draw a rectangle with the appropriate texture coordinates. Simple.</p>
<p>Except the quad is made of two triangles. And that diagonal seam between them is where the trouble starts.</p>
<pre tabindex="0"><code> v2 -------- v3
  |  \   T2  |
  | T1  \    |
  |       \  |
 v0 -------- v1
</code></pre><p>For LINEAR filtering, this is fine &ndash; the interpolation across the seam is smooth enough that nobody notices. But for NEAREST filtering, a texel is selected based on which texel center is closest to the interpolated texture coordinate. At the diagonal seam, the two triangles can produce <em>slightly different</em> texture coordinates for pixels that sit right on the boundary. Different coordinates mean different nearest-texel selection, and that means an inconsistent stripe of wrong texels running diagonally across the blit.</p>
<p>The dEQP <code>nearest_consistency</code> tests specifically check for this: they blit with NEAREST filtering and verify that every pixel picks the same texel regardless of which triangle it fell into.</p>
<p><img alt="tags" loading="lazy" src="/img/diagonal-seam.png"></p>
<h2 id="what-others-do">What others do</h2>
<p>This isn&rsquo;t a new problem. V3D already has a workaround in <code>u_blitter</code>: it sets <code>use_index_buffer</code> to reorder the triangle indices so that the shared edge of the two triangles is along a different diagonal. This changes which pixels land on the seam and can be enough to pass the tests on some hardware.</p>
<p>On GC7000, that wasn&rsquo;t sufficient. The floating-point interpolation differences are large enough that the seam remains visible regardless of which diagonal you pick.</p>
<h2 id="what-the-blob-does">What the blob does</h2>
<p>Looking at command stream traces from the proprietary Vivante driver, the answer was clear: they don&rsquo;t draw a quad at all. They draw a <strong>single oversized triangle</strong> and let the scissor clip it to the destination rectangle.</p>
<p>No seam, no problem.</p>
<h2 id="the-single-triangle-transform">The single triangle transform</h2>
<p>The idea is simple: take a rectangle and find a single triangle that fully covers it. The smallest such triangle is a right triangle with legs twice the width and twice the height of the rectangle:</p>
<pre tabindex="0"><code> v2
  |\
  |  \
  | rect\
  |------+\
  |      |  \
 v0 -----|--- v1
</code></pre><p>Vertex 0 stays at the rectangle&rsquo;s top-left corner. Vertex 1 extends to twice the rectangle width. Vertex 2 extends to twice the rectangle height. The original rectangle is fully contained within this triangle.</p>
<p>The math for transforming the blitter&rsquo;s 4-vertex quad into a 3-vertex triangle is:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">for</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="n">a</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">a</span> <span class="o">&lt;</span> <span class="mi">2</span><span class="p">;</span> <span class="n">a</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>      <span class="cm">/* pos and texcoord */</span>
</span></span><span class="line"><span class="cl">   <span class="k">for</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="n">c</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">c</span> <span class="o">&lt;</span> <span class="mi">4</span><span class="p">;</span> <span class="n">c</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>   <span class="cm">/* xyzw components */</span>
</span></span><span class="line"><span class="cl">      <span class="kt">float</span> <span class="n">v0</span> <span class="o">=</span> <span class="n">ctx</span><span class="o">-&gt;</span><span class="n">vertices</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="n">a</span><span class="p">][</span><span class="n">c</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">      <span class="n">ctx</span><span class="o">-&gt;</span><span class="n">vertices</span><span class="p">[</span><span class="mi">1</span><span class="p">][</span><span class="n">a</span><span class="p">][</span><span class="n">c</span><span class="p">]</span> <span class="o">=</span> <span class="mf">2.0f</span> <span class="o">*</span> <span class="n">ctx</span><span class="o">-&gt;</span><span class="n">vertices</span><span class="p">[</span><span class="mi">1</span><span class="p">][</span><span class="n">a</span><span class="p">][</span><span class="n">c</span><span class="p">]</span> <span class="o">-</span> <span class="n">v0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">ctx</span><span class="o">-&gt;</span><span class="n">vertices</span><span class="p">[</span><span class="mi">2</span><span class="p">][</span><span class="n">a</span><span class="p">][</span><span class="n">c</span><span class="p">]</span> <span class="o">=</span> <span class="mf">2.0f</span> <span class="o">*</span> <span class="n">ctx</span><span class="o">-&gt;</span><span class="n">vertices</span><span class="p">[</span><span class="mi">3</span><span class="p">][</span><span class="n">a</span><span class="p">][</span><span class="n">c</span><span class="p">]</span> <span class="o">-</span> <span class="n">v0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">   <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>This transforms both position and texture coordinates consistently, so the texture mapping across the visible (scissored) region is identical to what the full quad would have produced &ndash; minus the seam.</p>
<h2 id="scissor-is-essential">Scissor is essential</h2>
<p>The oversized triangle extends beyond the destination rectangle, so we need scissor to clip it. The blitter doesn&rsquo;t always have a scissor set up, so when <code>use_single_triangle</code> is enabled, we synthesize one from the destination box:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="n">ctx</span><span class="o">-&gt;</span><span class="n">base</span><span class="p">.</span><span class="n">use_single_triangle</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">scissor</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="n">synth_scissor</span><span class="p">.</span><span class="n">minx</span> <span class="o">=</span> <span class="nf">MAX2</span><span class="p">(</span><span class="n">dstbox</span><span class="o">-&gt;</span><span class="n">x</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">   <span class="n">synth_scissor</span><span class="p">.</span><span class="n">miny</span> <span class="o">=</span> <span class="nf">MAX2</span><span class="p">(</span><span class="n">dstbox</span><span class="o">-&gt;</span><span class="n">y</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">   <span class="n">synth_scissor</span><span class="p">.</span><span class="n">maxx</span> <span class="o">=</span> <span class="n">dstbox</span><span class="o">-&gt;</span><span class="n">x</span> <span class="o">+</span> <span class="n">dstbox</span><span class="o">-&gt;</span><span class="n">width</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">   <span class="n">synth_scissor</span><span class="p">.</span><span class="n">maxy</span> <span class="o">=</span> <span class="n">dstbox</span><span class="o">-&gt;</span><span class="n">y</span> <span class="o">+</span> <span class="n">dstbox</span><span class="o">-&gt;</span><span class="n">height</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">   <span class="n">scissor</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">synth_scissor</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h2 id="keeping-it-scoped">Keeping it scoped</h2>
<p>The single-triangle transform should only apply to blit operations, not to clears or other blitter draws. A transient <code>single_triangle_active</code> flag is set around the actual blit draw calls and checked in the vertex emission code. Drivers opt in by setting <code>use_single_triangle</code> on the blitter context at creation time.</p>
<p>For etnaviv, that&rsquo;s a single line:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="n">ctx</span><span class="o">-&gt;</span><span class="n">blitter</span><span class="o">-&gt;</span><span class="n">use_single_triangle</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
</span></span></code></pre></div><h2 id="the-design">The design</h2>
<p>The implementation is split into two commits: the <code>u_blitter.c</code> infrastructure (vertex transform, synthesized scissor, gating flag, 3-vertex draw path) that any driver can opt into, and the one-line etnaviv enablement. Keeping them separate means another driver hitting the same seam can flip the flag without touching u_blitter code.</p>
<p>The work landed upstream in <a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/39973">u_blitter: Add single-triangle draw mode for NEAREST blit consistency</a>.</p>
<h2 id="the-takeaway">The takeaway</h2>
<p>Sometimes the fix for a rendering artifact isn&rsquo;t better math or tighter tolerances &ndash; it&rsquo;s removing the geometric feature that causes the problem in the first place. Two triangles have a seam. One triangle doesn&rsquo;t.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Fixing the R/B swap the right way</title>
      <link>https://christian-gmeiner.info/2026-06-10-fixing-the-rb-swap-the-right-way/</link>
      <pubDate>Wed, 10 Jun 2026 00:00:00 +0000</pubDate>
      <guid>https://christian-gmeiner.info/2026-06-10-fixing-the-rb-swap-the-right-way/</guid>
      <description>&lt;p&gt;If you&amp;rsquo;ve ever looked at a GPU render and seen blue where red should be, you&amp;rsquo;ve met the R/B swap problem. For etnaviv this has been a long-standing source of complexity. We were solving it in the shader, but the proprietary blob driver had a simpler approach all along. As part of my work at &lt;a href=&#34;https://www.igalia.com/&#34;&gt;Igalia&lt;/a&gt;, I finally sat down and did it properly.&lt;/p&gt;
&lt;h2 id=&#34;the-problem&#34;&gt;The problem&lt;/h2&gt;
&lt;p&gt;Vivante GPUs have a quirk: the Pixel Engine (PE) always writes pixels in BGRA byte order. When your API says &amp;ldquo;render to R8G8B8A8_UNORM&amp;rdquo;, what actually lands in memory is B, G, R, A. Every byte of every pixel, every frame. The hardware just works that way.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>If you&rsquo;ve ever looked at a GPU render and seen blue where red should be, you&rsquo;ve met the R/B swap problem. For etnaviv this has been a long-standing source of complexity. We were solving it in the shader, but the proprietary blob driver had a simpler approach all along. As part of my work at <a href="https://www.igalia.com/">Igalia</a>, I finally sat down and did it properly.</p>
<h2 id="the-problem">The problem</h2>
<p>Vivante GPUs have a quirk: the Pixel Engine (PE) always writes pixels in BGRA byte order. When your API says &ldquo;render to R8G8B8A8_UNORM&rdquo;, what actually lands in memory is B, G, R, A. Every byte of every pixel, every frame. The hardware just works that way.</p>
<p>The question is: where do you fix it?</p>
<p>The etnaviv driver was doing it in the shader. Before the fragment shader writes its output, a <a href="https://docs.mesa3d.org/nir/index.html">NIR</a> lowering pass swaps the R and B channels:</p>
<pre tabindex="0"><code>   alu-&gt;src[0].swizzle[0] = 2;   /* .r reads from .b */
   alu-&gt;src[0].swizzle[2] = 0;   /* .b reads from .r */
</code></pre><p>This works, until it doesn&rsquo;t. The shader key needs a <code>frag_rb_swap</code> bitmask per render target. The blend color needs per-RT R/B swapping to match. And it falls apart entirely for scalar outputs - if a shader writes a single float, there&rsquo;s no <code>.z</code> component to swizzle into <code>.x</code>. That&rsquo;s exactly the NIR validation failure we hit:</p>
<pre tabindex="0"><code>Test case &#39;dEQP-GLES3.functional.fragment_out.basic.fixed.rgb8_lowp_float&#39;..
NIR validation failed after etna_lower_io in ../mesa/src/gallium/drivers/etnaviv/etnaviv_compiler_nir.c:1296
1 errors:
shader: MESA_SHADER_FRAGMENT
source_blake3: {0x4d463d73, 0x4b27d742, 0x27a92b64, 0x375c010f, 0xb2ce3767, 0x2adc55cc, 0x6da8105b, 0x5b9fce29}
name: GLSL1
prev_stage: MESA_SHADER_VERTEX
inputs_read: 32
outputs_written: 4
perspective_varyings: 32
max_subgroup_size: 128
min_subgroup_size: 1
api_subgroup_size_draw_uniform: true
first_ubo_is_default_ubo: true
known_interpolation_qualifiers: true
flrp_lowered: true
inputs: 1
outputs: 1
decl_var shader_in INTERP_MODE_SMOOTH none highp float packed:var0 (VARYING_SLOT_VAR0.x, 0, 0)
decl_var shader_out INTERP_MODE_NONE none mediump float out0 (FRAG_RESULT_DATA0.x, 0, 0)
decl_function main () (entrypoint)

impl main {
    block b0:  // preds:
    32    %3 = load_const (0x00000000)
    32    %4 = @load_input (%3 (0x0)) (base=0, range=1, component=0, dest_type=float32, io location=VARYING_SLOT_VAR0 slots=1)  // packed:var0
    32    %2 = deref_var &amp;out0 (shader_out mediump float)
    32    %5 = mov %4.z
error: src-&gt;swizzle[i] &lt; num_components (../mesa/src/compiler/nir/nir_validate.c:217)

               @store_deref (%2, %5) (wrmask=x, access=none)
               // succs: b1
    block b1:
}

FATAL ERROR: Test program crashed
</code></pre><h2 id="what-the-blob-does">What the blob does</h2>
<p>Looking at command stream traces from the proprietary driver, the answer is almost disappointingly simple. Instead of this:</p>
<pre tabindex="0"><code>  Texture format: A8B8G8R8     (read BGRA as BGRA)
  PE format:      A8B8G8R8     (write BGRA)
  Shader:         swap R &lt;-&gt; B
</code></pre><p>The blob does this:</p>
<pre tabindex="0"><code>  Texture format: A8R8G8B8     (read BGRA as RGBA - hardware swaps on read)
  PE format:      A8B8G8R8     (write BGRA - unchanged)
  Shader:         nothing
</code></pre><p>That&rsquo;s it. Tell the texture sampler the data is A8R8G8B8, and it will correctly interpret the BGRA bytes as RGBA channels. The PE keeps writing BGRA because that&rsquo;s what it does. No shader modification needed.</p>
<p>In our format table, the change is a single field:</p>
<pre tabindex="0"><code>  -  VT(R8G8B8A8_UNORM, UNSIGNED_BYTE, A8B8G8R8, A8B8G8R8)
  +  VT(R8G8B8A8_UNORM, UNSIGNED_BYTE, A8R8G8B8, A8B8G8R8)
                                        ^^^^^^^^
                                     texture format
</code></pre><h2 id="the-data-flow">The data flow</h2>
<p>To understand why this works, trace a red pixel through the pipeline:</p>
<pre tabindex="0"><code>                          BGRA-internal byte order
                         +------------------------+
                         |                        |
  API: glClear(1,0,0,1)  |   Memory: [0,0,255,255]|   CPU: expects [255,0,0,255]
  &#34;red = 1.0&#34;            |   (B=0, G=0, R=255,    |   &#34;RGBA order&#34;
                         |    A=255)              |
                         +------------------------+

  +-----------+     +--------+     +--------+     +---------+
  | Shader    |     | PE     |     | Memory |     | Sampler |
  | out=RGBA  | --&gt; | writes | --&gt; | stores | --&gt; | reads   |
  | (1,0,0,1) |     | BGRA   |     | BGRA   |     | as      |
  |           |     |        |     | bytes  |     | A8R8G8B8|
  +-----------+     +--------+     +--------+     +---------+
       |                |               |              |
    R=1.0            B=0x00          [00 00 FF FF]   R=1.0
    G=0.0            G=0x00                          G=0.0
    B=0.0            R=0xFF                          B=0.0
    A=1.0            A=0xFF                          A=1.0
</code></pre><p>The shader writes (1,0,0,1). The PE swaps R/B on write, so memory gets [0,0,255,255] in BGRA order. The sampler, told the format is A8R8G8B8, reads those same bytes back as (1,0,0,1). Round-trip complete, no shader involvement.</p>
<h2 id="the-cpu-boundary-problem">The CPU boundary problem</h2>
<p>GPU-to-GPU is clean. But what happens at the CPU boundary - <code>glReadPixels</code>, <code>glTexSubImage</code>, <code>glBlitFramebuffer</code> to a CPU-mapped buffer? The CPU expects RGBA byte order. Memory has BGRA. Something needs to swap.</p>
<p>This is where the hardware copy/resolve engines come in - <code>RS</code> and <code>BLT</code>. Both can perform R/B swapping during their copy operations. The RS engine has a <code>swap_rb</code> bit. The BLT engine has per-side swizzle fields. We just need to activate this at the right moment.</p>
<p>The key insight: only transfer blits (tiled-to-linear copies for CPU access) need the swap. GPU-internal blits - glBlitFramebuffer between two render targets, TS resolve, mipmap generation - are all operating on data already in BGRA order on both sides. Swapping there would be wrong.</p>
<p>So we gate it with a context flag:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="n">ctx</span><span class="o">-&gt;</span><span class="n">in_transfer_blit</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nf">etna_copy_resource_box</span><span class="p">(</span><span class="n">pctx</span><span class="p">,</span> <span class="n">trans</span><span class="o">-&gt;</span><span class="n">rsc</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">rsc</span><span class="o">-&gt;</span><span class="n">base</span><span class="p">,</span> <span class="p">...);</span>
</span></span><span class="line"><span class="cl"><span class="n">ctx</span><span class="o">-&gt;</span><span class="n">in_transfer_blit</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
</span></span></code></pre></div><p>And in the RS blit path:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="p">.</span><span class="n">swap_rb</span> <span class="o">=</span> <span class="n">ctx</span><span class="o">-&gt;</span><span class="n">in_transfer_blit</span> <span class="o">&amp;&amp;</span>
</span></span><span class="line"><span class="cl">           <span class="nf">translate_pe_format_rb_swap</span><span class="p">(</span><span class="n">blit_info</span><span class="o">-&gt;</span><span class="n">src</span><span class="p">.</span><span class="n">format</span><span class="p">),</span>
</span></span></code></pre></div><h2 id="the-texture-shadow-trap">The texture shadow trap</h2>
<p>With the basic approach working, tests passed on GC7000 (BLT engine). But GC2000 (RS engine) had a regression: <code>fbo-blit</code> showed blue where red should be.</p>
<p>After adding debug prints and tracing the code paths, the culprit was the texture shadow. Some resources can&rsquo;t be sampled directly by the texture unit - for example, a render target might use a layout the sampler doesn&rsquo;t understand. For these, the driver allocates a second copy of the resource in a sampler-compatible tiled layout. This is the &ldquo;texture shadow&rdquo;.</p>
<p>The shadow is a workaround that hurts performance - it means extra memory and extra copies. Ideally we wouldn&rsquo;t need it at all. But while it exists, the driver uses it as a shortcut for CPU transfers: read directly from the shadow and detile in software, skipping the blit engine:</p>
<pre tabindex="0"><code>Passing probes:  PATH=temp_resource+RS_blit    swap_rb=1   -&gt; correct
Failing probes:  PATH=texture_shadow                       -&gt; R/B swapped
</code></pre><p>The texture shadow path does a software detile - raw byte copy, no R/B swap. With BGRA-internal byte order, that gives you BGRA bytes on the CPU side. Wrong.</p>
<p>The fix: skip the texture shadow shortcut for formats that need R/B swap, forcing through the blit engine path which handles the conversion:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="n">rsc</span><span class="o">-&gt;</span><span class="n">texture</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="nf">etna_resource_newer</span><span class="p">(</span><span class="n">rsc</span><span class="p">,</span> <span class="nf">etna_resource</span><span class="p">(</span><span class="n">rsc</span><span class="o">-&gt;</span><span class="n">texture</span><span class="p">))</span> <span class="o">&amp;&amp;</span>
</span></span><span class="line"><span class="cl">    <span class="o">!</span><span class="nf">translate_pe_format_rb_swap</span><span class="p">(</span><span class="n">prsc</span><span class="o">-&gt;</span><span class="n">format</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="cm">/* Use texture shadow - safe, no R/B swap needed */</span>
</span></span><span class="line"><span class="cl">   <span class="n">rsc</span> <span class="o">=</span> <span class="nf">etna_resource</span><span class="p">(</span><span class="n">rsc</span><span class="o">-&gt;</span><span class="n">texture</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="cm">/* Use blit engine - handles R/B swap correctly */</span>
</span></span><span class="line"><span class="cl">   <span class="p">...</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h2 id="results">Results</h2>
<p>This is a net-negative patch series - 15 files changed, 76 insertions, 113 deletions. The <a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/38710">etnaviv: Remove RB swap logic in the fragment shader</a> contains all that&rsquo;s needed:</p>
<ol>
<li><strong>blt: Use img-&gt;swizzle for CONFIG SWIZ fields</strong> - preparation for per-image swizzle support</li>
<li><strong>Add translate_pe_internal_format helper</strong> - maps RGBA pipe formats to BGRA equivalents for clear color packing</li>
<li><strong>Use BGRA-internal texture format with BLT/RS R/B swizzle</strong> - the main change</li>
<li><strong>Compute blend color directly in etna_set_blend_color</strong> - simplifies blend color, no deferred update needed</li>
</ol>
<p>Fixes the NIR validation failure with scalar fragment outputs. And as a nice side effect, removing the shader-based swap means fewer shader variants, fewer instructions and less overhead. <a href="https://github.com/glmark2/glmark2">glmark2</a>-es2-wayland improves from ~835 to ~874 FPS - a 4.7% performance increase.</p>
<p>Sometimes matching what the hardware vendor does is the right answer. The blob driver figured this out years ago. We just needed to look at the traces.</p>
<h2 id="thats-where-i-thought-the-story-ended">That&rsquo;s where I thought the story ended</h2>
<p>The texture-format trick has a hidden assumption baked into it: that the GPU both writes <em>and</em> reads every resource. The PE writes BGRA, the sampler is told the format is A8R8G8B8, and the byte order cancels out. It&rsquo;s a closed loop, and as long as the data never leaves the GPU, nobody outside ever sees the BGRA bytes.</p>
<p><a href="https://docs.kernel.org/driver-api/dma-buf.html">dmabuf</a> breaks the loop.</p>
<p>When a buffer is shared with another process - a Wayland compositor, a video decoder, a camera - the byte order is no longer our private business. It&rsquo;s mandated by the <a href="https://docs.kernel.org/userspace-api/dma-buf-alloc-exchange.html">DRM FourCC</a>. An external producer writes honest RGBA bytes into the buffer. Then our sampler, still convinced the format is A8R8G8B8, reads them as BGRA. Red and blue swap. And on the way out, a transfer blit happily swaps data that was already correct. The optimization that made GPU-internal rendering clean made buffer sharing wrong.</p>
<pre tabindex="0"><code>  GPU-internal (closed loop):       dmabuf (loop broken):

  PE writes BGRA                    external producer writes RGBA
       |                                  |
  sampler reads as A8R8G8B8         sampler reads as A8R8G8B8
       |                                  |
  cancels out -&gt; correct            reads RGBA as BGRA -&gt; swapped
</code></pre><h2 id="step-back-out-for-shared-resources">Step back out for shared resources</h2>
<p>The first fix is the obvious one: when a resource is shared, don&rsquo;t play the trick. Use the native A8B8G8R8 texture format so the sampler reads RGBA bytes as RGBA, skip the R/B swizzle in the BLT and RS transfer blits, and re-enable the texture shadow shortcut that the swizzle had forced us to disable. Internal resources keep the BGRA-internal optimization untouched.</p>
<p>That handles imports. But there&rsquo;s a case it doesn&rsquo;t cover: a resource <em>we</em> rendered into and then export. The PE wrote BGRA, because that&rsquo;s all the PE knows how to do. The external consumer expects native order.</p>
<p>Here a second kind of shadow shows up. Just as the sampler gets a <em>texture shadow</em> when it can&rsquo;t read the base layout, the PE gets a <em>render shadow</em> - a render-compatible copy - when it can&rsquo;t draw into the base layout. (On some GPUs, like GC2000, no single tiling satisfies both the texture engine and the pixel engine, so a resource can end up carrying both shadows.) When an exported resource is flushed, that render shadow is resolved back to the base. So we hook <code>etna_flush_resource()</code> and do the R/B swap during that copy - using the BLT destination swizzle or the RS <code>SWAP_RB</code> bit. The swap rides along on a copy we were doing anyway.</p>
<h2 id="one-buffer-two-byte-orders">One buffer, two byte orders</h2>
<p>Now the same shared buffer can be in one of two states. Just imported, or just flushed for export? Native RGBA. Freshly rendered by the PE, not yet flushed? PE-internal BGRA. A static texture format chosen at sampler-view creation can&rsquo;t be right for both.</p>
<p>So the format choice becomes dynamic. A <code>shared_native_order</code> flag tracks which order the bytes are currently in, and the sampler-view format follows from it:</p>
<table>
  <thead>
      <tr>
          <th><code>shared_native_order</code></th>
          <th>How you get there</th>
          <th>Bytes in buffer</th>
          <th>Sampler format</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>true</code></td>
          <td>set on import, and again after <code>flush_resource()</code> swaps on export</td>
          <td>native RGBA</td>
          <td><code>A8B8G8R8</code> (native)</td>
      </tr>
      <tr>
          <td><code>false</code></td>
          <td>cleared when the PE renders straight into the buffer with no render shadow, and no shader swap fixed up the bytes (see below)</td>
          <td>PE-internal BGRA</td>
          <td><code>A8R8G8B8</code> (the trick)</td>
      </tr>
  </tbody>
</table>
<p>Both texture paths - state-based and descriptor-based - pre-compute the native format variant at sampler-view creation, so picking the right one at emit time costs a single branch, not a per-frame format recompute.</p>
<h2 id="the-shader-swap-comes-back">The shader swap comes back</h2>
<p>Which brings us to LINEAR_PE GPUs, and a twist I didn&rsquo;t see coming.</p>
<p>On these GPUs, a linear shared resource is render-compatible. There is no render shadow - the PE writes straight into the buffer that gets handed to the compositor. So after a draw, the buffer holds BGRA, and <code>flush_resource()</code> would have to issue a full-surface blit to swap it. That&rsquo;s real bandwidth, on every frame, that the old shadow-based path never paid.</p>
<p>There&rsquo;s a cheaper place to do the swap: in the shader, on the way out. Which is exactly the thing this whole series set out to delete.</p>
<p>So it comes back - but only for this one case, and done properly. A per-RT <code>frag_rb_swap</code> bitmask in the shader key drives a NIR lowering pass that swaps channels 0 and 2 on the fragment output. The original shader swap fell over on scalar outputs, because there was no <code>.z</code> to swizzle from. This one widens the output variable to vec4 first, padding the missing components with undef, then applies the swizzle with an adjusted writemask:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl">   <span class="cm">/* Pad source to 4 components (undef for missing) */</span>
</span></span><span class="line"><span class="cl">   <span class="n">nir_def</span> <span class="o">*</span><span class="n">padded</span> <span class="o">=</span> <span class="nf">nir_pad_vec4</span><span class="p">(</span><span class="o">&amp;</span><span class="n">b</span><span class="p">,</span> <span class="n">src</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">   <span class="cm">/* Swap R and B channels */</span>
</span></span><span class="line"><span class="cl">   <span class="kt">unsigned</span> <span class="n">swiz</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span><span class="mi">2</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">3</span><span class="p">};</span>
</span></span></code></pre></div><p>That&rsquo;s the scalar <code>rgb8_lowp_float</code> crash from the top of this post - fixed, in the one path that now needs a shader swap at all.</p>
<p>Wiring it up is a check in <code>etna_draw_vbo()</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl">   <span class="k">if</span> <span class="p">(</span><span class="nf">VIV_FEATURE</span><span class="p">(</span><span class="n">screen</span><span class="p">,</span> <span class="n">ETNA_FEATURE_LINEAR_PE</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">pfb</span><span class="o">-&gt;</span><span class="n">nr_cbufs</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">         <span class="k">struct</span> <span class="n">etna_resource</span> <span class="o">*</span><span class="n">rsc</span> <span class="o">=</span> <span class="nf">etna_resource</span><span class="p">(</span><span class="n">pfb</span><span class="o">-&gt;</span><span class="n">cbufs</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">texture</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">         <span class="k">if</span> <span class="p">(</span><span class="n">rsc</span><span class="o">-&gt;</span><span class="n">shared</span> <span class="o">&amp;&amp;</span> <span class="n">rsc</span><span class="o">-&gt;</span><span class="n">layout</span> <span class="o">==</span> <span class="n">ETNA_LAYOUT_LINEAR</span> <span class="o">&amp;&amp;</span>
</span></span><span class="line"><span class="cl">             <span class="nf">translate_pe_format_rb_swap</span><span class="p">(</span><span class="n">pfb</span><span class="o">-&gt;</span><span class="n">cbufs</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">format</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">            <span class="n">key</span><span class="p">.</span><span class="n">frag_rb_swap</span> <span class="o">|=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">i</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">   <span class="p">}</span>
</span></span></code></pre></div><p>The bitmask is per-RT, so an MRT setup with a mix of shared and private targets does the right thing for each. And because the shader produced native bytes directly, <code>shared_native_order</code> stays true and <code>flush_resource()</code> skips its blit entirely.</p>
<p>The fixes for all of this live in a follow-up series, <a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/40029">etnaviv: Fix dmabuf R/B byte order for PE_FORMAT_RB_SWAP formats</a>.</p>
<h2 id="so-was-removing-the-shader-swap-a-mistake">So was removing the shader swap a mistake?</h2>
<p>No - but it wasn&rsquo;t the whole answer either.</p>
<p>The shader swap was wrong as the <em>universal</em> solution. It cost a shader-key dimension, per-RT blend-color fixups, and it crashed on scalar outputs. The texture-format trick is genuinely better for the common case, where a resource lives and dies on the GPU.</p>
<p>What the dmabuf work showed is that there is no single right place to fix R/B order. There&rsquo;s a place that&rsquo;s cheapest for each path: the texture format for GPU-internal resources, a transfer-blit swap at the CPU boundary, a flush-time swap for exported render targets, and - for LINEAR_PE, where the PE writes straight into a shared buffer - the shader, after all. The trick isn&rsquo;t picking one. It&rsquo;s knowing which boundary you&rsquo;re standing on, and swapping there.</p>
]]></content:encoded>
    </item>
    <item>
      <title>PanVK Extension Sprint: Mesa 26.1</title>
      <link>https://christian-gmeiner.info/2026-04-20-panvk-extensions/</link>
      <pubDate>Mon, 20 Apr 2026 00:00:00 +0000</pubDate>
      <guid>https://christian-gmeiner.info/2026-04-20-panvk-extensions/</guid>
      <description>&lt;p&gt;Last week marks the Mesa 26.1 branch point, and I wanted to take a moment to look back at what happened on the PanVK front.&lt;/p&gt;
&lt;p&gt;Spoiler: it was a busy one.&lt;/p&gt;
&lt;h3 id=&#34;the-landscape&#34;&gt;The landscape&lt;/h3&gt;
&lt;p&gt;PanVK - the Vulkan driver for Arm Mali GPUs (Valhall and newer) - is a collaborative effort. &lt;a href=&#34;https://www.collabora.com/&#34;&gt;Collabora&lt;/a&gt; has been doing incredible work on the compiler backend and the foundational infrastructure. &lt;a href=&#34;https://www.arm.com/&#34;&gt;Arm&lt;/a&gt; themselves are actively contributing to the open source Mali GPU stack as well, reviewing patches and pushing driver quality forward. On the &lt;a href=&#34;https://www.igalia.com/&#34;&gt;Igalia&lt;/a&gt; side, my focus this cycle was &lt;strong&gt;Vulkan extension coverage&lt;/strong&gt;. The kind of work that doesn&amp;rsquo;t make for flashy demos but is absolutely critical for real-world application compatibility - especially for things like &lt;a href=&#34;https://github.com/doitsujin/dxvk&#34;&gt;DXVK&lt;/a&gt;.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Last week marks the Mesa 26.1 branch point, and I wanted to take a moment to look back at what happened on the PanVK front.</p>
<p>Spoiler: it was a busy one.</p>
<h3 id="the-landscape">The landscape</h3>
<p>PanVK - the Vulkan driver for Arm Mali GPUs (Valhall and newer) - is a collaborative effort. <a href="https://www.collabora.com/">Collabora</a> has been doing incredible work on the compiler backend and the foundational infrastructure. <a href="https://www.arm.com/">Arm</a> themselves are actively contributing to the open source Mali GPU stack as well, reviewing patches and pushing driver quality forward. On the <a href="https://www.igalia.com/">Igalia</a> side, my focus this cycle was <strong>Vulkan extension coverage</strong>. The kind of work that doesn&rsquo;t make for flashy demos but is absolutely critical for real-world application compatibility - especially for things like <a href="https://github.com/doitsujin/dxvk">DXVK</a>.</p>
<h3 id="why-extensions-matter">Why extensions matter</h3>
<p>A Vulkan driver without extensions is like a car without wheels - technically complete, practically useless. Applications (and translation layers like DXVK, vkd3d-proton, and Zink) probe for specific extensions and adjust their behavior accordingly. Missing even one can mean falling back to a slower path or refusing to run entirely.</p>
<p>Three different things drove the extension work this cycle:</p>
<ul>
<li><strong>The Proton stack</strong> - extensions consumed by DXVK and vkd3d-proton, the translation layers that make D3D9–12 games run on Vulkan.</li>
<li><strong>DDK feature parity</strong> - extensions Arm&rsquo;s binary Mali driver exposes that PanVK didn&rsquo;t yet, tracked in the <a href="https://gitlab.freedesktop.org/panfrost/mesa/-/work_items/125">DDK feature parity ticket</a>.</li>
<li><strong>Catching up on <a href="https://mesamatrix.net/">mesamatrix.net</a></strong> - closing the visible gap with the other Mesa Vulkan drivers (RADV, ANV, Turnip).</li>
</ul>
<p>So I set out to close gaps. Lots of them.</p>
<h3 id="the-proton-stack-essentials">The Proton stack essentials</h3>
<p>These are extensions DXVK and vkd3d-proton actually require - not just nice-to-haves on a recommendation list. Each one unblocks something concrete in the D3D-to-Vulkan translation path.</p>
<p><strong><code>VK_EXT_conditional_rendering</code></strong> (<a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/40452">!40452</a>) was probably the most involved piece of work. D3D12 has predicated rendering (<code>SetPredication</code>), and vkd3d-proton uses this extension to implement it efficiently. It wasn&rsquo;t a simple &ldquo;flip a bit&rdquo; situation - I had to add the core state tracking, wrap all draw and dispatch calls with conditional checks, handle inherited state in secondary command buffers, and make sure meta operations (like internal clears and resolves) properly disable conditional rendering so they don&rsquo;t get accidentally skipped. That ended up being five patches touching draw paths, dispatch, and the secondary command buffer inheritance logic.</p>
<p><strong><code>VK_VALVE_mutable_descriptor_type</code></strong> (<a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/40254">!40254</a>) is one of those extensions that exists purely because Valve needed it. In D3D, descriptor types are more fluid than in Vulkan - a descriptor slot might hold a sampler one frame and a storage buffer the next. vkd3d-proton enables this to avoid expensive descriptor set re-creation when types change. It&rsquo;s a trivial alias of the already-supported <code>VK_EXT_mutable_descriptor_type</code>, so enabling it was a one-liner.</p>
<p><strong><code>VK_EXT_memory_budget</code></strong> (<a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/40246">!40246</a>) lets applications (and both DXVK and vkd3d-proton) query how much GPU memory is actually available versus how much is in use. Without it, apps are flying blind on memory management, which can lead to over-allocation and stuttering. Getting the heap budget reporting right required hooking into the kernel memory accounting.</p>
<p><strong><code>VK_EXT_attachment_feedback_loop_layout</code></strong> (<a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/40498">!40498</a>) - feedback loops let you read from an attachment that you&rsquo;re simultaneously rendering to (think screen-space effects that sample the current framebuffer). DXVK uses this in its D3D9 hazard layout path to avoid artifacts in certain games.</p>
<p><strong><code>VK_EXT_shader_stencil_export</code></strong> (<a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/39944">!39944</a>) - allows fragment shaders to write stencil values directly, rather than relying on the fixed-function stencil path. DXVK leans on this in its meta-copy and meta-resolve paths, and vkd3d-proton enables it too. The Panfrost stack already supported everything needed; literally a one-line advertisement in <code>physical_device.c</code>.</p>
<p><strong><code>VK_KHR_shader_untyped_pointers</code></strong> (<a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/40457">!40457</a>, v9+) - a newer KHR extension that relaxes pointer type requirements in SPIR-V. DXVK calls this out as a dependency for descriptor heaps. Restricted to v9+ because Bifrost has issues with 8-bit vector loads through untyped pointers combined with 16-bit storage. Also needed to lower memcpy derefs before explicit IO lowering.</p>
<h3 id="catching-up-to-the-ddk">Catching up to the DDK</h3>
<p>The panfrost keeps a <a href="https://gitlab.freedesktop.org/panfrost/mesa/-/work_items/125">DDK feature parity ticket</a> tracking everything Arm&rsquo;s binary Mali driver exposes that PanVK doesn&rsquo;t yet. Four of those got crossed off this cycle:</p>
<ul>
<li><strong><code>VK_ARM_scheduling_controls</code></strong> (<a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/40063">!40063</a>, CSF only) - an ARM-specific extension for controlling shader core scheduling on Command Stream Frontend (CSF) hardware. I also fixed the per-queue shader core count so CSF group creation uses the right values.</li>
<li><strong><code>VK_EXT_legacy_dithering</code></strong> (<a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/39781">!39781</a>) - implements ordered dithering in the blending stage, which some applications expect from legacy APIs. Wired up the existing Panfrost dithering infrastructure (<code>pan_dithered_format_from_pipe_format()</code>) — just plumbing the <code>VK_RENDERING_ENABLE_LEGACY_DITHERING_BIT_EXT</code> flag through the blend descriptor and color attachment internal conversion paths.</li>
<li><strong><code>VK_EXT_rgba10x6_formats</code></strong> (<a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/40653">!40653</a>) - a last-minute addition that just squeezed in before the branch point. This required adding the <code>PIPE_FORMAT_X6R10X6G10X6B10X6A10_UNORM</code> format to Mesa&rsquo;s gallium format table first, then wiring it up in PanVK. Used for 10-bit per channel content in video and HDR scenarios.</li>
<li><strong><code>VK_EXT_astc_decode_mode</code></strong> (<a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/39799">!39799</a>) - controls the format used when decoding ASTC compressed textures, allowing apps to choose lower-precision decoding for performance. The Panfrost hardware already supports controlling ASTC decode precision via the Decode Wide plane descriptor field; just needed to parse <code>VkImageViewASTCDecodeModeEXT</code> from the image view pNext chain and set <code>astc.narrow</code> accordingly. v9+ only because the relevant ASTC plane descriptor fields only exist from Valhall onward.</li>
</ul>
<h3 id="catching-up-on-mesamatrix">Catching up on mesamatrix</h3>
<p><a href="https://mesamatrix.net/">mesamatrix.net</a> tracks Vulkan extension support across the Mesa drivers. The remaining extensions this cycle were about closing the visible gap with RADV, ANV, and Turnip — extensions that don&rsquo;t have a single big consumer driving them, but whose absence shows up as red squares on the matrix and as silent fallbacks in apps that probe for them.</p>
<ul>
<li><strong><code>VK_EXT_color_write_enable</code></strong> (<a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/39913">!39913</a>) - per-attachment control over which color channels actually get written. The common Vulkan runtime already handled all the pipeline state and dynamic command plumbing, and panvk&rsquo;s blend descriptor emission was already consuming <code>color_write_enables</code>, so this was effectively an &ldquo;advertise the feature&rdquo; change.</li>
<li><strong><code>VK_EXT_depth_clamp_control</code></strong> (<a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/39925">!39925</a>) - lets applications specify a custom depth clamp range instead of always clamping to the viewport&rsquo;s minDepth/maxDepth. Mali GPUs have native <code>LOW_DEPTH_CLAMP</code>/<code>HIGH_DEPTH_CLAMP</code> registers, so it was a matter of wiring the existing runtime state through to those.</li>
<li><strong><code>VK_EXT_attachment_feedback_loop_dynamic_state</code></strong> (<a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/40498">!40498</a>) - the dynamic-state companion to <code>VK_EXT_attachment_feedback_loop_layout</code> above; lets you toggle feedback-loop state per draw call without pipeline rebuilds.</li>
<li><strong><code>VK_EXT_map_memory_placed</code></strong> (<a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/40315">!40315</a>) - lets applications control <em>where</em> in their virtual address space GPU memory gets mapped. This simplified <code>pan_kmod_bo_mmap()</code> to always map the whole BO, cleaning up the kernel module interface.</li>
<li><strong><code>VK_EXT_shader_atomic_float</code></strong> (<a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/40506">!40506</a>) - atomic operations on float values in shaders. The existing <code>axchg</code> instruction is type-agnostic, so no compiler changes were needed; image atomics are already lowered to global atomics. Just had to add <code>R32_FLOAT</code> to the storage-image-atomic format flag.</li>
<li><strong><code>VK_EXT_nested_command_buffer</code></strong> (<a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/40120">!40120</a>, v10+) - allows secondary command buffers to call other secondary command buffers. The CSF backend&rsquo;s <code>cs_call()</code> is a hardware call/return instruction that nests naturally, and the existing <code>CmdExecuteCommands</code> already does the caller/callee state merging. The 8-level hardware call stack, minus one for the kernel ringbuffer call and two reserved for future driver use, leaves <code>maxCommandBufferNestingLevel</code> at 5.</li>
<li><strong><code>VK_EXT_image_view_min_lod</code></strong> (<a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/39938">!39938</a>) - allows clamping the minimum LOD at the image view level rather than just the sampler. Mali v6+ has per-texture-descriptor LOD clamp fields independent from the sampler&rsquo;s, so this just plumbs <code>vk_image_view::min_lod</code> through <code>pan_image_view</code> into the texture descriptor — no shader lowering or descriptor merging needed.</li>
<li><strong><code>VK_EXT_zero_initialize_device_memory</code></strong> (<a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/39658">!39658</a>) - guarantees that newly allocated device memory is zeroed. The kernel side already does the heavy lifting — <code>panfrost</code>/<code>panthor</code> use <code>drm_gem_shmem</code>, which serves zeroed pages from the shmem subsystem. And since panvk treats layout transitions as no-ops, <code>VK_IMAGE_LAYOUT_ZERO_INITIALIZED_EXT</code> falls out for free. (Did need one format-table fix: dropping <code>STORAGE_IMAGE</code> support from compressed formats to avoid crashes in the new dEQP tests.)</li>
</ul>
<h3 id="by-the-numbers">By the numbers</h3>
<p>That&rsquo;s <strong>18 extensions</strong> across roughly a dozen merge requests - ranging from single-patch additions to multi-patch series like conditional rendering. Collectively they represent a meaningful shift in what PanVK can claim to support: more of the Proton stack working out of the box, four more checkboxes against the DDK, and fewer red squares on the mesamatrix.</p>
<h3 id="whats-next">What&rsquo;s next</h3>
<p>The extension sprint isn&rsquo;t over - there are still gaps to fill, and each one removed makes PanVK more viable for real workloads. But 26.1 was a good milestone. The driver is getting to the point where you can throw a DXVK game at it and have a reasonable expectation that it just works.</p>
<p>Back to it. ⚡</p>
]]></content:encoded>
    </item>
    <item>
      <title>GLES3 on etnaviv: Fixing the Hard Parts</title>
      <link>https://christian-gmeiner.info/2026-02-20-gles3-on-etnaviv-fixing-the-hard-parts/</link>
      <pubDate>Fri, 20 Feb 2026 00:00:00 +0000</pubDate>
      <guid>https://christian-gmeiner.info/2026-02-20-gles3-on-etnaviv-fixing-the-hard-parts/</guid>
      <description>&lt;p&gt;This is the start of a series about getting OpenGL ES 3.0 conformance on Vivante GC7000 hardware using the open-source etnaviv driver in Mesa. Thanks to &lt;a href=&#34;https://www.igalia.com/&#34;&gt;Igalia&lt;/a&gt; for giving me the opportunity to spend some time on these topics.&lt;/p&gt;
&lt;h2 id=&#34;where-we-are&#34;&gt;Where We Are&lt;/h2&gt;
&lt;p&gt;etnaviv has supported GLES2 on Vivante GPUs for a long time. GLES3 support has been progressing steadily, but the remaining dEQP failures are the stubborn ones - the cases where the hardware doesn&amp;rsquo;t quite do what the spec says, and the driver has to get creative.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>This is the start of a series about getting OpenGL ES 3.0 conformance on Vivante GC7000 hardware using the open-source etnaviv driver in Mesa. Thanks to <a href="https://www.igalia.com/">Igalia</a> for giving me the opportunity to spend some time on these topics.</p>
<h2 id="where-we-are">Where We Are</h2>
<p>etnaviv has supported GLES2 on Vivante GPUs for a long time. GLES3 support has been progressing steadily, but the remaining dEQP failures are the stubborn ones - the cases where the hardware doesn&rsquo;t quite do what the spec says, and the driver has to get creative.</p>
<p>These aren&rsquo;t missing feature bits or unimplemented extensions. These are the problems where you stare at a command stream trace from the proprietary blob driver and realize they&rsquo;re doing something <em>completely different</em> from what you&rsquo;d expect, because the hardware has a quirk that nobody documented.</p>
<h2 id="the-approach">The Approach</h2>
<p>My workflow for each fix follows a pattern:</p>
<ol>
<li><strong>Run the failing dEQP test</strong>, note the failure mode (wrong pixels, crash, GPU hang)</li>
<li><strong>Capture command stream traces</strong> from both the blob (proprietary driver) and etnaviv for the same test</li>
<li><strong>Compare the traces</strong> - what states differ? What draw calls differ? Is the blob doing extra work?</li>
<li><strong>Understand why</strong> - read the spec, reason about the hardware behavior, figure out what the blob knows that we don&rsquo;t</li>
<li><strong>Implement the fix</strong> in Mesa, test, iterate</li>
</ol>
<p>The blob traces are invaluable. Vivante&rsquo;s proprietary driver has years of hardware workarounds baked in. When something doesn&rsquo;t work, the answer is usually hiding in the trace.</p>
<h2 id="the-hardware">The Hardware</h2>
<p>The primary test target is a GC7000 rev 6214 (HALTI5 generation). This is a capable GPU found in the NXP i.MX8MQ SoC. It has a BLT engine, texture descriptors, and most of the features needed for GLES3 - but also its own set of rasterization quirks and interpolation behaviors that need workarounds.</p>
<p>In the future, I plan to expand the focus to the broader GC7000 GPU family.</p>
<h2 id="up-next">Up Next</h2>
<p>The first post will tackle the R/B swap problem - the PE always writes pixels in BGRA byte order, and we&rsquo;ve been fixing it in the shader. The blob has a simpler answer. Stay tuned.</p>
<h2 id="following-along">Following Along</h2>
<p>All of this work happens upstream in <a href="https://gitlab.freedesktop.org/mesa/mesa">Mesa</a>.</p>
<p>If you&rsquo;re interested in GPU driver development, these posts aim to show what the work actually looks like &ndash; not just the final patch, but the debugging, the trace analysis, and the reasoning that gets you there.</p>
]]></content:encoded>
    </item>
    <item>
      <title>My first Vulkan extension</title>
      <link>https://christian-gmeiner.info/2026-02-13-my-first-vulkan-extension/</link>
      <pubDate>Fri, 13 Feb 2026 00:00:00 +0000</pubDate>
      <guid>https://christian-gmeiner.info/2026-02-13-my-first-vulkan-extension/</guid>
      <description>&lt;p&gt;After years of working on etnaviv - a Gallium/OpenGL driver for Vivante GPUs - I&amp;rsquo;ve been wanting to get into Vulkan. As part of my work at &lt;a href=&#34;https://www.igalia.com/&#34;&gt;Igalia&lt;/a&gt;, the goal was to bring &lt;a href=&#34;https://docs.vulkan.org/refpages/latest/refpages/source/VK_EXT_blend_operation_advanced.html&#34;&gt;&lt;code&gt;VK_EXT_blend_operation_advanced&lt;/code&gt;&lt;/a&gt; to lavapipe. But rather than going straight there, I started with Honeykrisp - the Vulkan driver for Apple Silicon - as a first target: a real hardware driver to validate the implementation against before wiring it up in a software renderer. My first Vulkan extension, and my first real contribution to Honeykrisp.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>After years of working on etnaviv - a Gallium/OpenGL driver for Vivante GPUs - I&rsquo;ve been wanting to get into Vulkan. As part of my work at <a href="https://www.igalia.com/">Igalia</a>, the goal was to bring <a href="https://docs.vulkan.org/refpages/latest/refpages/source/VK_EXT_blend_operation_advanced.html"><code>VK_EXT_blend_operation_advanced</code></a> to lavapipe. But rather than going straight there, I started with Honeykrisp - the Vulkan driver for Apple Silicon - as a first target: a real hardware driver to validate the implementation against before wiring it up in a software renderer. My first Vulkan extension, and my first real contribution to Honeykrisp.</p>
<h1 id="why-this-extension">Why this extension?</h1>
<p>A customer needed advanced blending support in lavapipe, so the extension choice was made for me. But it turned out to be a great fit for a first extension - useful, self-contained, and not a multi-month rabbit hole. Standard Vulkan blending is limited to basic operations like add and subtract with blend factors. That&rsquo;s fine for most rendering, but if you want Photoshop-style effects - multiply, screen, overlay, color dodge, color burn - you&rsquo;re stuck doing it manually in shaders or with extra render passes.</p>
<p>The extension adds 19 blend operations that handle all of this in the fixed-function pipeline. Useful for UI toolkits, image editors, and anywhere you need creative compositing.</p>
<h1 id="the-journey">The journey</h1>
<p>What started as &ldquo;just wire up an extension&rdquo; turned into a proper refactoring adventure. The existing blend infrastructure in Mesa was scattered - OpenGL had its own enum definitions, Vulkan had separate conversions, and the actual NIR blend math lived in glsl-specific code.</p>
<p>So I took a step back and cleaned things up. Moved the blend mode enums into a shared util/blend.h header. Added proper helpers in the Vulkan runtime for converting between API types. Then came the fun part: implementing the actual blend equations in nir/lower_blend.</p>
<p>Each of those 19 blend modes has its own formula from the spec. Some are simple (multiply is just src * dst), others get hairy with conditionals and special cases for luminosity and saturation. About 570 lines of NIR code later, I had a lowering pass that any Mesa driver can use.</p>
<p>For example, here&rsquo;s how the <a href="https://docs.vulkan.org/spec/latest/chapters/framebuffer.html#framebuffer-blend-advanced">spec defines advanced blending</a>. Each mode plugs into a general equation:</p>
<pre tabindex="0"><code>RGB = f(Cs,Cd) * X * p0 + Cs * Y * p1 + Cd * Z * p2
A   =            X * p0 +      Y * p1 +      Z * p2
</code></pre><p>Where <code>p0</code>, <code>p1</code>, <code>p2</code> are weighting factors based on source/destination alpha coverage, and <code>f(Cs,Cd)</code> is the per-mode blend function. For overlay - probably the most recognizable blend mode from Photoshop - the spec defines:</p>
<pre tabindex="0"><code>f(Cs,Cd) = 2*Cs*Cd,              if Cd &lt;= 0.5
           1 - 2*(1-Cs)*(1-Cd),  otherwise
</code></pre><p>And here&rsquo;s that same formula expressed as NIR - Mesa&rsquo;s intermediate representation:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">static</span> <span class="kr">inline</span> <span class="n">nir_def</span> <span class="o">*</span>
</span></span><span class="line"><span class="cl"><span class="nf">blend_overlay</span><span class="p">(</span><span class="n">nir_builder</span> <span class="o">*</span><span class="n">b</span><span class="p">,</span> <span class="n">nir_def</span> <span class="o">*</span><span class="n">src</span><span class="p">,</span> <span class="n">nir_def</span> <span class="o">*</span><span class="n">dst</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="cm">/* f(Cs,Cd) = 2*Cs*Cd, if Cd &lt;= 0.5
</span></span></span><span class="line"><span class="cl"><span class="cm">    *            1-2*(1-Cs)*(1-Cd), otherwise
</span></span></span><span class="line"><span class="cl"><span class="cm">    */</span>
</span></span><span class="line"><span class="cl">   <span class="n">nir_def</span> <span class="o">*</span><span class="n">rule_1</span> <span class="o">=</span> <span class="nf">nir_fmul</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="nf">nir_fmul</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="n">src</span><span class="p">,</span> <span class="n">dst</span><span class="p">),</span> <span class="nf">imm3</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="mf">2.0</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">   <span class="n">nir_def</span> <span class="o">*</span><span class="n">rule_2</span> <span class="o">=</span>
</span></span><span class="line"><span class="cl">      <span class="nf">nir_fsub</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="nf">imm3</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">               <span class="nf">nir_fmul</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="nf">nir_fmul</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="nf">nir_fsub</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="nf">imm3</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">),</span> <span class="n">src</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                                        <span class="nf">nir_fsub</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="nf">imm3</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">),</span> <span class="n">dst</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">                         <span class="nf">imm3</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="mf">2.0</span><span class="p">)));</span>
</span></span><span class="line"><span class="cl">   <span class="k">return</span> <span class="nf">nir_bcsel</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="nf">nir_fge</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="nf">imm3</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="mf">0.5f</span><span class="p">),</span> <span class="n">dst</span><span class="p">),</span> <span class="n">rule_1</span><span class="p">,</span> <span class="n">rule_2</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><code>nir_fmul</code>, <code>nir_fsub</code>, <code>nir_bcsel</code> - multiply, subtract, conditional select. Each call builds a node in the shader&rsquo;s IR graph. This is what &ldquo;lowering&rdquo; looks like: translating a high-level blend mode into operations the GPU&rsquo;s shader core can execute. The outer framework - the <code>p0</code>/<code>p1</code>/<code>p2</code> weighting - is handled by the caller; each blend function just implements its <code>f(Cs,Cd)</code>.</p>
<h1 id="the-turnip-surprise">The Turnip surprise</h1>
<p>Everything was working on Honeykrisp, tests were passing, life was good. Then the merge pipeline started failing - on Turnip (the Adreno Vulkan driver). Not my code, not my hardware, but my changes were breaking it.</p>
<p>I reached out for help, and Zan Dobersek stepped up. After some digging, he found the culprit: I was violating a subtle corner of the spec around attachmentCount. Turns out, when certain dynamic states are set and advancedBlendCoherentOperations isn&rsquo;t enabled, attachmentCount gets ignored entirely. My state tracking code wasn&rsquo;t accounting for that.</p>
<p>One fixup commit later, Turnip was happy again. This is the part they don&rsquo;t tell you about Vulkan extensions - you&rsquo;re not just implementing for your driver, you&rsquo;re touching shared infrastructure that every driver depends on. And the Mesa CI will absolutely let you know if you break something.</p>
<h1 id="lavapipe-landed">lavapipe landed</h1>
<p>One week later, lavapipe has it too. This was the original goal, and the shared infrastructure did exactly what it was supposed to - the <a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/39612">lavapipe MR</a> is mostly just flipping the extension on. The lowering pass, the enum plumbing, the runtime helpers - all reused as-is. The full <code>dEQP-VK.pipeline.*.blend_operation_advanced.*</code> test suite passes on both drivers.</p>
<p>Two drivers in two weeks. That&rsquo;s what building the right abstractions gets you.</p>
<h1 id="whats-next">What&rsquo;s next</h1>
<p>The shared NIR lowering pass is there for any Mesa Vulkan driver to use. If your hardware doesn&rsquo;t have native advanced blending support, enabling the extension is now mostly plumbing. I&rsquo;m curious to see if other drivers pick it up.</p>
<p>For me, this was a good first step into Vulkan - and into working on Honeykrisp. I&rsquo;m looking forward to what comes next.</p>
<p>The <a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/38929">Honeykrisp/NIR MR</a> and the <a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/39612">lavapipe MR</a> are both merged if you want to look at the code. Thanks to Alyssa Rosenzweig for the review and guidance, and to Zan Dobersek for debugging the Turnip regression with me.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
