After years of working on etnaviv - a Gallium/OpenGL driver for Vivante GPUs - I’ve been wanting to get into Vulkan. As part of my work at Igalia, the goal was to bring VK_EXT_blend_operation_advanced 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.
Why this extension?
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’s fine for most rendering, but if you want Photoshop-style effects - multiply, screen, overlay, color dodge, color burn - you’re stuck doing it manually in shaders or with extra render passes.
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.
The journey
What started as “just wire up an extension” 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.
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.
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.
For example, here’s how the spec defines advanced blending. Each mode plugs into a general equation:
RGB = f(Cs,Cd) * X * p0 + Cs * Y * p1 + Cd * Z * p2
A = X * p0 + Y * p1 + Z * p2
Where p0, p1, p2 are weighting factors based on source/destination alpha coverage, and f(Cs,Cd) is the per-mode blend function. For overlay - probably the most recognizable blend mode from Photoshop - the spec defines:
f(Cs,Cd) = 2*Cs*Cd, if Cd <= 0.5
1 - 2*(1-Cs)*(1-Cd), otherwise
And here’s that same formula expressed as NIR - Mesa’s intermediate representation:
static inline nir_def *
blend_overlay(nir_builder *b, nir_def *src, nir_def *dst)
{
/* f(Cs,Cd) = 2*Cs*Cd, if Cd <= 0.5
* 1-2*(1-Cs)*(1-Cd), otherwise
*/
nir_def *rule_1 = nir_fmul(b, nir_fmul(b, src, dst), imm3(b, 2.0));
nir_def *rule_2 =
nir_fsub(b, imm3(b, 1.0),
nir_fmul(b, nir_fmul(b, nir_fsub(b, imm3(b, 1.0), src),
nir_fsub(b, imm3(b, 1.0), dst)),
imm3(b, 2.0)));
return nir_bcsel(b, nir_fge(b, imm3(b, 0.5f), dst), rule_1, rule_2);
}
nir_fmul, nir_fsub, nir_bcsel - multiply, subtract, conditional select. Each call builds a node in the shader’s IR graph. This is what “lowering” looks like: translating a high-level blend mode into operations the GPU’s shader core can execute. The outer framework - the p0/p1/p2 weighting - is handled by the caller; each blend function just implements its f(Cs,Cd).
The Turnip surprise
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.
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’t enabled, attachmentCount gets ignored entirely. My state tracking code wasn’t accounting for that.
One fixup commit later, Turnip was happy again. This is the part they don’t tell you about Vulkan extensions - you’re not just implementing for your driver, you’re touching shared infrastructure that every driver depends on. And the Mesa CI will absolutely let you know if you break something.
lavapipe landed
One week later, lavapipe has it too. This was the original goal, and the shared infrastructure did exactly what it was supposed to - the lavapipe MR is mostly just flipping the extension on. The lowering pass, the enum plumbing, the runtime helpers - all reused as-is. The full dEQP-VK.pipeline.*.blend_operation_advanced.* test suite passes on both drivers.
Two drivers in two weeks. That’s what building the right abstractions gets you.
What’s next
The shared NIR lowering pass is there for any Mesa Vulkan driver to use. If your hardware doesn’t have native advanced blending support, enabling the extension is now mostly plumbing. I’m curious to see if other drivers pick it up.
For me, this was a good first step into Vulkan - and into working on Honeykrisp. I’m looking forward to what comes next.
The Honeykrisp/NIR MR and the lavapipe MR 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.