<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Borys Kharchenko</title>
    <description>The latest articles on DEV Community by Borys Kharchenko (@arximus).</description>
    <link>https://dev.to/arximus</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1716936%2F52402179-80ed-4bf9-8f92-fef1883aefd7.jpg</url>
      <title>DEV Community: Borys Kharchenko</title>
      <link>https://dev.to/arximus</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arximus"/>
    <language>en</language>
    <item>
      <title>Why Figma's new shader effects don't render in Electron on Linux and the GaneshGL + interop fix</title>
      <dc:creator>Borys Kharchenko</dc:creator>
      <pubDate>Thu, 25 Jun 2026 13:22:00 +0000</pubDate>
      <link>https://dev.to/arximus/why-figmas-new-shader-effects-dont-render-in-electron-on-linux-and-the-ganeshgl-interop-fix-353</link>
      <guid>https://dev.to/arximus/why-figmas-new-shader-effects-dont-render-in-electron-on-linux-and-the-ganeshgl-interop-fix-353</guid>
      <description>&lt;p&gt;Figma's Config 2026 shader fills (dithering, blur, frosted glass, fractal noise, liquid metal) run on &lt;strong&gt;WebGPU&lt;/strong&gt;. They render fine in Chrome on Linux. Wrap the same Figma in &lt;strong&gt;Electron&lt;/strong&gt; on Linux + AMD, and they silently don't show up.&lt;/p&gt;

&lt;p&gt;I chased this on an AMD Radeon 780M (Mesa RADV, Wayland/GNOME) for a self-hosted Electron Figma client. The fix turned out to be the &lt;em&gt;opposite&lt;/em&gt; of what every "enable Skia Graphite" thread suggests. Here's the whole journey, with a reproducible probe harness.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TLDR Don't enable Skia Graphite. Use &lt;code&gt;--ozone-platform=x11 --ignore-gpu-blocklist --enable-unsafe-webgpu&lt;/code&gt; (no &lt;code&gt;--enable-skia-graphite&lt;/code&gt;). That keeps the fast &lt;strong&gt;GaneshGL&lt;/strong&gt; raster backend while turning on &lt;strong&gt;&lt;code&gt;webgpu_on_vk_via_gl_interop&lt;/code&gt;&lt;/strong&gt;, which is what actually lets the shaders run — and without the canvas lag Graphite introduces. And apply those switches &lt;strong&gt;before &lt;code&gt;app.ready&lt;/code&gt;&lt;/strong&gt;, or Chromium ignores them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The wrong assumption: "you need Skia Graphite"
&lt;/h2&gt;

&lt;p&gt;The intuitive theory: Figma's WGSL shaders need Chromium's new &lt;strong&gt;Skia Graphite&lt;/strong&gt; backend (Dawn → Vulkan), and release Electron keeps Skia on the old &lt;strong&gt;Ganesh-GL&lt;/strong&gt; path, so shaders can't reach the GPU.&lt;/p&gt;

