Skip to content

Commit 18ad9d9

Browse files
committed
examples/10-graphics: 可移植性驱动默认是看不见的
四期(软件设备把 CI 上限抬到运行)的程序侧那一半。 接设备之前先测了上游产物,而第一行的读数就是本条的理由:Khronos 官方的 `MoltenVK-macos.tar` v1.4.2 里,ICD 带着 **`"is_portability_driver": true`**。 **loader 默认不把可移植性驱动交给 `vkEnumeratePhysicalDevices`** —— 一个照着原生 驱动写的 Vulkan 程序因此在 macOS 上一个设备都找不到,并把它报成「这台机器没有 GPU」。诊断是错的,而它看起来完全合理。 规范要的是成对的两半:实例要**启用** `VK_KHR_portability_enumeration` **并且**置位 `VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR`;随后凡是声明了 `VK_KHR_portability_subset` 的设备,必须在 `vkCreateDevice` 时启用它,否则调用失败。 按**能力**问,不按 `#ifdef __APPLE__`:性质是「我面前这个 loader 在展示可移植性 驱动」,而 `#ifdef` 两个方向都会错 —— Linux 上跑翻译层的机器也有它,macOS 上对着 原生驱动构建的程序并不需要它。 实测:Linux/lavapipe 上读数不变,中心像素仍是 `(124, 70, 62, 255)`。 顺带记下 Windows 那一侧的测量:`mesa3d-26.2.0-release-msvc` 的 `vulkan_lvp.dll` **只导入系统 DLL**(无 MSVCP140/VCRUNTIME140),静态 CRT,机器上不需要装任何东西。 载荷侧(把 MoltenVK 与 Windows 的 lavapipe 收进索引)未做:那要往发布组织推新的二进制 资产,而 GitCode 的资产不可替换不可删除。
1 parent 49e2d6b commit 18ad9d9

3 files changed

Lines changed: 106 additions & 1 deletion

File tree

.agents/docs/2026-09-07-heterogeneous-cross-platform-ecosystem.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,11 @@ install/config 形状,而那套形状是按 Linux 写的:
332332
- **二期(shaderc 三平台)**:xim-pkgindex #778,规则按平台选编译器。已实现。
333333
- **三期(CUDA/SYCL 上 Windows)**:xim-pkgindex #779(五个包),规则侧的路径推导与
334334
路线判断。已实现;端到端由 `windows-test` job 装卸五个包并断言注册的程序验证。
335-
- **四期(软件设备把 CI 上限抬到运行)**:**未实现,记录为发布工作**。Windows 上需要
335+
- **四期(软件设备把 CI 上限抬到运行)**:**部分实现**。程序侧那一半做完了:一个
336+
Vulkan 程序在 macOS 上**看不见任何设备**,除非它显式打开可移植性枚举 —— MoltenVK 是
337+
portability driver,而 loader 默认不把这种驱动交给 `vkEnumeratePhysicalDevices`
338+
示例现在按**能力**而不是按 `#ifdef __APPLE__` 问这件事(见 10.4e)。载荷侧仍未做,
339+
记录为发布工作。Windows 上需要
336340
一个 Mesa-on-Windows 的 `vulkan_lvp` 构建;macOS 上 runner 自带 GPU 与 MoltenVK,
337341
所以那一侧不需要软件设备,需要的是把 `compat.vulkan` 的 macOS 腿接到示例上。
338342
今天的上限:三平台**构建**,Linux **运行**
@@ -396,6 +400,31 @@ include 它。**
396400
同一版还给「每条规则都为本宿主编译过」那个夹具补了它自己的分母:它断言的是「每一条」,
397401
而「每一条」是它 carry 的一张清单。
398402

