DEV Community

niixolabs
niixolabs

Posted on

Why Chromium/CEF apps render a black window under Wine (and how to fix it with macOS IOSurface)

The problem

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.

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.

Symptom: the window appears, the contents stay black

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.

Root cause: the process that draws and the process that owns the window are different

  • Modern Chromium/CEF is designed to always run GPU compositing in a separate process
  • The GPU process draws; the browser process owns the window
  • They need a "present path" to transfer the composited frame between them
  • But mainline Wine has no equivalent of this cross-process present, and the D3D→Metal layer (DXMT) returns "cross-process swapchain not supported"
  • Result: the window is created, but no frame ever arrives → black

Why -cef-disable-gpu is a cop-out

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.

The fix: bridge the processes with IOSurface

macOS has IOSurface, which lets processes share GPU memory. The approach:

  1. GPU process: blit the composited frame into a Metal texture backed by an IOSurface
  2. Pass a global IOSurfaceID between the processes (via IPC/file)
  3. Window process: IOSurfaceLookup(id) to get the surface, attach it to the visible layer's contents
  4. Put a ~16ms (60fps) poll timer on the consumer side and call setNeedsDisplay

Settings that mattered

  • Set IOSurfaceIsGlobal = YES in the IOSurfaceCreate dictionary — without it, another process cannot Lookup the surface
  • On the producer side, key it by the root HWND from GetAncestor(hWnd, GA_ROOT) — keying per child window mismatches

The transparency wall

By default you get opaque compositing and overlaps break. I had to flip the opaque flag to non-opaque in three places across DXMT and winemac.

How "unusual" is this, honestly

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."

Full patch and build steps

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):

Top comments (0)