&lt;p&gt;So I built a probe — a bare Electron &lt;code&gt;BrowserWindow&lt;/code&gt; that renders a WebGL triangle + a WebGPU clear pass, then dumps &lt;code&gt;app.getGPUFeatureStatus()&lt;/code&gt;, &lt;code&gt;getGPUInfo()&lt;/code&gt;, the WebGL renderer string, and the WebGPU adapter — and ran it across a flag matrix on multiple Electron versions. (Harness + raw dumps in the gist: &lt;strong&gt;[LINK TO GIST]&lt;/strong&gt;.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First gotcha:&lt;/strong&gt; &lt;code&gt;getGPUFeatureStatus()&lt;/code&gt; reports "everything software/off" if you read it at &lt;code&gt;whenReady&lt;/code&gt; — the GPU process hasn't reported yet. Read it &lt;em&gt;after&lt;/em&gt; the window has actually rendered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second gotcha — switch vs. feature:&lt;/strong&gt; &lt;code&gt;--enable-features=SkiaGraphite&lt;/code&gt; hits a guard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gpu/config/gpu_finch_features.cc:650]
  Enabling Graphite on a not-yet-supported platform is disallowed for safety
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That guard is about &lt;strong&gt;Wayland ozone&lt;/strong&gt;, not Linux in general. The dedicated &lt;strong&gt;switch&lt;/strong&gt; &lt;code&gt;--enable-skia-graphite&lt;/code&gt; bypasses it, &lt;em&gt;and&lt;/em&gt; you need X11 ozone. Minimal recipe that actually flips Graphite on (verified against real Chromium 149 → &lt;code&gt;Skia Backend: GraphiteDawnVulkan&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;electron &lt;span class="nt"&gt;--ozone-platform&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;x11 &lt;span class="nt"&gt;--enable-skia-graphite&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It worked. &lt;code&gt;chrome://gpu&lt;/code&gt; showed &lt;code&gt;Skia Backend: GraphiteDawnVulkan&lt;/code&gt;. Victory?&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch: Graphite is slow here
&lt;/h2&gt;

&lt;p&gt;With Graphite on, &lt;strong&gt;panning the canvas lagged&lt;/strong&gt;. Noticeably. And the same flags in raw Chromium lagged identically — so it's not an Electron artifact, it's GraphiteDawnVulkan on an integrated GPU + Mesa being heavy for this workload.&lt;/p&gt;

&lt;p&gt;So I had working shaders but worse performance. Toggling it off restored speed but killed the shaders. Dead end — until an accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  The accident: a third, better state
&lt;/h2&gt;

&lt;p&gt;I turned the in-app toggle &lt;strong&gt;off&lt;/strong&gt;, the app asked to restart, I clicked restart — and the shaders &lt;strong&gt;kept working&lt;/strong&gt;, &lt;em&gt;with the good performance back&lt;/em&gt;. &lt;code&gt;chrome://gpu&lt;/code&gt; said &lt;code&gt;Skia Backend: GaneshGL&lt;/code&gt; (Graphite off!) but &lt;strong&gt;&lt;code&gt;WebGPU interop: Hardware accelerated&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Then I fully quit and cold-started — shaders gone, interop off again.&lt;/p&gt;

&lt;p&gt;I saved all three &lt;code&gt;chrome://gpu&lt;/code&gt; dumps. The &lt;code&gt;Command Line&lt;/code&gt; field (chrome://gpu prints the real process argv) told the story:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;ozone&lt;/th&gt;
&lt;th&gt;skia-graphite&lt;/th&gt;
&lt;th&gt;ignore-blocklist&lt;/th&gt;
&lt;th&gt;unsafe-webgpu&lt;/th&gt;
&lt;th&gt;Skia Backend&lt;/th&gt;
&lt;th&gt;interop&lt;/th&gt;
&lt;th&gt;shaders&lt;/th&gt;
&lt;th&gt;perf&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;1&lt;/strong&gt; toggle ON&lt;/td&gt;
&lt;td&gt;x11&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;GraphiteDawnVulkan&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;🐢 lag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;2&lt;/strong&gt; toggle OFF + in-app restart&lt;/td&gt;
&lt;td&gt;x11&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;GaneshGL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;on&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚡ fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;3&lt;/strong&gt; toggle OFF + cold start&lt;/td&gt;
&lt;td&gt;wayland&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;GaneshGL&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;fast&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;State 2 is the sweet spot: &lt;strong&gt;GaneshGL (fast) + &lt;code&gt;webgpu_on_vk_via_gl_interop&lt;/code&gt; (shaders work)&lt;/strong&gt;. It appeared by accident because &lt;code&gt;app.relaunch()&lt;/code&gt; re-launches with the &lt;em&gt;previous&lt;/em&gt; process's argv (note the &lt;code&gt;--ozone-platform=x11&lt;/code&gt; that showed up duplicated), carrying the unlocked-GPU flags forward — but the now-toggled-off code path didn't re-add &lt;code&gt;--enable-skia-graphite&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pinning down the real recipe
&lt;/h2&gt;

&lt;p&gt;Back to the probe to bisect what controls &lt;code&gt;webgpu_on_vk_via_gl_interop&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;flags&lt;/th&gt;
&lt;th&gt;backend&lt;/th&gt;
&lt;th&gt;interop&lt;/th&gt;
&lt;th&gt;webgpu adapter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;x11 + ignore-blocklist + unsafe-webgpu&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GaneshGL&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;on&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ deviceOk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;+ enable-skia-graphite&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Graphite&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;off&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;x11 + unsafe-webgpu&lt;/code&gt; (no blocklist bypass)&lt;/td&gt;
&lt;td&gt;GaneshGL&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;x11 + ignore-blocklist&lt;/code&gt; (no unsafe-webgpu)&lt;/td&gt;
&lt;td&gt;GaneshGL&lt;/td&gt;
&lt;td&gt;on&lt;/td&gt;
&lt;td&gt;❌ no adapter&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--ignore-gpu-blocklist&lt;/code&gt; is &lt;strong&gt;required&lt;/strong&gt; to flip interop on.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--enable-unsafe-webgpu&lt;/code&gt; is &lt;strong&gt;required&lt;/strong&gt; to get a real WebGPU adapter (the shaders' device).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--enable-skia-graphite&lt;/code&gt; is &lt;strong&gt;harmful&lt;/strong&gt; — it switches to the slow Graphite backend &lt;em&gt;and&lt;/em&gt; turns interop off.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The recipe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;electron &lt;span class="nt"&gt;--ozone-platform&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;x11 &lt;span class="nt"&gt;--ignore-gpu-blocklist&lt;/span&gt; &lt;span class="nt"&gt;--enable-unsafe-webgpu&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;→ &lt;code&gt;Skia Backend: GaneshGL&lt;/code&gt;, &lt;code&gt;webgpu_on_vk_via_gl_interop: enabled&lt;/code&gt;, a real hardware WebGPU device (AMD RDNA-3, &lt;code&gt;shader-f16&lt;/code&gt;/&lt;code&gt;subgroups&lt;/code&gt;), fast canvas, working shaders, zero warnings.&lt;/p&gt;

&lt;p&gt;It works on native Wayland too, &lt;strong&gt;but&lt;/strong&gt; Wayland + &lt;code&gt;ignore-gpu-blocklist&lt;/code&gt; enables Vulkan compositing, which logs &lt;code&gt;'--ozone-platform=wayland' is not compatible with Vulkan&lt;/code&gt; and risks instability. X11 (under XWayland) is clean — the only cost is losing native Wayland niceties (fractional scaling, per-monitor DPI).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Side note on Chromium's &lt;a href="https://issues.chromium.org/issues/442791440" rel="noopener noreferrer"&gt;issue 442791440&lt;/a&gt;: &lt;code&gt;webgpu_on_vk_via_gl_interop&lt;/code&gt; is listed as a &lt;em&gt;disabled feature&lt;/em&gt; by default for AMD+Mesa, but in practice it flips to "Hardware accelerated" once the blocklist is bypassed and Graphite isn't forced. Useful data point if you're tracking that bug.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The bug that made it all flaky: switches applied too late
&lt;/h2&gt;

&lt;p&gt;Why did the recipe only work after an accidental &lt;code&gt;app.relaunch()&lt;/code&gt;, never on a clean start? Because &lt;code&gt;app.commandLine.appendSwitch(...)&lt;/code&gt; only takes effect if it runs &lt;strong&gt;synchronously, before &lt;code&gt;app.ready&lt;/code&gt;&lt;/strong&gt;. The main process did:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// app.ready fires during this await&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                    &lt;span class="c1"&gt;// ← appendSwitch('ozone-platform','x11'), ('enable-skia-graphite'), ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;                               &lt;span class="c1"&gt;//    too late — Chromium already initialized the GPU process&lt;/span&gt;
&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app even logged "Skia Graphite enabled" — because the &lt;em&gt;code path&lt;/em&gt; ran — while &lt;code&gt;chrome://gpu&lt;/code&gt; reported plain &lt;code&gt;GaneshGL&lt;/code&gt;, because the &lt;em&gt;switches were ignored&lt;/em&gt;. A trivial &lt;code&gt;await Promise.resolve()&lt;/code&gt; before the &lt;code&gt;appendSwitch&lt;/code&gt; calls is enough to lose them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; apply GPU switches at the top level of your main entry, before any &lt;code&gt;await&lt;/code&gt;. (Read your settings synchronously with &lt;code&gt;fs.readFileSync&lt;/code&gt; if you must — that's exactly what you'd already do for &lt;code&gt;--remote-debugging-port&lt;/code&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;For Figma's WebGPU shader fills in Electron on Linux/AMD, the path is &lt;strong&gt;GaneshGL + &lt;code&gt;webgpu_on_vk_via_gl_interop&lt;/code&gt;&lt;/strong&gt;, not Skia Graphite.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--enable-skia-graphite&lt;/code&gt; is a trap on integrated GPUs: it's slower &lt;em&gt;and&lt;/em&gt; disables the interop bridge.&lt;/li&gt;
&lt;li&gt;The interop switch is &lt;code&gt;--ozone-platform=x11 --ignore-gpu-blocklist --enable-unsafe-webgpu&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Apply GPU command-line switches &lt;strong&gt;before &lt;code&gt;app.ready&lt;/code&gt;&lt;/strong&gt;, or they're silently dropped.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getGPUFeatureStatus()&lt;/code&gt; lies if you read it too early — read it after first paint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reproducible harness, all three &lt;code&gt;chrome://gpu&lt;/code&gt; dumps, and the full flag matrix: &lt;strong&gt;&lt;a href="https://gist.github.com/arximus88/f8d93ae1568a5c49e4206250f323b178" rel="noopener noreferrer"&gt;https://gist.github.com/arximus88/f8d93ae1568a5c49e4206250f323b178&lt;/a&gt;&lt;/strong&gt;.&lt;br&gt;
Project: &lt;strong&gt;&lt;a href="https://github.com/arximus88/figma-linux-next" rel="noopener noreferrer"&gt;https://github.com/arximus88/figma-linux-next&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tested on Electron 42.0.1 / 42.3.0 / 43.0.0-beta.6 (Chrome 148–150), AMD Radeon 780M, Mesa 26.1.2 RADV, GNOME/Wayland.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>electron</category>
      <category>webgpu</category>
      <category>figma</category>
    </item>
  </channel>
</rss>