403+
### 10.4e 四期的程序侧:可移植性驱动默认是看不见的
404+
405+
把软件/可移植设备接到 CI 上之前,先测了上游产物:
406+
407+
| 产物 | 读数 |
408+
|---|---|
409+
| `MoltenVK-macos.tar` v1.4.2(Khronos 官方) | 92 个条目;`dynamic/dylib/macOS/libMoltenVK.dylib` 是 x86_64+arm64 的 universal binary(11 MB),ICD 用相对路径 `./libMoltenVK.dylib`,并带 **`"is_portability_driver": true`** |
410+
| `mesa3d-26.2.0-release-msvc.7z`(pal1000 重分发) | `x64/vulkan_lvp.dll` 56 MB + `x64/lvp_icd.x86_64.json`;**只导入系统 DLL**(KERNEL32/GDI32/USER32/ADVAPI32/ole32/SHELL32/ntdll),没有 MSVCP140 —— 静态 CRT,机器上不需要装任何东西 |
411+
412+
第一行那个 `is_portability_driver` 是本节存在的理由:**loader 默认不把可移植性驱动交给
413+
`vkEnumeratePhysicalDevices`**。一个照着原生驱动写的程序因此在 macOS 上一个设备都找不到,
414+
并把它报成「这台机器没有 GPU」—— 诊断是错的,而它看起来完全合理。
415+
416+
规范要求的是成对的两半:实例要**启用** `VK_KHR_portability_enumeration` **并且**置位
417+
`VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR`;随后凡是声明了
418+
`VK_KHR_portability_subset` 的设备,必须在 `vkCreateDevice` 时启用它,否则调用失败。
419+
420+
示例按**能力**问这两件事,不按 `#ifdef __APPLE__`。性质是「我面前这个 loader 在展示
421+
可移植性驱动」,而 `#ifdef` 两个方向都会错:Linux 上跑翻译层的机器也有它,而 macOS 上
422+
对着原生驱动构建的程序并不需要它。实测:Linux/lavapipe 上读数不变,中心像素仍是
423+
`(124, 70, 62, 255)`
424+
425+
载荷侧(把 MoltenVK 与 Windows 的 lavapipe 收进索引)未做,因为那要往发布组织推新的
426+
二进制资产,而 GitCode 的资产不可替换不可删除 —— 这是一个该由人做的决定,不该顺手做掉。
427+
399428
### 10.5 一条留下的不一致,以及它什么时候消失
400429

401430
`mcpp:plugins` 0.2.5/0.2.6 里 `xim:shaderc` 在 macOS 与 Windows 上是**精确版本**,而

examples/10-graphics/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,23 @@ dropped while the source that includes it, selected by the same predicate, was
8080
kept. Packages are therefore unconditional or conditioned on the platform;
8181
`[build]` sources are what the accelerator selects.
8282

83+
**A portability driver is hidden until the program asks for it.** macOS has no
84+
native Vulkan: MoltenVK implements it on top of Metal, and the specification
85+
calls such an implementation a *portability driver*. The loader does not show
86+
one to `vkEnumeratePhysicalDevices` unless the instance enables
87+
`VK_KHR_portability_enumeration` and sets
88+
`VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR`; a device that advertises
89+
`VK_KHR_portability_subset` must then have that extension enabled at
90+
`vkCreateDevice` or the call fails. A program written against a native driver
91+
alone therefore finds no device on such a machine and reports it as "no GPU
92+
here", which is the wrong diagnosis.
93+
94+
`src/vulkan/render.cpp` asks the loader and the device what they advertise
95+
rather than testing for the platform. The property is "the loader in front of
96+
me is showing portability drivers", and an `#ifdef __APPLE__` would be wrong in
97+
both directions: a Linux machine running a translation layer has it, and a
98+
macOS build against a native driver does not need it.
99+
83100
**One shader per stem.** The generated name is the shader's stem and its stage,
84101
so `ui/text.vert` and `world/text.vert` would both produce `text_vert.h`
85102
declaring `text_vert_spv`. The rule refuses that and names both files. The

examples/10-graphics/offscreen/src/vulkan/render.cpp

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,43 @@ const char* g_ran_on = "";
3333

3434
constexpr unsigned char kClear[4] = { 16, 16, 16, 255 };
3535

