From 595792c9455721b8e553295c63124c792b3feef0 Mon Sep 17 00:00:00 2001 From: Kai Ninomiya Date: Fri, 26 Sep 2025 00:03:21 -0700 Subject: [PATCH 1/5] Roll Emdawnwebgpu and repoint USE_WEBGPU tests at Emdawnwebgpu In preparation to remove USE_WEBGPU entirely (#24265). Ideally these tests would live in Dawn, but we don't have a way to automate tests in the browser yet, so for the moment we'll keep them in Emscripten. DO NOT SUBMIT: Needs Emdawnwebgpu roll in order to pass --- src/jsifier.mjs | 2 +- test/test_browser.py | 16 +-- test/test_other.py | 17 +-- test/webgpu_basic_rendering.cpp | 195 ++++++++++++-------------------- test/webgpu_get_device.cpp | 9 -- test/webgpu_jsvalstore.cpp | 98 ---------------- test/webgpu_required_limits.c | 58 +++++----- 7 files changed, 115 insertions(+), 280 deletions(-) delete mode 100644 test/webgpu_get_device.cpp delete mode 100644 test/webgpu_jsvalstore.cpp diff --git a/src/jsifier.mjs b/src/jsifier.mjs index 8a6064c56dba1..ab86c44226913 100644 --- a/src/jsifier.mjs +++ b/src/jsifier.mjs @@ -284,7 +284,7 @@ function handleI64Signatures(symbol, snippet, sig, i53abi) { const newArgs = []; let argConversions = ''; if (sig.length > argNames.length + 1) { - error(`handleI64Signatures: signature too long for ${symbol}`); + error(`handleI64Signatures: signature '${sig}' too long for ${symbol}(${argNames.join(', ')})`); return snippet; } for (let i = 0; i < argNames.length; i++) { diff --git a/test/test_browser.py b/test/test_browser.py index 3b73a0e103d0f..5de403d081df5 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -4450,24 +4450,16 @@ def test_webgl_simple_extensions(self, webgl_version, simple_enable_extensions): 'closure': (['-sASSERTIONS', '--closure=1'],), 'closure_advanced': (['-sASSERTIONS', '--closure=1', '-O3'],), 'main_module': (['-sMAIN_MODULE=1'],), + 'pthreads': (['-pthread', '-sOFFSCREENCANVAS_SUPPORT'],), }) @requires_webgpu def test_webgpu_basic_rendering(self, args): - self.btest_exit('webgpu_basic_rendering.cpp', cflags=['-Wno-error=deprecated', '-sUSE_WEBGPU'] + args) + self.btest_exit('webgpu_basic_rendering.cpp', cflags=['--use-port=emdawnwebgpu', '-sEXIT_RUNTIME'] + args) @requires_webgpu def test_webgpu_required_limits(self): - self.btest_exit('webgpu_required_limits.c', cflags=['-Wno-error=deprecated', '-sUSE_WEBGPU', '-sASYNCIFY']) - - @requires_webgpu - def test_webgpu_basic_rendering_pthreads(self): - self.btest_exit('webgpu_basic_rendering.cpp', cflags=['-Wno-error=deprecated', '-sUSE_WEBGPU', '-pthread', '-sOFFSCREENCANVAS_SUPPORT']) - - def test_webgpu_get_device(self): - self.btest_exit('webgpu_get_device.cpp', cflags=['-Wno-error=deprecated', '-sUSE_WEBGPU', '-sASSERTIONS', '--closure=1']) - - def test_webgpu_get_device_pthreads(self): - self.btest_exit('webgpu_get_device.cpp', cflags=['-Wno-error=deprecated', '-sUSE_WEBGPU', '-pthread']) + self.set_setting('NO_DEFAULT_TO_CXX', 0) # emdawnwebgpu uses C++ internally + self.btest_exit('webgpu_required_limits.c', cflags=['--use-port=emdawnwebgpu', '-sEXIT_RUNTIME']) # Tests the feature that shell html page can preallocate the typed array and place it # to Module.buffer before loading the script page. diff --git a/test/test_other.py b/test/test_other.py index 86d4797bfafed..11c4561a013fe 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -2586,6 +2586,7 @@ def test_contrib_ports(self): @requires_network def test_remote_ports(self): + self.set_setting('NO_DEFAULT_TO_CXX', 0) # emdawnwebgpu uses C++ internally self.emcc(test_file('hello_world.c'), ['--use-port=emdawnwebgpu']) @crossplatform @@ -9585,13 +9586,12 @@ def test_closure_full_js_library(self, args): @also_with_wasm64 def test_closure_webgpu(self): - # This test can be removed if USE_WEBGPU is later included in INCLUDE_FULL_LIBRARY. + self.set_setting('NO_DEFAULT_TO_CXX', 0) # emdawnwebgpu uses C++ internally self.build('hello_world.c', cflags=[ '--closure=1', '-Werror=closure', - '-Wno-error=deprecated', '-sINCLUDE_FULL_LIBRARY', - '-sUSE_WEBGPU', + '--use-port=emdawnwebgpu', ]) # Tests --closure-args command line flag @@ -12230,15 +12230,6 @@ def test_standalone_syscalls(self): for engine in config.WASM_ENGINES: self.assertContained(expected, self.run_js('test.wasm', engine)) - @parameterized({ - '': ([],), - 'assertions': (['-sASSERTIONS'],), - 'closure': (['-sASSERTIONS', '--closure=1'],), - 'dylink': (['-sMAIN_MODULE'],), - }) - def test_webgpu_compiletest(self, args): - self.run_process([EMXX, test_file('webgpu_jsvalstore.cpp'), '-Wno-error=deprecated', '-sUSE_WEBGPU', '-sASYNCIFY'] + args) - @flaky('https://github.com/emscripten-core/emscripten/issues/25343') @also_with_wasm64 @parameterized({ @@ -12247,7 +12238,7 @@ def test_webgpu_compiletest(self, args): 'closure_assertions': (['--closure=1', '-Werror=closure', '-sASSERTIONS'],), }) def test_emdawnwebgpu_link_test(self, args): - self.run_process([EMXX, test_file('test_emdawnwebgpu_link_test.cpp'), '--use-port=emdawnwebgpu', '-sASYNCIFY'] + args) + self.emcc(test_file('test_emdawnwebgpu_link_test.cpp'), ['--use-port=emdawnwebgpu', '-sASYNCIFY'] + args) def test_signature_mismatch(self): create_file('a.c', 'void foo(); int main() { foo(); return 0; }') diff --git a/test/webgpu_basic_rendering.cpp b/test/webgpu_basic_rendering.cpp index 7016a1f127f49..5f625f75244ec 100644 --- a/test/webgpu_basic_rendering.cpp +++ b/test/webgpu_basic_rendering.cpp @@ -4,6 +4,8 @@ // found in the LICENSE file. // Based on https://github.com/kainino0x/webgpu-cross-platform-demo +// (specifically on an old version that didn't use Asyncify; here we've +// intentionally kept Asyncify off to have some coverage of that case). #include @@ -16,7 +18,6 @@ #include #include -#include static const wgpu::Instance instance = wgpuCreateInstance(nullptr); @@ -39,53 +40,50 @@ static wgpu::Device device; static wgpu::Queue queue; static wgpu::Buffer readbackBuffer; static wgpu::RenderPipeline pipeline; -static int testsCompleted = 0; -void GetAdapter(void (*callback)(wgpu::Adapter)) { - instance.RequestAdapter(nullptr, [](WGPURequestAdapterStatus status, WGPUAdapter cAdapter, const char* message, void* userdata) { - if (message) { - printf("RequestAdapter: %s\n", message); +void GetDevice(void (*callback)()) { + instance.RequestAdapter(nullptr, wgpu::CallbackMode::AllowSpontaneous, [=](wgpu::RequestAdapterStatus status, wgpu::Adapter a, wgpu::StringView message) { + if (message.length) { + printf("RequestAdapter: %.*s\n", int(message.length), message.data); } - assert(status == WGPURequestAdapterStatus_Success); + assert(status == wgpu::RequestAdapterStatus::Success); - wgpu::Adapter adapter = wgpu::Adapter::Acquire(cAdapter); - reinterpret_cast(userdata)(adapter); - }, reinterpret_cast(callback)); -} + wgpu::DeviceDescriptor desc; + desc.SetUncapturedErrorCallback( + [](const wgpu::Device&, wgpu::ErrorType errorType, wgpu::StringView message) { + printf("UncapturedError (type=%d): %.*s\n", errorType, int(message.length), message.data); + }); -void GetDevice(void (*callback)(wgpu::Device)) { - adapter.RequestDevice(nullptr, [](WGPURequestDeviceStatus status, WGPUDevice cDevice, const char* message, void* userdata) { - if (message) { - printf("RequestDevice: %s\n", message); - } - assert(status == WGPURequestDeviceStatus_Success); + adapter = a; + adapter.RequestDevice(&desc, wgpu::CallbackMode::AllowSpontaneous, [=](wgpu::RequestDeviceStatus status, wgpu::Device d, wgpu::StringView message) { + if (message.length) { + printf("RequestDevice: %.*s\n", int(message.length), message.data); + } + assert(status == wgpu::RequestDeviceStatus::Success); - wgpu::Device device = wgpu::Device::Acquire(cDevice); - reinterpret_cast(userdata)(device); - }, reinterpret_cast(callback)); + device = d; + callback(); + }); + }); } void init() { - device.SetUncapturedErrorCallback( - [](WGPUErrorType errorType, const char* message, void*) { - printf("%d: %s\n", errorType, message); - }, nullptr); - queue = device.GetQueue(); wgpu::ShaderModule shaderModule{}; { - wgpu::ShaderModuleWGSLDescriptor wgslDesc{}; + wgpu::ShaderSourceWGSL wgslDesc{}; wgslDesc.code = shaderCode; wgpu::ShaderModuleDescriptor descriptor{}; descriptor.nextInChain = &wgslDesc; shaderModule = device.CreateShaderModule(&descriptor); - shaderModule.GetCompilationInfo([](WGPUCompilationInfoRequestStatus status, const WGPUCompilationInfo* info, void*) { - assert(status == WGPUCompilationInfoRequestStatus_Success); - assert(info->messageCount == 0); - std::printf("Shader compile succeeded\n"); - }, nullptr); + shaderModule.GetCompilationInfo(wgpu::CallbackMode::AllowSpontaneous, + [=](wgpu::CompilationInfoRequestStatus status, const wgpu::CompilationInfo* info) { + assert(status == wgpu::CompilationInfoRequestStatus::Success); + assert(info->messageCount == 0); + printf("Shader compile succeeded\n"); + }); } { @@ -128,7 +126,7 @@ void init() { // The depth stencil attachment isn't really needed to draw the triangle // and doesn't really affect the render result. // But having one should give us a slightly better test coverage for the compile of the depth stencil descriptor. -void render(wgpu::TextureView view, wgpu::TextureView depthStencilView) { +void render(wgpu::Device device, wgpu::TextureView view, wgpu::TextureView depthStencilView) { wgpu::RenderPassColorAttachment attachment{}; attachment.view = view; attachment.loadOp = wgpu::LoadOp::Clear; @@ -162,38 +160,27 @@ void render(wgpu::TextureView view, wgpu::TextureView depthStencilView) { queue.Submit(1, &commands); } -void issueContentsCheck(const char* functionName, +void issueContentsCheck(std::string functionName, wgpu::Device device, wgpu::Buffer readbackBuffer, uint32_t expectData) { - struct UserData { - const char* functionName; - wgpu::Buffer readbackBuffer; - uint32_t expectData; - }; - - UserData* userdata = new UserData; - userdata->functionName = functionName; - userdata->readbackBuffer = readbackBuffer; - userdata->expectData = expectData; - readbackBuffer.MapAsync( - wgpu::MapMode::Read, 0, 4, - [](WGPUBufferMapAsyncStatus status, void* vp_userdata) { - assert(status == WGPUBufferMapAsyncStatus_Success); - std::unique_ptr userdata(reinterpret_cast(vp_userdata)); + wgpu::MapMode::Read, 0, 4, wgpu::CallbackMode::AllowSpontaneous, + [=](wgpu::MapAsyncStatus status, wgpu::StringView message) { + if (message.length) { + printf("readbackBuffer.MapAsync: %.*s\n", int(message.length), message.data); + } + assert(status == wgpu::MapAsyncStatus::Success); - const void* ptr = userdata->readbackBuffer.GetConstMappedRange(); + const void* ptr = readbackBuffer.GetConstMappedRange(); - printf("%s: readback -> %p%s\n", userdata->functionName, + printf("%s: readback -> %p%s\n", functionName.c_str(), ptr, ptr ? "" : " <------- FAILED"); assert(ptr != nullptr); uint32_t readback = static_cast(ptr)[0]; - userdata->readbackBuffer.Unmap(); + readbackBuffer.Unmap(); printf(" got %08x, expected %08x%s\n", - readback, userdata->expectData, - readback == userdata->expectData ? "" : " <------- FAILED"); - - testsCompleted++; - }, userdata); + readback, expectData, + readback == expectData ? "" : " <------- FAILED"); + }); } void doCopyTestMappedAtCreation(bool useRange) { @@ -238,7 +225,7 @@ void doCopyTestMappedAtCreation(bool useRange) { } queue.Submit(1, &commands); - issueContentsCheck(__FUNCTION__, dst, kValue); + issueContentsCheck(__FUNCTION__, device, dst, kValue); } void doCopyTestMapAsync(bool useRange) { @@ -253,32 +240,23 @@ void doCopyTestMapAsync(bool useRange) { } size_t offset = useRange ? 8 : 0; - struct UserData { - const char* functionName; - bool useRange; - size_t offset; - wgpu::Buffer src; - }; - - UserData* userdata = new UserData; - userdata->functionName = __FUNCTION__; - userdata->useRange = useRange; - userdata->offset = offset; - userdata->src = src; - - src.MapAsync(wgpu::MapMode::Write, offset, 4, - [](WGPUBufferMapAsyncStatus status, void* vp_userdata) { - assert(status == WGPUBufferMapAsyncStatus_Success); - std::unique_ptr userdata(reinterpret_cast(vp_userdata)); - - uint32_t* ptr = static_cast(userdata->useRange ? - userdata->src.GetMappedRange(userdata->offset, 4) : - userdata->src.GetMappedRange()); - printf("%s: getMappedRange -> %p%s\n", userdata->functionName, + std::string functionName = __FUNCTION__; + src.MapAsync( + wgpu::MapMode::Write, offset, 4, wgpu::CallbackMode::AllowSpontaneous, + [=](wgpu::MapAsyncStatus status, wgpu::StringView message) { + if (message.length) { + printf("src.MapAsync: %.*s\n", int(message.length), message.data); + } + assert(status == wgpu::MapAsyncStatus::Success); + + uint32_t* ptr = static_cast(useRange ? + src.GetMappedRange(offset, 4) : + src.GetMappedRange()); + printf("%s: getMappedRange -> %p%s\n", functionName.c_str(), ptr, ptr ? "" : " <------- FAILED"); assert(ptr != nullptr); *ptr = kValue; - userdata->src.Unmap(); + src.Unmap(); wgpu::Buffer dst; { @@ -291,13 +269,13 @@ void doCopyTestMapAsync(bool useRange) { wgpu::CommandBuffer commands; { wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); - encoder.CopyBufferToBuffer(userdata->src, userdata->offset, dst, 0, 4); + encoder.CopyBufferToBuffer(src, offset, dst, 0, 4); commands = encoder.Finish(); } queue.Submit(1, &commands); - issueContentsCheck(userdata->functionName, dst, kValue); - }, userdata); + issueContentsCheck(functionName, device, dst, kValue); + }); } void doRenderTest() { @@ -324,7 +302,7 @@ void doRenderTest() { descriptor.format = wgpu::TextureFormat::Depth32Float; depthTexture = device.CreateTexture(&descriptor); } - render(readbackTexture.CreateView(), depthTexture.CreateView()); + render(device, readbackTexture.CreateView(), depthTexture.CreateView()); { // A little texture.GetFormat test @@ -343,10 +321,10 @@ void doRenderTest() { wgpu::CommandBuffer commands; { wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); - wgpu::ImageCopyTexture src{}; + wgpu::TexelCopyTextureInfo src{}; src.texture = readbackTexture; src.origin = {0, 0, 0}; - wgpu::ImageCopyBuffer dst{}; + wgpu::TexelCopyBufferInfo dst{}; dst.buffer = readbackBuffer; dst.layout.bytesPerRow = 256; wgpu::Extent3D extent = {1, 1, 1}; @@ -357,7 +335,7 @@ void doRenderTest() { // Check the color value encoded in the shader makes it out correctly. static const uint32_t expectData = 0xff0080ff; - issueContentsCheck(__FUNCTION__, readbackBuffer, expectData); + issueContentsCheck(__FUNCTION__, device, readbackBuffer, expectData); } wgpu::Surface surface; @@ -370,15 +348,10 @@ void frame() { surface.GetCurrentTexture(&surfaceTexture); wgpu::TextureView backbuffer = surfaceTexture.texture.CreateView(); - render(backbuffer, canvasDepthStencilView); - - // TODO: Read back from the canvas with drawImage() (or something) and - // check the result. + render(device, backbuffer, canvasDepthStencilView); + // Test should complete when runtime exists after all async work is done. emscripten_cancel_main_loop(); - - // exit(0) (rather than emscripten_force_exit(0)) ensures there is no dangling keepalive. - exit(0); } void run() { @@ -391,7 +364,7 @@ void run() { doRenderTest(); { - wgpu::SurfaceDescriptorFromCanvasHTMLSelector canvasDesc{}; + wgpu::EmscriptenSurfaceSourceCanvasHTMLSelector canvasDesc{}; canvasDesc.selector = "#canvas"; wgpu::SurfaceDescriptor surfDesc{}; @@ -405,9 +378,9 @@ void run() { .device = device, .format = capabilities.formats[0], .usage = wgpu::TextureUsage::RenderAttachment, - .alphaMode = wgpu::CompositeAlphaMode::Auto, .width = kWidth, .height = kHeight, + .alphaMode = wgpu::CompositeAlphaMode::Auto, .presentMode = wgpu::PresentMode::Fifo}; surface.Configure(&config); @@ -419,33 +392,15 @@ void run() { canvasDepthStencilView = device.CreateTexture(&descriptor).CreateView(); } } + emscripten_set_main_loop(frame, 0, false); } int main() { - GetAdapter([](wgpu::Adapter a) { - adapter = a; - - wgpu::AdapterInfo info; - adapter.GetInfo(&info); - printf("adapter vendor: %s\n", info.vendor); - printf("adapter architecture: %s\n", info.architecture); - printf("adapter device: %s\n", info.device); - printf("adapter description: %s\n", info.description); - - GetDevice([](wgpu::Device dev) { - device = dev; - run(); - }); - }); + GetDevice(run); - // The test result will be reported when the main_loop completes. - // emscripten_exit_with_live_runtime isn't needed because the WebGPU - // callbacks should all automatically keep the runtime alive until - // emscripten_set_main_loop, and that should keep it alive until - // emscripten_cancel_main_loop. - // - // This code is returned when the runtime exits unless something else sets - // it, like exit(0). - return 99; + // This exit code will be reported when all of the async operations + // complete and the main loop is cancelled. Until then, keepalive keeps + // the runtime from exiting. + return 0; } diff --git a/test/webgpu_get_device.cpp b/test/webgpu_get_device.cpp deleted file mode 100644 index 80ef054bf3c66..0000000000000 --- a/test/webgpu_get_device.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include -#include - -int main() { - EM_ASM({ - Module['preinitializedWebGPUDevice'] = { this_is: 'a_dummy_object' }; - }); - emscripten_webgpu_get_device(); -} diff --git a/test/webgpu_jsvalstore.cpp b/test/webgpu_jsvalstore.cpp deleted file mode 100644 index b37f02d1c22ea..0000000000000 --- a/test/webgpu_jsvalstore.cpp +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2020 The Emscripten Authors. All rights reserved. -// Emscripten is available under two separate licenses, the MIT license and the -// University of Illinois/NCSA Open Source License. Both these licenses can be -// found in the LICENSE file. - -// Make sure this header exists and compiles -// (webgpu_cpp.h includes webgpu.h so that's tested too). -#include - -#include -#include - -class EmJsHandle { -public: - EmJsHandle() : mHandle(0) {} - EmJsHandle(int handle) : mHandle(handle) {} - ~EmJsHandle() { - if (mHandle != 0) { - emscripten_webgpu_release_js_handle(mHandle); - } - } - - EmJsHandle(const EmJsHandle&) = delete; - EmJsHandle& operator=(const EmJsHandle&) = delete; - - EmJsHandle(EmJsHandle&& rhs) : mHandle(rhs.mHandle) { rhs.mHandle = 0; } - - EmJsHandle& operator=(EmJsHandle&& rhs) { - int tmp = rhs.mHandle; - rhs.mHandle = this->mHandle; - this->mHandle = tmp; - return *this; - } - - int Get() { return mHandle; } - -private: - int mHandle; -}; - -EM_ASYNC_JS(int, init_js_device, (), { - const adapter = await navigator.gpu.requestAdapter(); - const device = await adapter.requestDevice(); - return JsValStore.add(device); -}); - -wgpu::Device init_device() { - EmJsHandle deviceHandle = EmJsHandle(init_js_device()); - wgpu::Device device = wgpu::Device::Acquire(emscripten_webgpu_import_device(deviceHandle.Get())); - return device; -} - -int main() { - wgpu::Device device = init_device(); - - wgpu::BufferDescriptor desc = {}; - desc.size = 4; - desc.usage = wgpu::BufferUsage::MapWrite | wgpu::BufferUsage::CopySrc; - wgpu::Buffer buffer = device.CreateBuffer(&desc); - - EmJsHandle bufferHandle = EmJsHandle(emscripten_webgpu_export_buffer(buffer.Get())); - EM_ASM( - { - const b = JsValStore.get($0); - b.mapAsync(GPUMapMode.WRITE).then(() => { - console.log('Mapping length', b.getMappedRange().byteLength); - b.unmap(); - }); - }, bufferHandle.Get()); - - EmJsHandle deviceHandle = EmJsHandle(emscripten_webgpu_export_device(device.Get())); - EmJsHandle textureHandle = EmJsHandle(EM_ASM_INT( - { - const device = JsValStore.get($0); - const t = device.createTexture({ - size : [ 16, 16 ], - usage : GPUTextureUsage.COPY_DST, - format : 'rgba8unorm', - }); - return JsValStore.add(t); - }, - deviceHandle.Get())); - - EmJsHandle canvasHandle = - EmJsHandle(EM_ASM_INT({ return JsValStore.add(document.createElement('canvas')); })); - EM_ASM( - { - const device = JsValStore.get($0); - const canvas = JsValStore.get($1); - const texture = JsValStore.get($2); - console.log('Copy', canvas, 'to', texture, 'with', device); - }, - deviceHandle.Get(), canvasHandle.Get(), textureHandle.Get()); - -#ifdef REPORT_RESULT - REPORT_RESULT(0); -#endif -} diff --git a/test/webgpu_required_limits.c b/test/webgpu_required_limits.c index 6250cdfc67d80..106aa8fd6e8ac 100644 --- a/test/webgpu_required_limits.c +++ b/test/webgpu_required_limits.c @@ -3,12 +3,10 @@ #include #include -WGPUSupportedLimits adapter_supported_limits = { - 0, -}; +static WGPULimits adapter_supported_limits = {0}; -void assertLimitsCompatible(WGPULimits required_limits, - WGPULimits supported_limits) { +static void assertLimitsCompatible(WGPULimits required_limits, + WGPULimits supported_limits) { #define ASSERT_LIMITS_COMPATIBLE(limitName) \ assert(required_limits.limitName == supported_limits.limitName) ASSERT_LIMITS_COMPATIBLE(maxTextureDimension1D); @@ -33,7 +31,6 @@ void assertLimitsCompatible(WGPULimits required_limits, ASSERT_LIMITS_COMPATIBLE(maxBufferSize); ASSERT_LIMITS_COMPATIBLE(maxVertexAttributes); ASSERT_LIMITS_COMPATIBLE(maxVertexBufferArrayStride); - ASSERT_LIMITS_COMPATIBLE(maxInterStageShaderComponents); ASSERT_LIMITS_COMPATIBLE(maxInterStageShaderVariables); ASSERT_LIMITS_COMPATIBLE(maxColorAttachments); ASSERT_LIMITS_COMPATIBLE(maxColorAttachmentBytesPerSample); @@ -46,43 +43,50 @@ void assertLimitsCompatible(WGPULimits required_limits, #undef ASSERT_LIMITS_COMPATIBLE } -void on_device_request_ended(WGPURequestDeviceStatus status, - WGPUDevice device, - char const* message, - void* userdata) { +static void on_device_request_ended(WGPURequestDeviceStatus status, + WGPUDevice device, + WGPUStringView message, + void* userdata1, void* userdata2) { assert(status == WGPURequestDeviceStatus_Success); - WGPUSupportedLimits device_supported_limits; + WGPULimits device_supported_limits = {0}; wgpuDeviceGetLimits(device, &device_supported_limits); // verify that the obtained device fullfils required limits - assertLimitsCompatible(adapter_supported_limits.limits, - device_supported_limits.limits); + assertLimitsCompatible(adapter_supported_limits, + device_supported_limits); + + wgpuDeviceRelease(device); + exit(0); } -void on_adapter_request_ended(WGPURequestAdapterStatus status, - WGPUAdapter adapter, - char const* message, - void* userdata) { +static void on_adapter_request_ended(WGPURequestAdapterStatus status, + WGPUAdapter adapter, + WGPUStringView message, + void* userdata1, void* userdata2) { assert(status == WGPURequestAdapterStatus_Success); wgpuAdapterGetLimits(adapter, &adapter_supported_limits); - // for device limits, require the limits supported by adapter - WGPURequiredLimits device_required_limits = {0,}; - device_required_limits.limits = adapter_supported_limits.limits; + WGPUDeviceDescriptor device_desc = { + // for device limits, require the limits supported by adapter + .requiredLimits = &adapter_supported_limits, + }; + wgpuAdapterRequestDevice(adapter, &device_desc, (WGPURequestDeviceCallbackInfo){ + .mode = WGPUCallbackMode_AllowSpontaneous, + .callback = on_device_request_ended, + }); - WGPUDeviceDescriptor device_desc = {0,}; - device_desc.requiredFeatureCount = 0; - device_desc.requiredLimits = &device_required_limits; - wgpuAdapterRequestDevice(adapter, &device_desc, on_device_request_ended, NULL); + wgpuAdapterRelease(adapter); } -int main() { +int main(void) { const WGPUInstance instance = wgpuCreateInstance(NULL); - WGPURequestAdapterOptions adapter_options = {0,}; - wgpuInstanceRequestAdapter(instance, &adapter_options, on_adapter_request_ended, NULL); + wgpuInstanceRequestAdapter(instance, NULL, (WGPURequestAdapterCallbackInfo){ + .mode = WGPUCallbackMode_AllowSpontaneous, + .callback = on_adapter_request_ended, + }); // This code is returned when the runtime exits unless something else sets // it, like exit(0). From d12517b991a47df028f478fa41889d448961c565 Mon Sep 17 00:00:00 2001 From: Kai Ninomiya Date: Fri, 26 Sep 2025 11:07:36 -0700 Subject: [PATCH 2/5] stronger test that keepalives work correctly --- test/webgpu_basic_rendering.cpp | 84 ++++++++++++++++++++++++--------- 1 file changed, 61 insertions(+), 23 deletions(-) diff --git a/test/webgpu_basic_rendering.cpp b/test/webgpu_basic_rendering.cpp index 5f625f75244ec..ec4329f724958 100644 --- a/test/webgpu_basic_rendering.cpp +++ b/test/webgpu_basic_rendering.cpp @@ -18,6 +18,42 @@ #include #include +#include + +EM_JS_DEPS(deps, "$keepRuntimeAlive"); + +// Keeps track of whether async tests are still alive to make sure they finish +// before exit. This tests that keepalives exist where they should. +static int sScopeCount = 0; +class ScopedCounter { + public: + ScopedCounter(const ScopedCounter&&) { Increment(); } + ScopedCounter(const ScopedCounter&) { Increment(); } + ScopedCounter() { Increment(); } + ~ScopedCounter() { Decrement(); } + private: + void Increment() { sScopeCount++; } + void Decrement() { + assert(sScopeCount > 0); + sScopeCount--; + if (sScopeCount == 0) { + // Check we don't reach 0 before the runtime is ready to exit. + // (Make sure this test has scopes for everything that does keepalive.) + bool runtime_is_kept_alive = EM_ASM_INT({ return keepRuntimeAlive(); }); + assert(!runtime_is_kept_alive); + } + } +}; + +void RegisterCheckScopesAtExit() { + atexit([](){ + // Check we don't exit before the tests are done. + // (Make sure there's a keepalive for everything the test has scopes for.) + assert(sScopeCount == 0); + // Overwrite the old return code and exit now that everything is done. + exit(0); + }); +} static const wgpu::Instance instance = wgpuCreateInstance(nullptr); @@ -126,7 +162,7 @@ void init() { // The depth stencil attachment isn't really needed to draw the triangle // and doesn't really affect the render result. // But having one should give us a slightly better test coverage for the compile of the depth stencil descriptor. -void render(wgpu::Device device, wgpu::TextureView view, wgpu::TextureView depthStencilView) { +void render(wgpu::TextureView view, wgpu::TextureView depthStencilView) { wgpu::RenderPassColorAttachment attachment{}; attachment.view = view; attachment.loadOp = wgpu::LoadOp::Clear; @@ -160,11 +196,10 @@ void render(wgpu::Device device, wgpu::TextureView view, wgpu::TextureView depth queue.Submit(1, &commands); } -void issueContentsCheck(std::string functionName, wgpu::Device device, - wgpu::Buffer readbackBuffer, uint32_t expectData) { +void issueContentsCheck(ScopedCounter scope, std::string functionName, wgpu::Buffer readbackBuffer, uint32_t expectData) { readbackBuffer.MapAsync( wgpu::MapMode::Read, 0, 4, wgpu::CallbackMode::AllowSpontaneous, - [=](wgpu::MapAsyncStatus status, wgpu::StringView message) { + [=, scope=scope](wgpu::MapAsyncStatus status, wgpu::StringView message) { if (message.length) { printf("readbackBuffer.MapAsync: %.*s\n", int(message.length), message.data); } @@ -183,7 +218,7 @@ void issueContentsCheck(std::string functionName, wgpu::Device device, }); } -void doCopyTestMappedAtCreation(bool useRange) { +void doCopyTestMappedAtCreation(ScopedCounter scope, bool useRange) { static constexpr uint32_t kValue = 0x05060708; size_t size = useRange ? 12 : 4; wgpu::Buffer src; @@ -225,10 +260,10 @@ void doCopyTestMappedAtCreation(bool useRange) { } queue.Submit(1, &commands); - issueContentsCheck(__FUNCTION__, device, dst, kValue); + issueContentsCheck(scope, __FUNCTION__, dst, kValue); } -void doCopyTestMapAsync(bool useRange) { +void doCopyTestMapAsync(ScopedCounter scope, bool useRange) { static constexpr uint32_t kValue = 0x01020304; size_t size = useRange ? 12 : 4; wgpu::Buffer src; @@ -274,11 +309,11 @@ void doCopyTestMapAsync(bool useRange) { } queue.Submit(1, &commands); - issueContentsCheck(functionName, device, dst, kValue); + issueContentsCheck(scope, functionName, dst, kValue); }); } -void doRenderTest() { +void doRenderTest(ScopedCounter scope) { wgpu::Texture readbackTexture; { wgpu::TextureDescriptor descriptor{}; @@ -302,7 +337,7 @@ void doRenderTest() { descriptor.format = wgpu::TextureFormat::Depth32Float; depthTexture = device.CreateTexture(&descriptor); } - render(device, readbackTexture.CreateView(), depthTexture.CreateView()); + render(readbackTexture.CreateView(), depthTexture.CreateView()); { // A little texture.GetFormat test @@ -335,7 +370,7 @@ void doRenderTest() { // Check the color value encoded in the shader makes it out correctly. static const uint32_t expectData = 0xff0080ff; - issueContentsCheck(__FUNCTION__, device, readbackBuffer, expectData); + issueContentsCheck(scope, __FUNCTION__, readbackBuffer, expectData); } wgpu::Surface surface; @@ -343,12 +378,14 @@ wgpu::TextureView canvasDepthStencilView; const uint32_t kWidth = 300; const uint32_t kHeight = 150; -void frame() { +void frame(void* vp_scope) { + auto scope = std::unique_ptr(reinterpret_cast(vp_scope)); + wgpu::SurfaceTexture surfaceTexture; surface.GetCurrentTexture(&surfaceTexture); wgpu::TextureView backbuffer = surfaceTexture.texture.CreateView(); - render(device, backbuffer, canvasDepthStencilView); + render(backbuffer, canvasDepthStencilView); // Test should complete when runtime exists after all async work is done. emscripten_cancel_main_loop(); @@ -357,11 +394,12 @@ void frame() { void run() { init(); - doCopyTestMappedAtCreation(false); - doCopyTestMappedAtCreation(true); - doCopyTestMapAsync(false); - doCopyTestMapAsync(true); - doRenderTest(); + ScopedCounter scope; + doCopyTestMappedAtCreation(scope, false); + doCopyTestMappedAtCreation(scope, true); + doCopyTestMapAsync(scope, false); + doCopyTestMapAsync(scope, true); + doRenderTest(scope); { wgpu::EmscriptenSurfaceSourceCanvasHTMLSelector canvasDesc{}; @@ -393,14 +431,14 @@ void run() { } } - emscripten_set_main_loop(frame, 0, false); + emscripten_set_main_loop_arg(frame, new ScopedCounter(), 0, false); } int main() { GetDevice(run); - // This exit code will be reported when all of the async operations - // complete and the main loop is cancelled. Until then, keepalive keeps - // the runtime from exiting. - return 0; + RegisterCheckScopesAtExit(); + // The test result will be reported when all scopes exit. + // If that doesn't happen, this is the fallback return code. + return 99; } From ef5a6daed7de5e16459714c7d5f901c7be5af38a Mon Sep 17 00:00:00 2001 From: Kai Ninomiya Date: Fri, 26 Sep 2025 17:24:27 -0700 Subject: [PATCH 3/5] Roll Emdawnwebgpu --- tools/ports/emdawnwebgpu.py | 46 +++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/tools/ports/emdawnwebgpu.py b/tools/ports/emdawnwebgpu.py index b78d15bc19cfe..d0a2c3221ddee 100644 --- a/tools/ports/emdawnwebgpu.py +++ b/tools/ports/emdawnwebgpu.py @@ -3,9 +3,10 @@ # University of Illinois/NCSA Open Source License. Both these licenses can be # found in the LICENSE file. -# https://dawn.googlesource.com/dawn/+/80062b708e44aa4d8c48e555ed0cc801396069f6/src/emdawnwebgpu/pkg/README.md +# https://dawn.googlesource.com/dawn/+/faa7054b5b65c3ce3774151952a68aa7668aa20b/src/emdawnwebgpu/pkg/README.md r""" -The full README of Emdawnwebgpu follows. +This "remote port" instructs Emscripten (4.0.10+) how to automatically download +the actual port for Emdawnwebgpu. See README below for instructions. # Emdawnwebgpu @@ -84,6 +85,17 @@ --closure-args=--externs=path/to/emdawnwebgpu_pkg/webgpu/src/webgpu-externs.js +#### Without using a port file (**Unsupported!**) + +It is possible to integrate the Emdawnwebgpu sources directly into your build +process, which may be necessary for certain build systems, but this is not +officially supported. Using a port file instead is strongly recommended. + +If you do this, the port files or Dawn's GN or CMake files can serve as a +reference for the steps needed. Note that in all cases, the sources include both +C++ and JS code. While it is possible to precompile the C++ code to `.a`, the JS +code cannot be precompiled and must be provided at the final link step. + ### Cross-targeting Web/Native #### Using CMake @@ -132,17 +144,37 @@ package zip). """ -TAG = 'v20250807.221415' +import sys + +if __name__ == '__main__': + print('Please see documentation inside this file for details on how to use this port.') + sys.exit(1) + +_VERSION = 'v20250926.144300' -EXTERNAL_PORT = f'https://github.com/google/dawn/releases/download/{TAG}/emdawnwebgpu_pkg-{TAG}.zip' -SHA512 = 'ab9f3af2536ef3a29c20bb9c69f45b5ee512b8e33fb559f8d0bf4529cd2c11e2fbfb919c3d936e3b32af0e92bd710af71a1700776b5e56c99297cfbc3b73ceec' +# Remote-specific port information + +# - Where to download the port +EXTERNAL_PORT = f'https://github.com/google/dawn/releases/download/{_VERSION}/emdawnwebgpu_pkg-{_VERSION}.zip' +# - Hash to verify the download integrity +SHA512 = 'a186cf7f33266c9dfeca7d99ffac769a91b2129e34054f1e857cd82e8b033896da34cf758088bbdeeb128aa713df9953851f83ba6d677c438a929d245a789948' +# - Path of the port inside the zip file PORT_FILE = 'emdawnwebgpu_pkg/emdawnwebgpu.port.py' -# Port information (required) +# General port information # - Visible in emcc --show-ports and emcc --use-port=emdawnwebgpu:help LICENSE = "Some files: BSD 3-Clause License. Other files: Emscripten's license (available under both MIT License and University of Illinois/NCSA Open Source License)" # - Visible in emcc --use-port=emdawnwebgpu:help DESCRIPTION = "Emdawnwebgpu implements webgpu.h on WebGPU, replacing -sUSE_WEBGPU. **For info on usage and filing feedback, see link below.**" -URL = 'https://dawn.googlesource.com/dawn/+/80062b708e44aa4d8c48e555ed0cc801396069f6/src/emdawnwebgpu/pkg/README.md' +URL = 'https://dawn.googlesource.com/dawn/+/faa7054b5b65c3ce3774151952a68aa7668aa20b/src/emdawnwebgpu/pkg/README.md' + + +# Emscripten <4.0.10 won't notice EXTERNAL_PORT and will try to use this. +def get(ports, settings, shared): + raise Exception('Remote ports require Emscripten 4.0.10+.') + + +def clear(ports, settings, shared): + pass From 4f93d5f3b991ac404925168b14199ddd242b6af0 Mon Sep 17 00:00:00 2001 From: Kai Ninomiya Date: Fri, 26 Sep 2025 23:01:23 -0700 Subject: [PATCH 4/5] Skip with FROZEN_CACHE --- test/test_other.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/test_other.py b/test/test_other.py index 11c4561a013fe..080eea008f9a6 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -9586,6 +9586,9 @@ def test_closure_full_js_library(self, args): @also_with_wasm64 def test_closure_webgpu(self): + if config.FROZEN_CACHE: + # TODO(crbug.com/446944885): Make Emdawnwebgpu work with FROZEN_CACHE if possible. + self.skipTest("test doesn't work with frozen cache") self.set_setting('NO_DEFAULT_TO_CXX', 0) # emdawnwebgpu uses C++ internally self.build('hello_world.c', cflags=[ '--closure=1', @@ -12238,6 +12241,9 @@ def test_standalone_syscalls(self): 'closure_assertions': (['--closure=1', '-Werror=closure', '-sASSERTIONS'],), }) def test_emdawnwebgpu_link_test(self, args): + if config.FROZEN_CACHE: + # TODO(crbug.com/446944885): Make Emdawnwebgpu work with FROZEN_CACHE if possible. + self.skipTest("test doesn't work with frozen cache") self.emcc(test_file('test_emdawnwebgpu_link_test.cpp'), ['--use-port=emdawnwebgpu', '-sASYNCIFY'] + args) def test_signature_mismatch(self): From 3d62df63118d91f599291d7b6bef16ace813a84c Mon Sep 17 00:00:00 2001 From: Kai Ninomiya Date: Mon, 29 Sep 2025 16:26:44 -0700 Subject: [PATCH 5/5] Remove unneeded NO_DEFAULT_TO_CXX --- test/test_other.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test_other.py b/test/test_other.py index 080eea008f9a6..6e675dfa1c65a 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -2586,7 +2586,6 @@ def test_contrib_ports(self): @requires_network def test_remote_ports(self): - self.set_setting('NO_DEFAULT_TO_CXX', 0) # emdawnwebgpu uses C++ internally self.emcc(test_file('hello_world.c'), ['--use-port=emdawnwebgpu']) @crossplatform