☰
如何把 x86 游戏画面显示到 iPhone 上:Madeira 的 IOSDisplayShim + CAMetalLayer 显示桥接详解
2026/10/2 6:53:20 网站建设 项目流程

如何把 x86 游戏画面显示到 iPhone 上:Madeira 的 IOSDisplayShim + CAMetalLayer 显示桥接详解

【免费下载链接】MadeiraRun x86-64 Windows PC games on jailed iOS via FEX-Emu + Wine + DXMT项目地址: https://gitcode.com/GitHub_Trending/mad/Madeira

Madeira 是一个让未越狱 iPhone 直接运行 x86-64 Windows PC 游戏的开源项目:它把 FEX-Emu(x86→ARM64 翻译)、Wine(Windows 兼容层)和 DXMT(D3D11→Metal 翻译)打包成单个 iOS 进程运行。而这一切能否真正"看见",关键就在显示链路——本文带你读懂 IOSDisplayShim.m 如何用UIKit + CAMetalLayer实现 Wine 显示驱动,把 Wine 客端的每一帧画到 iPhone 屏幕上。

一、背景:为什么 iOS 上的 Wine 需要一个"显示垫片层"

DXMT 负责把 Windows 的 D3D11 调用翻译成 Apple 的 Metal 指令,但它最初是为macOS设计的:它的 unix 端代码会假设一个类似 Cocoa 的显示驱动存在,通过下面这条链拿到渲染目标:

dlsym(RTLD_DEFAULT, "macdrv_functions") → get_win_data(hwnd) → client_cocoa_view → macdrv_view_create_metal_view → macdrv_view_get_metal_layer (拿到 CAMetalLayer,开画)

问题在于:iOS 上根本不存在 Cocoa,也没有"窗口列表"这个概念——整个设备只有一块屏幕。

IOSDisplayShim 的思路非常巧妙:既然 DXMT 只认符号名,那我就在主程序里伪造出一套它期望的 macdrv 函数表,并把"所有 HWND 都指向同一块 Swift 端创建的 CAMetalLayer"。这正是项目 README 所说的核心机制,详见 README.md 与 IOSDisplayShim.h 头文件注释。

二、核心机制:用 200 行代码"骗过" DXMT

整个垫片层只有约 260 行(IOSDisplayShim.m),核心分四步。

1. 进程级单例:一块层服务全局

iOS 只有一个"窗口",所以全局只保留一块层指针,用 pthread 互斥锁保护:

static CAMetalLayer *g_layer = nil; // 全局唯一渲染层 void madeira_display_set_layer(CAMetalLayer *layer) { pthread_mutex_lock(&g_lock); g_layer = layer; pthread_mutex_unlock(&g_lock); }

见 IOSDisplayShim.m。Swift 前端在游戏启动、D3D11 交换链创建之前,调用这个函数把自建的层注册进来。

2. 伪造"窗口数据":把 HWND 装进结构体

DXMT 每次渲染都要先问"这个窗口(HWND)对应的视图是什么"。垫片层用一个静态假结构体,把 HWND 塞进client_cocoa_view字段"带"着走:

  • my_get_win_data():记住传入的 HWND,返回伪造结构体指针
  • my_view_create_metal_view():这是最关键的两个函数之一,根据运行模式返回真正的层:
运行模式行为
游戏模式(默认)返回全局g_layer,即全屏单例层
桌面模式(MADEIRA_DESKTOP=1)调用 Winios.m 的桌面合成器,按窗口取每个窗口各自独立的 CAMetalLayer

返回前用CFBridgingRetain给层加一次引用计数,macdrv_view_release_metal_view时再释放——Objective-C 的 ARC 记账就这样被正确接进了 DXMT 的"创建/释放"生命周期里。

3. "取出层"只是原样返还

macdrv_view_get_metal_layer直接返回上一步打包的指针:

因为my_view_create_metal_view返回的就是层本身,这一步只是把"视图句柄"和"层句柄"划等号。

见 IOSDisplayShim.m。

4. 符号导出:一行属性决定生死 ⚠️

这是全文最"硬核"的部分。DXMT 靠dlsym按名字找符号,链接器根本看不见这种引用。Release 模式下-dead_strip会把这些"无人引用"的函数整个删掉,DXMT 就查不到驱动、直接报"your Wine has no exported symbols needed by DXMT"白屏退出。

项目的解法是在每个导出符号上加双保险属性,见 IOSDisplayShim.m:

__attribute__((used, visibility("default"))) struct macdrv_functions_t macdrv_functions = { ... };