36+
// WHETHER AN EXTENSION IS THERE, ASKED RATHER THAN ASSUMED.
37+
//
38+
// The two below decide whether this program can see a PORTABILITY driver -- an
39+
// implementation of Vulkan on top of something else, which is the only kind
40+
// macOS has, because Apple's platform interface is Metal and MoltenVK
41+
// translates. A portability driver is HIDDEN from `vkEnumeratePhysicalDevices`
42+
// unless the instance opts in, so a program written against a native driver
43+
// alone finds no device there and reports it as "this machine has no GPU".
44+
//
45+
// Written as a capability query rather than `#ifdef __APPLE__`: the property is
46+
// "the loader in front of me advertises this", and a Linux machine running a
47+
// portability layer would have it too. The `#ifdef` would also be wrong in the
48+
// other direction -- a macOS build against a native driver does not need it.
49+
bool instance_extension_present(const char* name) {
50+
std::uint32_t n = 0;
51+
if (vkEnumerateInstanceExtensionProperties(nullptr, &n, nullptr) != VK_SUCCESS || n == 0)
52+
return false;
53+
std::vector<VkExtensionProperties> props(n);
54+
if (vkEnumerateInstanceExtensionProperties(nullptr, &n, props.data()) != VK_SUCCESS)
55+
return false;
56+
for (auto const& p : props)
57+
if (std::strcmp(p.extensionName, name) == 0) return true;
58+
return false;
59+
}
60+
61+
bool device_extension_present(VkPhysicalDevice phys, const char* name) {
62+
std::uint32_t n = 0;
63+
if (vkEnumerateDeviceExtensionProperties(phys, nullptr, &n, nullptr) != VK_SUCCESS || n == 0)
64+
return false;
65+
std::vector<VkExtensionProperties> props(n);
66+
if (vkEnumerateDeviceExtensionProperties(phys, nullptr, &n, props.data()) != VK_SUCCESS)
67+
return false;
68+
for (auto const& p : props)
69+
if (std::strcmp(p.extensionName, name) == 0) return true;
70+
return false;
71+
}
72+
3673
int find_memory_type(VkPhysicalDevice phys, std::uint32_t bits,
3774
VkMemoryPropertyFlags want) {
3875
VkPhysicalDeviceMemoryProperties props{};
@@ -129,6 +166,18 @@ extern "C" int render_offscreen(unsigned w, unsigned h, unsigned char* rgba) {
129166
app.apiVersion = VK_API_VERSION_1_2;
130167
VkInstanceCreateInfo ici{ VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO };
131168
ici.pApplicationInfo = &app;
169+
170+
// The opt-in, when the loader has it. Both halves are required and the
171+
// pair is what the specification asks for: the extension has to be
172+
// ENABLED, and the flag has to be set, before a portability driver appears
173+
// in `vkEnumeratePhysicalDevices`.
174+
std::vector<const char*> instanceExts;
175+
if (instance_extension_present(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME)) {
176+
instanceExts.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME);
177+
ici.flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR;
178+
}
179+
ici.enabledExtensionCount = static_cast<std::uint32_t>(instanceExts.size());
180+
ici.ppEnabledExtensionNames = instanceExts.empty() ? nullptr : instanceExts.data();
132181
if (vkCreateInstance(&ici, nullptr, &f.instance) != VK_SUCCESS) return 1;
133182

134183
VkPhysicalDevice phys = VK_NULL_HANDLE;
@@ -143,6 +192,16 @@ extern "C" int render_offscreen(unsigned w, unsigned h, unsigned char* rgba) {
143192
VkDeviceCreateInfo dci{ VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO };
144193
dci.queueCreateInfoCount = 1;
145194
dci.pQueueCreateInfos = &qci;
195+
196+
// AND THE OTHER HALF: a device that advertises `VK_KHR_portability_subset`
197+
// must have it enabled, or `vkCreateDevice` fails. The specification makes
198+
// this one mandatory rather than optional precisely so that a program
199+
// cannot use such a device while believing it is a complete one.
200+
std::vector<const char*> deviceExts;
201+
if (device_extension_present(phys, "VK_KHR_portability_subset"))
202+
deviceExts.push_back("VK_KHR_portability_subset");
203+
dci.enabledExtensionCount = static_cast<std::uint32_t>(deviceExts.size());
204+
dci.ppEnabledExtensionNames = deviceExts.empty() ? nullptr : deviceExts.data();
146205
if (vkCreateDevice(phys, &dci, nullptr, &f.device) != VK_SUCCESS) return 1;
147206

148207
VkQueue queue = VK_NULL_HANDLE;

0 commit comments

Comments
 (0)