From 8cf71efd62dda6925fea5c585e8107ea1afb591f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Sat, 19 Sep 2026 11:52:04 +0200 Subject: [PATCH] Add Dawn Vulkan instance query v2 --- .../linux-exact-native-device-hook-646.md | 9 ++ .../linux-exact-vulkan-instance-query-649.md | 66 +++++++++ .../linux-external-image-interop-28.md | 12 ++ eng/graphics/DawnSymbolBoundary.cmake | 2 +- eng/graphics/README.md | 19 ++- eng/graphics/dawn_exports.py | 3 +- eng/graphics/dawn_native_device.cpp | 132 +++++++++++++++--- eng/graphics/tests/test_dawn_exports.py | 5 +- .../tests/test_dawn_native_device_bridge.py | 44 ++++++ .../CMakeLists.txt | 3 +- .../graphics/dawn_linux_external_provider.h | 39 ++++-- .../graphics/webscene/dawn_native_device.h | 66 ++++++++- ...linux_external_provider_contract_tests.cpp | 30 ++++ src/WebScene.Sdk/cmake/LinuxSDK.cmake | 3 +- .../cmake/WebSceneLinuxConfig.cmake | 2 +- src/WebScene.Sdk/cmake/WebSceneLinuxSDK.cmake | 3 +- 16 files changed, 398 insertions(+), 40 deletions(-) create mode 100644 docs/validation/linux-exact-vulkan-instance-query-649.md create mode 100644 eng/graphics/tests/test_dawn_native_device_bridge.py diff --git a/docs/validation/linux-exact-native-device-hook-646.md b/docs/validation/linux-exact-native-device-hook-646.md index 5da9387f..c6c51673 100644 --- a/docs/validation/linux-exact-native-device-hook-646.md +++ b/docs/validation/linux-exact-native-device-hook-646.md @@ -14,6 +14,15 @@ non-Vulkan or incomplete identities. SDK verification covers the installed header, bridge sources, export allowlist and cache inputs. AppScene includes no Dawn private header. +The first graphics SDK build exposed one deterministic compatibility defect: +Dawn compiles native objects with `-fno-exceptions`, while the initial registry +used an exception handler around `unordered_map` insertion. Issue #649 replaces +that insertion with a mutex-protected intrusive list whose nodes use +`new(std::nothrow)`. Allocation failure leaves the device unregistered, so the +query continues to reject it as foreign before dereference. The registry itself +uses destructor-free static storage to preserve the original teardown rule. +No exception syntax remains in the bridge. + `bind_dawn_linux_external_device` is the factory consumption point. It copies the `wgpu::Device` into the lifetime anchor and requires the same shared atomic loss flag used by the host's Dawn device-lost callback. Allocation, begin, diff --git a/docs/validation/linux-exact-vulkan-instance-query-649.md b/docs/validation/linux-exact-vulkan-instance-query-649.md new file mode 100644 index 00000000..650fc870 --- /dev/null +++ b/docs/validation/linux-exact-vulkan-instance-query-649.md @@ -0,0 +1,66 @@ +# Exact Dawn Vulkan instance query + +Issue: #649. Parent: WebScene #28. This is the WebScene prerequisite for +AppScene #184, #262 and #130. + +The pinned Linux Dawn monolith now exports +`websceneDawnQueryVulkanDeviceV2` beside the unchanged v1 entry point. For a +live Vulkan `WGPUDevice`, v2 returns the exact borrowed `VkInstance`, +`VkPhysicalDevice`, `VkDevice`, `VkQueue`, graphics queue family and +device/driver UUIDs. It also returns the exact `vkGetInstanceProcAddr` function +which Dawn loaded for that instance. The public C header represents Vulkan +handles and procedures with opaque ABI-compatible values; all private Dawn and +Vulkan implementation headers remain inside the pinned bridge source. + +## Validation and ownership + +The exported function validates arguments and the v2 size/version before it +looks up the token. Lookup scans the authoritative live registry while holding +its mutex and acquires a Dawn reference before releasing the mutex. Only then +does the query access private device state. Foreign and stale tokens therefore +fail before query-side private dereference. The bridge also rejects lost and +non-Vulkan devices, null native identities, zero UUIDs and an absent resolver. +It fills a local result and copies it to the caller only on success, leaving the +caller's bytes unchanged on every failure. + +V2 reports three required capabilities: enabled `VK_KHR_surface`, enabled +`VK_KHR_xlib_surface`, and the complete set of loaded generic surface and Xlib +creation/presentation-query procedures. A missing extension, build flag or +procedure returns `XLIB_PRESENTATION_UNAVAILABLE`; the provider publishes no +lifetime. The host supplies the X11 `Display` and visual ID and must call +`vkGetPhysicalDeviceXlibPresentationSupportKHR` through the returned resolver +before it creates an Xlib surface. + +`bind_dawn_linux_external_device` consumes v2 and copies the public +`wgpu::Device` into the existing native owner. That reference retains the Dawn +backend and every borrowed Vulkan identity. Allocator identity checks now +include the instance, resolver and presentation capability mask. No Vulkan +object returned by this API may be destroyed by the consumer, and no second +instance/device or CPU transfer path is permitted. + +## SDK and build contracts + +The Linux version script and post-build symbol inspection require both v1 and +v2 while rejecting every other non-WebGPU export. The installed and provenance +copies of `webscene/dawn_native_device.h`, bridge source and internal hook remain +content-hashed by the graphics manifest and `verify-sdk.py`. The graphics SDK +cache identity already hashes those files plus the symbol policy and export +inspector, so this ABI change cannot reuse a v1-only binary. Linux SDK CMake +targets publish the legacy v1 macro and the explicit v2 macro. + +The bridge registry was also made compatible with Dawn's `-fno-exceptions` +build. Registration uses a non-throwing intrusive-list allocation and fails +closed if allocation fails. Unregistration removes and frees the exact node; +lookup still retains the live Dawn device under the registry mutex. + +Per the active fast policy, authored builds and tests were not run. Validation +for this checkpoint is limited to `git diff --check`. + +## Remaining acceptance + +AppScene can now create its Xlib `VkSurfaceKHR` and Graphite backend against the +same Dawn instance/device/queue. Physical Linux acceptance must still prove the +display/visual presentation query, surface creation and swapchain format/mode, +rendered pixels, resize and device-loss teardown, queue-completion retirement, +the 30-minute FD/residency plateau, and zero CPU readback/upload in ordinary +frames. diff --git a/docs/validation/linux-external-image-interop-28.md b/docs/validation/linux-external-image-interop-28.md index c7c8411d..4e073ce1 100644 --- a/docs/validation/linux-external-image-interop-28.md +++ b/docs/validation/linux-external-image-interop-28.md @@ -78,6 +78,18 @@ public adapter/device tokens, borrowed native tuple, queue family and Vulkan device/driver UUIDs. The SDK ships only `webscene/dawn_native_device.h`; no Dawn private header crosses into AppScene. +Issue #649 adds the v2 query without changing the v1 ABI. V2 also returns the +same Dawn device's borrowed `VkInstance` and the exact +`vkGetInstanceProcAddr` loaded by that Vulkan backend. It succeeds only when +`VK_KHR_surface` and `VK_KHR_xlib_surface` were enabled and Dawn loaded the +surface creation, destruction, capability and Xlib presentation-query +procedures. `bind_dawn_linux_external_device` now consumes v2 and retains its +`wgpu::Device`, keeping the complete borrowed instance/device/queue tuple alive. +The X11 host must still call +`vkGetPhysicalDeviceXlibPresentationSupportKHR` with its own display and visual; +unsupported displays fail before surface creation and cannot select a second +Vulkan instance or a CPU presentation route. + The SDK now exposes `dawn_linux_external_device_factory`. An AppScene host with an exact-device Dawn integration creates the WebGPU instance/adapter/device and native allocator atomically, and returns one diff --git a/eng/graphics/DawnSymbolBoundary.cmake b/eng/graphics/DawnSymbolBoundary.cmake index c44a14e7..5ae67328 100644 --- a/eng/graphics/DawnSymbolBoundary.cmake +++ b/eng/graphics/DawnSymbolBoundary.cmake @@ -27,7 +27,7 @@ function(webscene_isolate_dawn) target_link_options(webgpu_dawn PRIVATE "LINKER:-exported_symbols_list,${CMAKE_BINARY_DIR}/webscene-dawn.exports") elseif(UNIX) file(WRITE "${CMAKE_BINARY_DIR}/webscene-dawn.exports" - "{ global: wgpu*; websceneDawnQueryVulkanDeviceV1; local: *; };\n") + "{ global: wgpu*; websceneDawnQueryVulkanDeviceV1; websceneDawnQueryVulkanDeviceV2; local: *; };\n") target_link_options(webgpu_dawn PRIVATE "LINKER:--version-script=${CMAKE_BINARY_DIR}/webscene-dawn.exports") endif() # Windows exports only functions decorated by WGPU_SHARED_LIBRARY; diff --git a/eng/graphics/README.md b/eng/graphics/README.md index cb3657dc..f8e5ec10 100644 --- a/eng/graphics/README.md +++ b/eng/graphics/README.md @@ -6,7 +6,24 @@ Implements part of [#23](https://github.com/wieslawsoltes/WebScene/issues/23), t `dependencies.lock.json` pins Dawn (including Tint and generated WebGPU headers), ANGLE, depot_tools, WebGPU CTS, Khronos WebGL and WPT to exact commits. Dawn's own pinned `DEPS` supplies its transitive source revisions; ANGLE uses its pinned `DEPS` through gclient with depot_tools self-update disabled. The installed package records the transitive Git revision graph, exact CMake/GN settings, tool identities, source licenses/notices and hashes of every installed header/library. There is no ProGPU or renderer replacement dependency. -Dawn uses a shared `webgpu_dawn` monolith exporting WebGPU C entry points. On Linux it also exports the single versioned `websceneDawnQueryVulkanDeviceV1` C entry point, which returns borrowed native identities only for a live device created by this exact Dawn build. Static co-linkage with V8 exposed conflicting unversioned Abseil symbols and caused native runtime hangs; the shared boundary keeps those implementations private. `DawnSymbolBoundary.cmake` applies the export policy, and the builder checks actual binary exports before sealing the SDK. The Linux builder temporarily connects Dawn's authoritative device add/remove path to the query registry and restores the pinned source before provenance sealing. No private Dawn header enters the installed SDK. ANGLE uses shared EGL/GLESv2 with static internal dependencies and C ABI entry points. Windows release builds use static CRT for Dawn, matching the existing native engine/V8; ANGLE's non-component Chromium configuration controls its runtime. Windows build/link and runtime dependency validation (including D3D shader compiler packaging) remain hardware-runner gates. Never exchange CRT-owned allocations between these libraries. +Dawn uses a shared `webgpu_dawn` monolith exporting WebGPU C entry points. On +Linux it also exports the additive `websceneDawnQueryVulkanDeviceV1` and +`websceneDawnQueryVulkanDeviceV2` C entry points. V1 returns the borrowed native +device tuple; v2 also returns Dawn's exact borrowed `VkInstance`, its exact +`vkGetInstanceProcAddr` resolver, and required Xlib presentation state. Both +accept only a live device created by this exact Dawn build. Static co-linkage +with V8 exposed conflicting unversioned Abseil symbols and caused native +runtime hangs; the shared boundary keeps those implementations private. +`DawnSymbolBoundary.cmake` applies the export policy, and the builder checks +actual binary exports before sealing the SDK. The Linux builder temporarily +connects Dawn's authoritative device add/remove path to the query registry and +restores the pinned source before provenance sealing. No private Dawn header +enters the installed SDK. ANGLE uses shared EGL/GLESv2 with static internal +dependencies and C ABI entry points. Windows release builds use static CRT for +Dawn, matching the existing native engine/V8; ANGLE's non-component Chromium +configuration controls its runtime. Windows build/link and runtime dependency +validation (including D3D shader compiler packaging) remain hardware-runner +gates. Never exchange CRT-owned allocations between these libraries. `GraphicsDependencies.cmake` verifies the SDK before importing it, including revision, RID, lock, settings, complete installed file inventory and content hashes. CMake cannot silently choose another `Dawn_DIR`. A dependency roll requires rebuilding headers and libraries together. Package manifests prove integrity and provenance, not API conformance or hardware execution. diff --git a/eng/graphics/dawn_exports.py b/eng/graphics/dawn_exports.py index 62248c9e..b403d9bc 100644 --- a/eng/graphics/dawn_exports.py +++ b/eng/graphics/dawn_exports.py @@ -19,7 +19,8 @@ def inspect_exports(library, rid): if match: symbol = match.group(1) symbols.append(symbol[1:] if rid.startswith("osx-") and symbol.startswith("_") else symbol) - allowed_bridge = ({"websceneDawnQueryVulkanDeviceV1"} + allowed_bridge = ({"websceneDawnQueryVulkanDeviceV1", + "websceneDawnQueryVulkanDeviceV2"} if rid.startswith("linux-") else set()) unexpected = [name for name in symbols if not re.fullmatch(r"wgpu[A-Z][A-Za-z0-9]*", name) diff --git a/eng/graphics/dawn_native_device.cpp b/eng/graphics/dawn_native_device.cpp index a18ac4c1..c357c7fc 100644 --- a/eng/graphics/dawn_native_device.cpp +++ b/eng/graphics/dawn_native_device.cpp @@ -3,7 +3,7 @@ #include #include -#include +#include #include "src/dawn/common/Ref.h" #include "src/dawn/native/Adapter.h" @@ -17,22 +17,31 @@ namespace { struct live_devices { std::mutex mutex; - std::unordered_map values; + struct entry { + WGPUDevice token; + dawn::native::DeviceBase* device; + entry* next; + }; + entry* first{}; }; live_devices& devices() { - // Dawn devices may outlive other process statics. Keep the registry alive - // until process teardown rather than racing its destructor at shutdown. - static auto* value=new live_devices; + // Construct in static storage without registering a destructor. Dawn + // devices may outlive ordinary process statics during loader teardown. + alignas(live_devices) static unsigned char storage[sizeof(live_devices)]; + static auto* value=::new(static_cast(storage)) live_devices; return *value; } dawn::Ref find_device(WGPUDevice token) noexcept { auto& registry=devices(); std::lock_guard lock(registry.mutex); - const auto found=registry.values.find(token); - if(found==registry.values.end()||!found->second->TryAddRef())return {}; - return dawn::AcquireRef(found->second); + for(auto* current=registry.first;current;current=current->next) { + if(current->token!=token)continue; + if(!current->device->TryAddRef())return {}; + return dawn::AcquireRef(current->device); + } + return {}; } bool nonzero_uuid(const uint8_t* value) noexcept { @@ -44,15 +53,21 @@ bool nonzero_uuid(const uint8_t* value) noexcept { namespace webscene::dawn_bridge { void register_device(dawn::native::DeviceBase* device) noexcept { if(!device)return; - try { - auto& registry=devices(); - std::lock_guard lock(registry.mutex); - registry.values[dawn::native::ToAPI(device)]=device; - } catch(...) { - // Device creation remains usable, while the exact-device query fails - // closed because an unregistered token is indistinguishable from one - // created by another WebGPU implementation. + const auto token=dawn::native::ToAPI(device); + auto* value=new(std::nothrow) live_devices::entry{token,device,nullptr}; + if(!value)return; + auto& registry=devices(); + std::lock_guard lock(registry.mutex); + for(auto* current=registry.first;current;current=current->next) { + if(current->token!=token)continue; + // A duplicate authoritative registration is harmless. A token + // collision remains bound to the first live object and therefore + // makes a query for the second object fail closed. + delete value; + return; } + value->next=registry.first; + registry.first=value; } void unregister_device(dawn::native::DeviceBase* device) noexcept { @@ -60,8 +75,16 @@ void unregister_device(dawn::native::DeviceBase* device) noexcept { auto& registry=devices(); std::lock_guard lock(registry.mutex); const auto token=dawn::native::ToAPI(device); - const auto found=registry.values.find(token); - if(found!=registry.values.end()&&found->second==device)registry.values.erase(found); + auto** link=®istry.first; + while(*link) { + auto* current=*link; + if(current->token==token&¤t->device==device) { + *link=current->next; + delete current; + return; + } + link=¤t->next; + } } } @@ -69,7 +92,7 @@ extern "C" webscene_dawn_native_device_status_v1 websceneDawnQueryVulkanDeviceV1 WGPUDevice token,webscene_dawn_native_device_v1* result) { if(!token||!result)return WEBSCENE_DAWN_NATIVE_DEVICE_INVALID_ARGUMENT_V1; if(result->struct_sizeversion!=WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION) + result->version!=WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION_V1) return WEBSCENE_DAWN_NATIVE_DEVICE_INCOMPATIBLE_ABI_V1; auto base=find_device(token); if(!base)return WEBSCENE_DAWN_NATIVE_DEVICE_FOREIGN_DEVICE_V1; @@ -93,7 +116,7 @@ extern "C" webscene_dawn_native_device_status_v1 websceneDawnQueryVulkanDeviceV1 webscene_dawn_native_device_v1 value{}; value.struct_size=sizeof(value); - value.version=WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION; + value.version=WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION_V1; value.adapter=dawn::native::ToAPI(base->GetAdapter()); value.device=token; value.vk_physical_device=reinterpret_cast(physical->GetVkPhysicalDevice()); @@ -105,3 +128,72 @@ extern "C" webscene_dawn_native_device_status_v1 websceneDawnQueryVulkanDeviceV1 *result=value; return WEBSCENE_DAWN_NATIVE_DEVICE_SUCCESS_V1; } + +extern "C" webscene_dawn_native_device_status_v2 websceneDawnQueryVulkanDeviceV2( + WGPUDevice token,webscene_dawn_native_device_v2* result) { + if(!token||!result)return WEBSCENE_DAWN_NATIVE_DEVICE_INVALID_ARGUMENT_V2; + if(result->struct_sizeversion!=WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION_V2) + return WEBSCENE_DAWN_NATIVE_DEVICE_INCOMPATIBLE_ABI_V2; + auto base=find_device(token); + if(!base)return WEBSCENE_DAWN_NATIVE_DEVICE_FOREIGN_DEVICE_V2; + auto guard=base->GetGuard(); + if(base->GetState()!=dawn::native::DeviceBase::State::Alive||!base->GetQueue()) + return WEBSCENE_DAWN_NATIVE_DEVICE_LOST_V2; + if(base->GetPhysicalDevice()->GetBackendType()!=wgpu::BackendType::Vulkan) + return WEBSCENE_DAWN_NATIVE_DEVICE_NOT_VULKAN_V2; + + auto* device=dawn::native::vulkan::ToBackend(base.Get()); + auto* physical=dawn::native::vulkan::ToBackend(base->GetPhysicalDevice()); + auto* queue=dawn::native::vulkan::ToBackend(base->GetQueue()); + const auto instance=device->GetVkInstance(); + const auto resolver=device->fn.GetInstanceProcAddr; + if(instance==VK_NULL_HANDLE||!resolver) + return WEBSCENE_DAWN_NATIVE_DEVICE_INVALID_IDENTITY_V2; + + VkPhysicalDeviceIDProperties ids{}; + ids.sType=VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES; + VkPhysicalDeviceProperties2 properties{}; + properties.sType=VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; + properties.pNext=&ids; + device->fn.GetPhysicalDeviceProperties2(physical->GetVkPhysicalDevice(),&properties); + if(!nonzero_uuid(ids.deviceUUID)||!nonzero_uuid(ids.driverUUID)) + return WEBSCENE_DAWN_NATIVE_DEVICE_INVALID_IDENTITY_V2; + + uint32_t capabilities=0; + const auto& global=device->GetGlobalInfo(); + if(global.HasExt(dawn::native::vulkan::InstanceExt::Surface)) + capabilities|=WEBSCENE_DAWN_VULKAN_SURFACE_EXTENSION_ENABLED_V2; +#if defined(DAWN_USE_X11) + if(global.HasExt(dawn::native::vulkan::InstanceExt::XlibSurface)) + capabilities|=WEBSCENE_DAWN_VULKAN_XLIB_SURFACE_EXTENSION_ENABLED_V2; + if(device->fn.DestroySurfaceKHR&&device->fn.GetPhysicalDeviceSurfaceSupportKHR&& + device->fn.GetPhysicalDeviceSurfaceCapabilitiesKHR&& + device->fn.GetPhysicalDeviceSurfaceFormatsKHR&& + device->fn.GetPhysicalDeviceSurfacePresentModesKHR&& + device->fn.CreateXlibSurfaceKHR&& + device->fn.GetPhysicalDeviceXlibPresentationSupportKHR) + capabilities|=WEBSCENE_DAWN_VULKAN_XLIB_PRESENTATION_PROCS_AVAILABLE_V2; +#endif + if((capabilities&WEBSCENE_DAWN_VULKAN_XLIB_PRESENTATION_REQUIRED_V2)!= + WEBSCENE_DAWN_VULKAN_XLIB_PRESENTATION_REQUIRED_V2) + return WEBSCENE_DAWN_NATIVE_DEVICE_XLIB_PRESENTATION_UNAVAILABLE_V2; + + webscene_dawn_native_device_v2 value{}; + value.struct_size=sizeof(value); + value.version=WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION_V2; + value.adapter=dawn::native::ToAPI(base->GetAdapter()); + value.device=token; + value.vk_instance=reinterpret_cast(instance); + value.vk_physical_device=reinterpret_cast(physical->GetVkPhysicalDevice()); + value.vk_device=reinterpret_cast(device->GetVkDevice()); + value.vk_queue=reinterpret_cast(queue->GetVkQueue()); + value.vk_get_instance_proc_addr= + reinterpret_cast(resolver); + value.queue_family=device->GetGraphicsQueueFamily(); + value.instance_capabilities=capabilities; + std::memcpy(value.device_uuid,ids.deviceUUID,VK_UUID_SIZE); + std::memcpy(value.driver_uuid,ids.driverUUID,VK_UUID_SIZE); + *result=value; + return WEBSCENE_DAWN_NATIVE_DEVICE_SUCCESS_V2; +} diff --git a/eng/graphics/tests/test_dawn_exports.py b/eng/graphics/tests/test_dawn_exports.py index 8a020336..66a6ffbd 100644 --- a/eng/graphics/tests/test_dawn_exports.py +++ b/eng/graphics/tests/test_dawn_exports.py @@ -18,7 +18,8 @@ def test_native_symbol_formats(self): for rid, text in [ ("osx-arm64", "00001000 T _wgpuCreateInstance\n00001010 T _wgpuGetProcAddress\n"), ("linux-x64", "00001000 T wgpuCreateInstance\n00001010 T wgpuGetProcAddress\n" - "00001020 T websceneDawnQueryVulkanDeviceV1\n"), + "00001020 T websceneDawnQueryVulkanDeviceV1\n" + "00001030 T websceneDawnQueryVulkanDeviceV2\n"), ("win-x64", " 1 0 00001000 wgpuCreateInstance\n 2 1 00001010 wgpuGetProcAddress\n")]: with self.subTest(rid=rid): self.assertEqual(self.inspect(text, rid)["status"], "passed") @@ -31,6 +32,8 @@ def test_linux_bridge_is_required_and_exact(self): for text in [ "1000 T wgpuCreateInstance\n1010 T wgpuGetProcAddress\n", "1000 T wgpuCreateInstance\n1010 T wgpuGetProcAddress\n" + "1020 T websceneDawnQueryVulkanDeviceV1\n", + "1000 T wgpuCreateInstance\n1010 T wgpuGetProcAddress\n" "1020 T websceneDawnQueryVulkanDeviceV2\n", ]: with self.subTest(text=text), self.assertRaises(ValueError): diff --git a/eng/graphics/tests/test_dawn_native_device_bridge.py b/eng/graphics/tests/test_dawn_native_device_bridge.py new file mode 100644 index 00000000..7e1fa463 --- /dev/null +++ b/eng/graphics/tests/test_dawn_native_device_bridge.py @@ -0,0 +1,44 @@ +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[3] + + +class DawnNativeDeviceBridgeTests(unittest.TestCase): + def test_v2_validates_public_inputs_before_live_registry_lookup(self): + source = (ROOT / "eng/graphics/dawn_native_device.cpp").read_text() + query = source.split("websceneDawnQueryVulkanDeviceV2(", 1)[1] + self.assertLess(query.index("!token||!result"), query.index("find_device(token)")) + self.assertLess(query.index("result->struct_size"), query.index("find_device(token)")) + self.assertLess(query.index("result->version"), query.index("find_device(token)")) + self.assertLess(query.index("find_device(token)"), query.index("base->GetGuard()")) + + def test_registry_is_compatible_with_dawns_no_exception_build(self): + source = (ROOT / "eng/graphics/dawn_native_device.cpp").read_text() + self.assertIn("new(std::nothrow) live_devices::entry", source) + self.assertNotIn("catch(", source.replace(" ", "")) + self.assertNotIn("std::unordered_map", source) + + def test_v1_and_v2_are_staged_hashed_and_exported_together(self): + header = (ROOT / "experiments/WebScene.NativeEngine.Probe/native/graphics/" + "webscene/dawn_native_device.h").read_text() + symbols = (ROOT / "eng/graphics/DawnSymbolBoundary.cmake").read_text() + exports = (ROOT / "eng/graphics/dawn_exports.py").read_text() + builder = (ROOT / "eng/graphics/build.py").read_text() + verifier = (ROOT / "eng/graphics/verify-sdk.py").read_text() + cache = (ROOT / ".github/actions/graphics-sdk/action.yml").read_text() + for version in ("V1", "V2"): + name = f"websceneDawnQueryVulkanDevice{version}" + self.assertIn(name, header) + self.assertIn(name, symbols) + self.assertIn(name, exports) + for name in ("dawn_native_device.h", "dawn_native_device.cpp", + "dawn_native_device_internal.h"): + self.assertIn(name, builder) + self.assertIn(name, verifier) + self.assertIn(name, cache) + + +if __name__ == "__main__": + unittest.main() diff --git a/experiments/WebScene.NativeEngine.Probe/CMakeLists.txt b/experiments/WebScene.NativeEngine.Probe/CMakeLists.txt index eb91a267..c4dcc97b 100644 --- a/experiments/WebScene.NativeEngine.Probe/CMakeLists.txt +++ b/experiments/WebScene.NativeEngine.Probe/CMakeLists.txt @@ -381,7 +381,8 @@ if(WEBSCENE_NATIVE_ENGINE_ENABLE_GRAPHICS) target_compile_features(webscene_graphics_dawn_linux_external_provider_contract_tests PRIVATE cxx_std_20) target_compile_definitions(webscene_graphics_dawn_linux_external_provider_contract_tests PRIVATE WEBSCENE_DAWN_LINUX_EXTERNAL_FACTORY_VERSION=1 - WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION=1) + WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION=1 + WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION_V2=2) target_include_directories(webscene_graphics_dawn_linux_external_provider_contract_tests PRIVATE native) target_link_libraries(webscene_graphics_dawn_linux_external_provider_contract_tests PRIVATE dawn::webgpu_dawn) diff --git a/experiments/WebScene.NativeEngine.Probe/native/graphics/dawn_linux_external_provider.h b/experiments/WebScene.NativeEngine.Probe/native/graphics/dawn_linux_external_provider.h index 68591cc8..719de7fd 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/graphics/dawn_linux_external_provider.h +++ b/experiments/WebScene.NativeEngine.Probe/native/graphics/dawn_linux_external_provider.h @@ -32,17 +32,21 @@ enum class dawn_linux_external_status : uint32_t { fence_export_failed, invalid_device_factory, native_device_query_failed, - device_lost + device_lost, + xlib_presentation_unavailable }; struct dawn_linux_external_device_lifetime { WGPUAdapter dawn_adapter_token{}; WGPUDevice dawn_device_token{}; // Opaque borrowed identities for the exact Vulkan tuple. They are never - // called or cast by WebScene; native_owner controls their typed lifetime. + // destroyed by WebScene; native_owner controls their typed lifetime. + const void* vk_instance{}; const void* vk_physical_device{}; const void* vk_device{}; const void* vk_queue{}; + webscene_dawn_vk_get_instance_proc_addr_v2 vk_get_instance_proc_addr{}; + webscene_dawn_vulkan_instance_capabilities_v2 instance_capabilities{}; std::array device_uuid{}; std::array driver_uuid{}; uint32_t dawn_queue_family{UINT32_MAX}; @@ -69,26 +73,35 @@ inline dawn_linux_external_status bind_dawn_linux_external_device( return dawn_linux_external_status::invalid_argument; if(device_lost->load(std::memory_order_acquire)) return dawn_linux_external_status::device_lost; - webscene_dawn_native_device_v1 native{}; + webscene_dawn_native_device_v2 native{}; native.struct_size=sizeof(native); - native.version=WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION; - const auto status=websceneDawnQueryVulkanDeviceV1(device.Get(),&native); - if(status==WEBSCENE_DAWN_NATIVE_DEVICE_LOST_V1) + native.version=WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION_V2; + const auto status=websceneDawnQueryVulkanDeviceV2(device.Get(),&native); + if(status==WEBSCENE_DAWN_NATIVE_DEVICE_LOST_V2) return dawn_linux_external_status::device_lost; - if(status==WEBSCENE_DAWN_NATIVE_DEVICE_NOT_VULKAN_V1) + if(status==WEBSCENE_DAWN_NATIVE_DEVICE_NOT_VULKAN_V2) return dawn_linux_external_status::not_vulkan; - if(status!=WEBSCENE_DAWN_NATIVE_DEVICE_SUCCESS_V1) + if(status==WEBSCENE_DAWN_NATIVE_DEVICE_XLIB_PRESENTATION_UNAVAILABLE_V2) + return dawn_linux_external_status::xlib_presentation_unavailable; + if(status!=WEBSCENE_DAWN_NATIVE_DEVICE_SUCCESS_V2) return dawn_linux_external_status::native_device_query_failed; if(native.adapter!=adapter.Get()||native.device!=device.Get()|| - !native.vk_physical_device||!native.vk_device||!native.vk_queue|| - native.queue_family==UINT32_MAX) + !native.vk_instance||!native.vk_physical_device||!native.vk_device|| + !native.vk_queue||!native.vk_get_instance_proc_addr|| + native.queue_family==UINT32_MAX|| + (native.instance_capabilities& + WEBSCENE_DAWN_VULKAN_XLIB_PRESENTATION_REQUIRED_V2)!= + WEBSCENE_DAWN_VULKAN_XLIB_PRESENTATION_REQUIRED_V2) return dawn_linux_external_status::native_device_query_failed; auto lifetime=std::make_shared(); lifetime->dawn_adapter_token=native.adapter; lifetime->dawn_device_token=native.device; + lifetime->vk_instance=native.vk_instance; lifetime->vk_physical_device=native.vk_physical_device; lifetime->vk_device=native.vk_device; lifetime->vk_queue=native.vk_queue; + lifetime->vk_get_instance_proc_addr=native.vk_get_instance_proc_addr; + lifetime->instance_capabilities=native.instance_capabilities; for(size_t index=0;indexdevice_uuid.size();++index) { lifetime->device_uuid[index]=native.device_uuid[index]; lifetime->driver_uuid[index]=native.driver_uuid[index]; @@ -209,7 +222,11 @@ inline bool same_dawn_linux_external_identity( expected->dawn_adapter_token&&expected->dawn_device_token&& expected->dawn_adapter_token==actual->dawn_adapter_token&& expected->dawn_device_token==actual->dawn_device_token&& - expected->vk_physical_device&&expected->vk_device&&expected->vk_queue&& + expected->vk_instance&&expected->vk_physical_device&&expected->vk_device&& + expected->vk_queue&&expected->vk_get_instance_proc_addr&& + (expected->instance_capabilities& + WEBSCENE_DAWN_VULKAN_XLIB_PRESENTATION_REQUIRED_V2)== + WEBSCENE_DAWN_VULKAN_XLIB_PRESENTATION_REQUIRED_V2&& nonzero_uuid(expected->device_uuid)&&nonzero_uuid(expected->driver_uuid)&& expected->dawn_queue_family!=UINT32_MAX; } diff --git a/experiments/WebScene.NativeEngine.Probe/native/graphics/webscene/dawn_native_device.h b/experiments/WebScene.NativeEngine.Probe/native/graphics/webscene/dawn_native_device.h index 1ecb6175..80503bd3 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/graphics/webscene/dawn_native_device.h +++ b/experiments/WebScene.NativeEngine.Probe/native/graphics/webscene/dawn_native_device.h @@ -6,8 +6,16 @@ extern "C" { #endif +#ifndef WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION_V1 +#define WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION_V1 1U +#endif +#ifndef WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION_V2 +#define WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION_V2 2U +#endif +/* The original name remains the v1 value for source compatibility. New code + * selects the explicit versioned constant matching the query it calls. */ #ifndef WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION -#define WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION 1U +#define WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION_V1 #endif typedef uint32_t webscene_dawn_native_device_status_v1; @@ -39,6 +47,53 @@ typedef struct webscene_dawn_native_device_v1 { uint8_t driver_uuid[16]; } webscene_dawn_native_device_v1; +typedef uint32_t webscene_dawn_native_device_status_v2; +enum { + WEBSCENE_DAWN_NATIVE_DEVICE_SUCCESS_V2 = 0, + WEBSCENE_DAWN_NATIVE_DEVICE_INVALID_ARGUMENT_V2 = 1, + WEBSCENE_DAWN_NATIVE_DEVICE_INCOMPATIBLE_ABI_V2 = 2, + WEBSCENE_DAWN_NATIVE_DEVICE_FOREIGN_DEVICE_V2 = 3, + WEBSCENE_DAWN_NATIVE_DEVICE_NOT_VULKAN_V2 = 4, + WEBSCENE_DAWN_NATIVE_DEVICE_LOST_V2 = 5, + WEBSCENE_DAWN_NATIVE_DEVICE_INVALID_IDENTITY_V2 = 6, + WEBSCENE_DAWN_NATIVE_DEVICE_XLIB_PRESENTATION_UNAVAILABLE_V2 = 7 +}; + +typedef void (*webscene_dawn_vk_proc_v2)(void); +typedef webscene_dawn_vk_proc_v2 (*webscene_dawn_vk_get_instance_proc_addr_v2)( + void* vk_instance, const char* name); + +typedef uint32_t webscene_dawn_vulkan_instance_capabilities_v2; +enum { + WEBSCENE_DAWN_VULKAN_SURFACE_EXTENSION_ENABLED_V2 = 1U << 0, + WEBSCENE_DAWN_VULKAN_XLIB_SURFACE_EXTENSION_ENABLED_V2 = 1U << 1, + WEBSCENE_DAWN_VULKAN_XLIB_PRESENTATION_PROCS_AVAILABLE_V2 = 1U << 2, + WEBSCENE_DAWN_VULKAN_XLIB_PRESENTATION_REQUIRED_V2 = + WEBSCENE_DAWN_VULKAN_SURFACE_EXTENSION_ENABLED_V2 | + WEBSCENE_DAWN_VULKAN_XLIB_SURFACE_EXTENSION_ENABLED_V2 | + WEBSCENE_DAWN_VULKAN_XLIB_PRESENTATION_PROCS_AVAILABLE_V2 +}; + +/* V2 adds the VkInstance which owns the returned device and the exact resolver + * Dawn loaded for that instance. All Vulkan identities and the resolver are + * borrowed from the live WGPUDevice and become invalid when it is released. + * The resolver is ABI-compatible with PFN_vkGetInstanceProcAddr on Linux. */ +typedef struct webscene_dawn_native_device_v2 { + uint32_t struct_size; + uint32_t version; + WGPUAdapter adapter; + WGPUDevice device; + void* vk_instance; + void* vk_physical_device; + void* vk_device; + void* vk_queue; + webscene_dawn_vk_get_instance_proc_addr_v2 vk_get_instance_proc_addr; + uint32_t queue_family; + webscene_dawn_vulkan_instance_capabilities_v2 instance_capabilities; + uint8_t device_uuid[16]; + uint8_t driver_uuid[16]; +} webscene_dawn_native_device_v2; + #if defined(_WIN32) #define WEBSCENE_DAWN_NATIVE_DEVICE_EXPORT __declspec(dllimport) #elif defined(__GNUC__) @@ -54,6 +109,15 @@ WEBSCENE_DAWN_NATIVE_DEVICE_EXPORT webscene_dawn_native_device_status_v1 websceneDawnQueryVulkanDeviceV1( WGPUDevice device, webscene_dawn_native_device_v1* result); +/* result must carry sizeof(webscene_dawn_native_device_v2) and ABI version 2. + * Success guarantees all XLIB_PRESENTATION_REQUIRED capability bits. The host + * must still call vkGetPhysicalDeviceXlibPresentationSupportKHR with its exact + * Display and visual before creating a surface. Failure leaves result + * unchanged, and foreign tokens are rejected before private dereference. */ +WEBSCENE_DAWN_NATIVE_DEVICE_EXPORT webscene_dawn_native_device_status_v2 +websceneDawnQueryVulkanDeviceV2( + WGPUDevice device, webscene_dawn_native_device_v2* result); + #undef WEBSCENE_DAWN_NATIVE_DEVICE_EXPORT #ifdef __cplusplus diff --git a/experiments/WebScene.NativeEngine.Probe/tests/graphics_dawn_linux_external_provider_contract_tests.cpp b/experiments/WebScene.NativeEngine.Probe/tests/graphics_dawn_linux_external_provider_contract_tests.cpp index a1af4d6c..b3e084d7 100644 --- a/experiments/WebScene.NativeEngine.Probe/tests/graphics_dawn_linux_external_provider_contract_tests.cpp +++ b/experiments/WebScene.NativeEngine.Probe/tests/graphics_dawn_linux_external_provider_contract_tests.cpp @@ -4,10 +4,13 @@ using namespace webscene::graphics; namespace { void require(bool value){if(!value)throw std::runtime_error("Dawn Linux external provider contract failed");} +webscene_dawn_vk_proc_v2 fake_instance_resolver(void*,const char*){return nullptr;} } int main(){ static_assert(WEBSCENE_DAWN_LINUX_EXTERNAL_FACTORY_VERSION==1); static_assert(WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION==1); + static_assert(WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION_V1==1); + static_assert(WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION_V2==2); webscene_dawn_native_device_v1 query{}; query.struct_size=sizeof(query); query.version=WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION; @@ -20,6 +23,22 @@ int main(){ query.version=WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION+1; require(websceneDawnQueryVulkanDeviceV1(first,&query)== WEBSCENE_DAWN_NATIVE_DEVICE_INCOMPATIBLE_ABI_V1); + webscene_dawn_native_device_v2 query_v2{}; + query_v2.struct_size=sizeof(query_v2); + query_v2.version=WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION_V2; + query_v2.vk_instance=reinterpret_cast(uintptr_t{9}); + require(websceneDawnQueryVulkanDeviceV2(nullptr,&query_v2)== + WEBSCENE_DAWN_NATIVE_DEVICE_INVALID_ARGUMENT_V2); + require(websceneDawnQueryVulkanDeviceV2(first,&query_v2)== + WEBSCENE_DAWN_NATIVE_DEVICE_FOREIGN_DEVICE_V2); + require(query_v2.vk_instance==reinterpret_cast(uintptr_t{9})); + query_v2.struct_size=sizeof(query_v2)-1; + require(websceneDawnQueryVulkanDeviceV2(first,&query_v2)== + WEBSCENE_DAWN_NATIVE_DEVICE_INCOMPATIBLE_ABI_V2); + query_v2.struct_size=sizeof(query_v2); + query_v2.version=WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION_V1; + require(websceneDawnQueryVulkanDeviceV2(first,&query_v2)== + WEBSCENE_DAWN_NATIVE_DEVICE_INCOMPATIBLE_ABI_V2); require(valid_dawn_linux_external_binding(first,first,first)); require(!valid_dawn_linux_external_binding(nullptr,first,first)); require(!valid_dawn_linux_external_binding(first,second,first)); @@ -29,14 +48,25 @@ int main(){ auto device=std::make_shared(); device->dawn_adapter_token=reinterpret_cast(uintptr_t{6}); device->dawn_device_token=first; + device->vk_instance=reinterpret_cast(uintptr_t{7}); device->vk_physical_device=reinterpret_cast(uintptr_t{3}); device->vk_device=reinterpret_cast(uintptr_t{4}); device->vk_queue=reinterpret_cast(uintptr_t{5}); + device->vk_get_instance_proc_addr=&fake_instance_resolver; + device->instance_capabilities=WEBSCENE_DAWN_VULKAN_XLIB_PRESENTATION_REQUIRED_V2; device->device_uuid[0]=1; device->driver_uuid[0]=2;device->dawn_queue_family=4; device->device_lost=std::make_shared>(false); device->native_owner=native_owner; require(same_dawn_linux_external_identity(device,device)); + const auto capabilities=device->instance_capabilities; + device->instance_capabilities=0; + require(!same_dawn_linux_external_identity(device,device)); + device->instance_capabilities=capabilities; + const auto resolver=device->vk_get_instance_proc_addr; + device->vk_get_instance_proc_addr=nullptr; + require(!same_dawn_linux_external_identity(device,device)); + device->vk_get_instance_proc_addr=resolver; auto copied=std::make_shared(*device); require(!same_dawn_linux_external_identity(device,copied)); device->device_lost->store(true,std::memory_order_release); diff --git a/src/WebScene.Sdk/cmake/LinuxSDK.cmake b/src/WebScene.Sdk/cmake/LinuxSDK.cmake index 70415d01..59825174 100644 --- a/src/WebScene.Sdk/cmake/LinuxSDK.cmake +++ b/src/WebScene.Sdk/cmake/LinuxSDK.cmake @@ -37,7 +37,8 @@ if(WEBSCENE_SDK_ENABLE_WEBGPU) target_include_directories(webscene_sdk_webgpu INTERFACE "${WEBSCENE_ROOT}/experiments/WebScene.NativeEngine.Probe/native/graphics") target_compile_definitions(webscene_sdk_webgpu INTERFACE WEBSCENE_DAWN_LINUX_EXTERNAL_FACTORY_VERSION=1 - WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION=1) + WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION=1 + WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION_V2=2) target_link_libraries(webscene_sdk_webgpu INTERFACE webscene_core dawn::webgpu_dawn) add_library(WebScene::WebGPU ALIAS webscene_sdk_webgpu) install(DIRECTORY "${WEBSCENE_GRAPHICS_SDK_ROOT}/dawn/include/" DESTINATION include) diff --git a/src/WebScene.Sdk/cmake/WebSceneLinuxConfig.cmake b/src/WebScene.Sdk/cmake/WebSceneLinuxConfig.cmake index 93a0f680..bf052860 100644 --- a/src/WebScene.Sdk/cmake/WebSceneLinuxConfig.cmake +++ b/src/WebScene.Sdk/cmake/WebSceneLinuxConfig.cmake @@ -41,7 +41,7 @@ if(NOT TARGET WebScene::Core) set_target_properties(WebScene::WebGPU PROPERTIES INTERFACE_LINK_LIBRARIES "WebScene::Core;WebScene::_Dawn;Threads::Threads;${CMAKE_DL_LIBS}" INTERFACE_INCLUDE_DIRECTORIES "${WebScene_SDK_ROOT}/include/graphics" - INTERFACE_COMPILE_DEFINITIONS "WEBSCENE_DAWN_LINUX_EXTERNAL_FACTORY_VERSION=1;WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION=1") + INTERFACE_COMPILE_DEFINITIONS "WEBSCENE_DAWN_LINUX_EXTERNAL_FACTORY_VERSION=1;WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION=1;WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION_V2=2") endif() add_executable(WebScene::Compiler IMPORTED GLOBAL) set_property(TARGET WebScene::Compiler PROPERTY IMPORTED_LOCATION "${WebScene_SDK_ROOT}/bin/webscene-uic") diff --git a/src/WebScene.Sdk/cmake/WebSceneLinuxSDK.cmake b/src/WebScene.Sdk/cmake/WebSceneLinuxSDK.cmake index fb35f97e..b31e2b58 100644 --- a/src/WebScene.Sdk/cmake/WebSceneLinuxSDK.cmake +++ b/src/WebScene.Sdk/cmake/WebSceneLinuxSDK.cmake @@ -42,7 +42,8 @@ if(WEBSCENE_SDK_WEBGPU) target_include_directories(webscene_sdk_webgpu INTERFACE "${WEBSCENE_ROOT}/experiments/WebScene.NativeEngine.Probe/native/graphics") target_compile_definitions(webscene_sdk_webgpu INTERFACE WEBSCENE_DAWN_LINUX_EXTERNAL_FACTORY_VERSION=1 - WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION=1) + WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION=1 + WEBSCENE_DAWN_NATIVE_DEVICE_ABI_VERSION_V2=2) target_link_libraries(webscene_sdk_webgpu INTERFACE webscene_core dawn::webgpu_dawn Threads::Threads) add_library(WebScene::WebGPU ALIAS webscene_sdk_webgpu) install(DIRECTORY "${WEBSCENE_GRAPHICS_SDK_ROOT}/dawn/include/" DESTINATION include)