used让链接器保留符号,visibility("default")让它进入导出表。源码注释里还记了一课:某次误用 Release 构建后 6 个符号消失、Thumper 白屏——所以每次改构建配置后,要用xcrun dyld_info -exports验证macdrv_functions还在导出表里。

三、Swift 端:直接挂在 UIWindow 上的 CAMetalLayer

层的创建和管理在 ContentView.swift 的MetalHostView中,这里有两个值得一提的工程决策:

① 不用 SwiftUI 托管,直接挂在 UIWindow 上。在 iOS 26/27 上,SwiftUI 托管视图的 backing layer 会被偶发路由到快照路径,导致 Metal 提交被静默丢弃(画面卡在旧帧)。所以项目用一个进程级单例的普通UIView(layerClass = CAMetalLayer.self)直接加到窗口上,SwiftUI 里的MetalBackedView只留作透明的"几何占位 + 触摸输入"层,触摸穿透到它处理。

② 几个关键的层属性配置(ContentView.swift):

  • pixelFormat = .bgra8Unorm、framebufferOnly = true:只用于屏幕呈现的标准配置
  • displaySyncEnabled = NO:通过私有 API 把低帧率游戏的 present 从显示同步调度中摘出来——否则游戏只有 1 FPS 时,大部分帧的presentedTime为 0,画面看起来像"卡死"
  • nominalFramesPerSecond跟随屏幕最高刷新率(ProMotion 机型可到 120Hz),帧率控制交给 FPSOverlay.swift 的 DisplayLink 完成

注册时机在didMoveToWindow()中只发生一次:

if !Self.layerRegistered { Self.layerRegistered = true madeira_display_set_layer(host.metalLayer) // 只注册这一次 }

见 ContentView.swift。

四、虚拟显示器:布局与触摸共用一套数学

Wine 客端渲染的其实是"虚拟显示器":会话启动时由环境变量MADEIRA_SCREEN_W/H播种(未设置则 1024×768),游戏调用ChangeDisplaySettings改分辨率时,win32u 会回调winios_display_mode_changed(),垫片层在主队列发出一条MadeiraDisplayModeChanged通知,Swift 端收到后重新布局。完整流程见 IOSDisplayShim.m 与 GuestDisplay.swift。

布局与触摸映射共用同一个GameSurfaceLayout结构(GuestDisplay.swift),支持 4 种模式:

模式效果
Fit完整显示、居中留黑边
Fill铺满屏幕、裁切溢出
Stretch强制拉伸到整个屏幕
Aspect按游戏实际回缓冲比例等比缩放

显示层的 frame 和触摸坐标换算用的是同一套 rect 计算——这是刻意为之:如果两边各自算比例,Fill/Stretch 模式下一像素的舍入差就会让点击位置偏移。触摸点还会被钳制到客端画面边界内,按在黑色边距上也不会产生越界坐标。

五、顺带一提:帧率钩子的弱符号兼容

垫片层还定义了一个__attribute__((weak))的madeira_set_display_max_fps(IOSDisplayShim.m)。带 30FPS 上限分支的 DXMT 会用强符号覆盖它;不带该功能的 DXMT 则链接到这个空实现,前端再据此隐藏对应设置项——同一份前端代码可以链接两种 DXMT,无需改动。

六、关键文件导航 📍

想动手看源码,按这个顺序效率最高:

  • 驱动接口定义:IOSDisplayShim.h
  • 垫片层实现(macdrv 函数表 + 虚拟显示器):IOSDisplayShim.m
  • CAMetalLayer 宿主与注册时机:ContentView.swift
  • 布局/触摸共用的几何计算:GuestDisplay.swift
  • 桌面模式的多窗口合成器:Winios/Winios.m
  • 整体架构背景:ARCHITECTURE_ANALYSIS.md

七、小结

IOSDisplayShim 展示了移植工程中一个优雅的通用套路:不要改造上游,而是满足它的探测方式。DXMT 按符号名探测显示驱动,垫片层就精确地伪造出它要的那 9 个函数,把"无数 Windows 窗口"收敛成"一块 iPhone 屏幕";再配合 Swift 端对 CAMetalLayer 的精细调教(窗口级托管、禁用显示同步、引用计数交接),x86 游戏的每一帧最终都稳稳落在 iOS 的 Metal 渲染管线上。对于任何想在 iOS 上桥接非原生图形栈的项目,这套"符号级适配 + 单例渲染目标"的做法都值得借鉴。

【免费下载链接】MadeiraRun x86-64 Windows PC games on jailed iOS via FEX-Emu + Wine + DXMT项目地址: https://gitcode.com/GitHub_Trending/mad/Madeira

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询