<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Niixo Dev Notes]]></title><description><![CDATA[Niixo Dev Notes]]></description><link>https://niixo.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Fri, 13 Nov 2026 18:34:01 GMT</lastBuildDate><atom:link href="https://niixo.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Why Chromium/CEF apps render a black window under Wine (and how to fix it with macOS IOSurface)]]></title><description><![CDATA[Chromium/CEF apps open a black window under Wine because GPU compositing runs in a separate process. Here's how to bridge it with macOS IOSurface.]]></description><link>https://niixo.hashnode.dev/why-chromium-cef-apps-render-a-black-window-under-wine-and-how-to-fix-it-with-macos-iosurface</link><guid isPermaLink="true">https://niixo.hashnode.dev/why-chromium-cef-apps-render-a-black-window-under-wine-and-how-to-fix-it-with-macos-iosurface</guid><category><![CDATA[wine]]></category><category><![CDATA[macOS]]></category><category><![CDATA[apple silicon]]></category><category><![CDATA[chromium]]></category><category><![CDATA[Metal]]></category><dc:creator><![CDATA[たかおか]]></dc:creator><pubDate>Mon, 22 Jun 2026 06:45:44 GMT</pubDate><content:encoded><![CDATA[<h2>The problem</h2>
<p>I was trying to run a Windows-only Steam game on an Apple Silicon Mac using a self-built Wine + DXMT (D3D11→Metal) stack — no commercial CrossOver. The last wall: the window opens, but its contents stay completely black.</p>
<p>The same symptom shows up with the Steam client itself and with many apps that embed CEF (Chromium Embedded Framework). This post explains why it happens and the idea behind fixing it with macOS IOSurface — focused on the "why" and "what to bridge," not full code.</p>
<h2>Symptom: the window appears, the contents stay black</h2>
<p>The window frame is created, but the inside is black. No crash. No fatal error in the logs. "It runs but doesn't render" — that's what made this hard to track down.</p>
<h2>Root cause: the process that draws and the process that owns the window are different</h2>
<ul>
<li><p>Modern Chromium/CEF is designed to always run GPU compositing in a separate process</p>
</li>
<li><p>The GPU process draws; the browser process owns the window</p>
</li>
<li><p>They need a "present path" to transfer the composited frame between them</p>
</li>
<li><p>But mainline Wine has no equivalent of this cross-process present, and the D3D→Metal layer (DXMT) returns "cross-process swapchain not supported"</p>
</li>
<li><p>Result: the window is created, but no frame ever arrives → black</p>
</li>
</ul>
<h2>Why <code>-cef-disable-gpu</code> is a cop-out</h2>
<p>Disabling GPU compositing falls back to software rendering, which does show something. But performance tanks and it's unusable for a game. I decided to build the frame-transfer path properly instead.</p>
<h2>The fix: bridge the processes with IOSurface</h2>
<p>macOS has IOSurface, which lets processes share GPU memory. The approach:</p>
<ol>
<li><p>GPU process: blit the composited frame into a Metal texture backed by an IOSurface</p>
</li>
<li><p>Pass a global IOSurfaceID between the processes (via IPC/file)</p>
</li>
<li><p>Window process: <code>IOSurfaceLookup(id)</code> to get the surface, attach it to the visible layer's contents</p>
</li>
<li><p>Put a ~16ms (60fps) poll timer on the consumer side and call <code>setNeedsDisplay</code></p>
</li>
</ol>
<h3>Settings that mattered</h3>
<ul>
<li><p>Set <code>IOSurfaceIsGlobal = YES</code> in the <code>IOSurfaceCreate</code> dictionary — without it, another process cannot Lookup the surface</p>
</li>
<li><p>On the producer side, key it by the root HWND from <code>GetAncestor(hWnd, GA_ROOT)</code> — keying per child window mismatches</p>
</li>
</ul>
<h3>The transparency wall</h3>
<p>By default you get opaque compositing and overlaps break. I had to flip the <code>opaque</code> flag to non-opaque in three places across DXMT and winemac.</p>
<h2>How "unusual" is this, honestly</h2>
<p>This isn't a finished product and has known limitations. CrossOver is genuinely well-engineered — this was an exercise in "how far can you get without paying for it."</p>
<h2>Full patch and build steps</h2>
<p>This post sticks to the concept. The full patch, self-built Wine steps, launch scripts and staged verification are written up in detail (in Japanese, but the commands/configs translate cleanly):</p>
<ul>
<li><p>Concept article (free): <a href="https://zenn.dev/niixolabs/articles/wine-cef-cross-process-present">https://zenn.dev/niixolabs/articles/wine-cef-cross-process-present</a></p>
</li>
<li><p>The full story (free): <a href="https://zenn.dev/niixolabs/articles/m1-mac-steam-wine-story">https://zenn.dev/niixolabs/articles/m1-mac-steam-wine-story</a></p>
</li>
<li><p>Complete step-by-step (paid book, ¥800; 3 of 7 chapters free): <a href="https://zenn.dev/niixolabs/books/m1-mac-steam-wine-build">https://zenn.dev/niixolabs/books/m1-mac-steam-wine-build</a></p>
</li>
</ul>
]]></content:encoded></item></channel></rss>