From fae23953ca4d20e92e13a00068be6e19dd6ceb2c Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 13 Feb 2026 17:56:14 +0100 Subject: [PATCH] :wrench: --- apps/example/ios/Podfile.lock | 130 +++++++++--------- .../webgpu/cpp/rnwgpu/RNWebGPUManager.cpp | 5 +- packages/webgpu/cpp/rnwgpu/api/GPUAdapter.cpp | 20 ++- packages/webgpu/cpp/rnwgpu/api/GPUDevice.cpp | 58 ++++++++ packages/webgpu/cpp/rnwgpu/api/GPUDevice.h | 85 ++++++++++++ .../cpp/rnwgpu/api/GPUUncapturedErrorEvent.h | 71 ++++++++++ packages/webgpu/package.json | 2 +- packages/webgpu/src/__tests__/Device.spec.ts | 89 ++++++++++++ 8 files changed, 389 insertions(+), 71 deletions(-) create mode 100644 packages/webgpu/cpp/rnwgpu/api/GPUUncapturedErrorEvent.h diff --git a/apps/example/ios/Podfile.lock b/apps/example/ios/Podfile.lock index 1931bcb69..10e49aa98 100644 --- a/apps/example/ios/Podfile.lock +++ b/apps/example/ios/Podfile.lock @@ -1865,7 +1865,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - react-native-wgpu (0.5.2): + - react-native-wgpu (0.5.3): - boost - DoubleConversion - fast_float @@ -2903,81 +2903,81 @@ SPEC CHECKSUMS: fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: 35c763d57c9832d0eef764316ca1c4d043581394 - RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f + RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 RCTDeprecation: c0ed3249a97243002615517dff789bf4666cf585 RCTRequired: 58719f5124f9267b5f9649c08bf23d9aea845b23 RCTTypeSafety: 4aefa8328ab1f86da273f08517f1f6b343f6c2cc React: 2073376f47c71b7e9a0af7535986a77522ce1049 React-callinvoker: 751b6f2c83347a0486391c3f266f291f0f53b27e - React-Core: 7195661f0b48e7ea46c3360ccb575288a20c932c - React-CoreModules: 14f0054ab46000dd3b816d6528af3bd600d82073 - React-cxxreact: 7f602425c63096c398dac13cd7a300efd7c281ae + React-Core: dff5d29973349b11dd6631c9498456d75f846d5e + React-CoreModules: c0ae04452e4c5d30e06f8e94692a49107657f537 + React-cxxreact: 376fd672c95dfb64ad5cc246e6a1e9edb78dec4c React-debug: 7b56a0a7da432353287d2eedac727903e35278f5 - React-defaultsnativemodule: 695d8a0b40f735edb3c4031e0f049e567fdac47a - React-domnativemodule: 6d66c1f61f277d008d98cae650ce2c025b89d3b9 - React-Fabric: 997d4115d688f483cb409a1290171bff3c93dab4 - React-FabricComponents: 8167e5e363ca3a3fe394d8afee355e4072bea1db - React-FabricImage: f8f9f2c97657116702acc670e3f4357bc842bed3 - React-featureflags: dfb4d0d527d55dd968231370f6832b9197ee653d - React-featureflagsnativemodule: c63cfd8fe95cd98f12ebb37daa919c4544810a45 - React-graphics: fd795f1c2a1133a08dde31725b20949edd545dca - React-hermes: 0a167bbb02c242664745e82154578c64e90a88e5 - React-idlecallbacksnativemodule: 1798c6aa33ddc7c2e9fa3c3d67729728639889e9 - React-ImageManager: c498ee6945dffacc82bfa175aa3264212f27c70b - React-jserrorhandler: 216951fea62fc26c600f4c96f0dc4fd53d1e7a9b - React-jsi: 9c27d27d3007b73c702ad3fd5a6166557c741020 - React-jsiexecutor: 2b24f4ed4026344a27f717bf947a434cbbeeff7a - React-jsinspector: 02394b059c48805780f7d977366317a24168d00e - React-jsinspectorcdp: f4b6d5c5c9db05ef44d082716714f90cfeed96bb - React-jsinspectornetwork: e7c77d01b5f0664e24c0bec1aea27d5e3d7fb746 - React-jsinspectortracing: aaa96a4e53abb88dc6d47da3b5744c710652fef9 - React-jsitooling: 226e5f4147c7b6f1ae1954a8406ffa713f3da828 - React-jsitracing: 8a2fbeaa9c53c3f0b23904ccffefc890eae48d71 - React-logger: 1767babce2d28c3251039ce05556714a2c8c6ded - React-Mapbuffer: 33f678ee25b6c0ee2b01b1ecec08e3e02424cefe - React-microtasksnativemodule: 44b44a4d3cd6ffb85d928abf741acdc26722de2e - react-native-safe-area-context: 54d812805f3c4e08a4580ad086cbde1d8780c2e4 - react-native-skia: 4df548eb44d05ce5e35679b15a9d765e5724126e - react-native-wgpu: b4dc1b3af4fb7e6169f504ad28a60508d530d06a - React-NativeModulesApple: b5d18bc109c45c9a1c6b71664991b5cc3adc4e48 + React-defaultsnativemodule: 393b81aaa6211408f50a6ef00a277847256dd881 + React-domnativemodule: 5fb5829baa7a7a0f217019cbad1eb226d94f7062 + React-Fabric: a17c4ae35503673b57b91c2d1388429e7cbee452 + React-FabricComponents: a76572ddeba78ebe4ec58615291e9db4a55cd46a + React-FabricImage: d806eb2695d7ef355ec28d1a21f5a14ac26b1cae + React-featureflags: 1690ec3c453920b6308e23a4e24eb9c3632f9c75 + React-featureflagsnativemodule: 7b7e8483fc671c5a33aefd699b7c7a3c0bdfdfec + React-graphics: ea146ee799dc816524a3a0922fc7be0b5a52dcc1 + React-hermes: fcbdc45ecf38259fe3b12642bd0757c52270a107 + React-idlecallbacksnativemodule: a353f9162eaa7ad787e68aba9f52a1cfa8154098 + React-ImageManager: ec5cf55ce9cc81719eb5f1f51d23d04db851c86c + React-jserrorhandler: 594c593f3d60f527be081e2cace7710c2bd9f524 + React-jsi: 59ec3190dd364cca86a58869e7755477d2468948 + React-jsiexecutor: b87d78a2e8dd7a6f56e9cdac038da45de98c944f + React-jsinspector: b9204adf1af622c98e78af96ec1bca615c2ce2bd + React-jsinspectorcdp: 4a356fa69e412d35d3a38c44d4a6cc555c5931e8 + React-jsinspectornetwork: 7820056773178f321cbf18689e1ffcd38276a878 + React-jsinspectortracing: b341c5ef6e031a33e0bd462d67fd397e8e9cd612 + React-jsitooling: 401655e05cb966b0081225c5201d90734a567cb9 + React-jsitracing: 67eff6dea0cb58a1e7bd8b49243012d88c0f511e + React-logger: a3cb5b29c32b8e447b5a96919340e89334062b48 + React-Mapbuffer: 9d2434a42701d6144ca18f0ca1c4507808ca7696 + React-microtasksnativemodule: 75b6604b667d297292345302cc5bfb6b6aeccc1b + react-native-safe-area-context: c00143b4823773bba23f2f19f85663ae89ceb460 + react-native-skia: 5bf2b2107cd7f2d806fd364f5e16b1c7554ed3cd + react-native-wgpu: 27d4c1aaa89ba015e8c02d5dbf8abeaa83c4d523 + React-NativeModulesApple: 879fbdc5dcff7136abceb7880fe8a2022a1bd7c3 React-oscompat: 93b5535ea7f7dff46aaee4f78309a70979bdde9d - React-perflogger: a03d913e3205b00aee4128082abe42fd45ce0c98 - React-performancetimeline: 9b5986cc15afafb9bf246d7dd55bdd138df94451 + React-perflogger: 5536d2df3d18fe0920263466f7b46a56351c0510 + React-performancetimeline: 9041c53efa07f537164dcfe7670a36642352f4c2 React-RCTActionSheet: 42195ae666e6d79b4af2346770f765b7c29435b9 - React-RCTAnimation: 5c10527683128c56ff2c09297fb080f7c35bd293 - React-RCTAppDelegate: c616bd5b0d12f0b21dfacee9cd2d512c6df013aa - React-RCTBlob: 6e3757bdd7dce6fd9788c0dd675fd6b6c432db9d - React-RCTFabric: e8f3b9da97477710bf0904a62eb5b5209c964694 - React-RCTFBReactNativeSpec: c042f8d60d44ad9e2c722da89323c0bdab7a37af - React-RCTImage: a3482fe1ae562d1bab08b42d4670a7c9a21813cd - React-RCTLinking: d82b9adb141aef9d2b38d446b837ae7017ab60aa - React-RCTNetwork: fa9350dd99354c5695964f589bd4790bdd4f6a85 - React-RCTRuntime: be99a38cd23388c08921d8969c82a1997a11ec90 - React-RCTSettings: b7f4a03f44dba1d3a4dc6770843547b203ca9129 - React-RCTText: 91dc597a5f6b27fd1048bb287c41ea05eeca9333 - React-RCTVibration: 27b09ddf74bddfa30a58d20e48f885ea6ed6c9d9 + React-RCTAnimation: fa103ccc3503b1ed8dedca7e62e7823937748843 + React-RCTAppDelegate: 665d4baf19424cef08276e9ac0d8771eec4519f9 + React-RCTBlob: 0fa9530c255644db095f2c4fd8d89738d9d9ecc0 + React-RCTFabric: 1fcd8af6e25f92532f56b4ba092e58662c14d156 + React-RCTFBReactNativeSpec: db171247585774f9f0a30f75109cc51568686213 + React-RCTImage: ba824e61ce2e920a239a65d130b83c3a1d426dff + React-RCTLinking: d2dc199c37e71e6f505d9eca3e5c33be930014d4 + React-RCTNetwork: 87137d4b9bd77e5068f854dd5c1f30d4b072faf6 + React-RCTRuntime: 137fafaa808a8b7e76a510e8be45f9f827899daa + React-RCTSettings: 71f5c7fd7b5f4e725a4e2114a4b4373d0e46048f + React-RCTText: b94d4699b49285bee22b8ebf768924d607eccee3 + React-RCTVibration: 6e3993c4f6c36a3899059f9a9ead560ddaf5a7d7 React-rendererconsistency: b4785e5ed837dc7c242bbc5fdd464b33ef5bfae7 - React-renderercss: cef3f26df2ddec558ce3c0790fc574b4fb62ce67 - React-rendererdebug: e68433ae67738caeb672a6c8cc993e9276b298a9 - React-RuntimeApple: dc1d4709bf847bc695dbe6e8aaf3e22ef25aef02 - React-RuntimeCore: ca3473c8b6578693fa3bad4d44240098d49d6723 - React-runtimeexecutor: 0db3ca0b09cd72489cef3a3729349b3c2cf13320 - React-RuntimeHermes: f92cabaf97ef2546a74360eddfc1c74a34cb9ff8 - React-runtimescheduler: 06aea75069e0d556a75d258bfc89eb0ebd5d557e - React-timing: 1a90df9a04d8e7fd165ff7fa0918b9595c776373 - React-utils: 92115441fb55ce01ded4abfb5e9336a74cd93e9c - ReactAppDependencyProvider: b20fba6c3d091a393925890009999472c8f94d95 - ReactCodegen: cf03d376a26d393f818d511240b026fc8c95313c - ReactCommon: 00df7b9f859c9d02181844255bb89a8bca544374 - ReactNativeHost: b63ce830e7c5b4e3adcf556b2ef41665da189da0 - ReactTestApp-DevSupport: c7bff1aee7663f2fb1eefcf60c41573f02916c41 + React-renderercss: e6fb0ba387b389c595ffa86b8b628716d31f58dc + React-rendererdebug: 60a03de5c7ea59bf2d39791eb43c4c0f5d8b24e3 + React-RuntimeApple: 3df6788cd9b938bb8cb28298d80b5fbd98a4d852 + React-RuntimeCore: fad8adb4172c414c00ff6980250caf35601a0f5d + React-runtimeexecutor: d2db7e72d97751855ea0bf5273d2ac84e5ea390c + React-RuntimeHermes: 04faa4cf9a285136a6d73738787fe36020170613 + React-runtimescheduler: f6a1c9555e7131b4a8b64cce01489ad0405f6e8d + React-timing: 1e6a8acb66e2b7ac9d418956617fd1fdb19322fd + React-utils: 52bbb03f130319ef82e4c3bc7a85eaacdb1fec87 + ReactAppDependencyProvider: 433ddfb4536948630aadd5bd925aff8a632d2fe3 + ReactCodegen: 64dbbed4e9e0264d799578ea78492479a66fba4a + ReactCommon: 394c6b92765cf6d211c2c3f7f6bc601dffb316a6 + ReactNativeHost: f5e054387e917216a2a021a3f7fdc4f9f158e7e4 + ReactTestApp-DevSupport: 9b7bbba5e8fed998e763809171d9906a1375f9d3 ReactTestApp-Resources: 1bd9ff10e4c24f2ad87101a32023721ae923bccf - RNGestureHandler: 92ad734ef0da16d69d0a325b7e1e9ae80bdce1f9 - RNReanimated: 52dad96755e908e80b403c5ccdd7e24451f0c42a - RNWorklets: 95eb4f990dd495be6bf35a2567b0351a6d7f73cf + RNGestureHandler: e37bdb684df1ac17c7e1d8f71a3311b2793c186b + RNReanimated: 464375ff2caa801358547c44eca894ff0bf68e74 + RNWorklets: ee58e869ea579800ec5f2f1cb6ae195fd3537546 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - Yoga: 922d794dce2af9c437f864bf4093abfa7a131adb + Yoga: a3ed390a19db0459bd6839823a6ac6d9c6db198d PODFILE CHECKSUM: b4c1d70c599aba416a49b6bad5eea5084b4e43d0 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/packages/webgpu/cpp/rnwgpu/RNWebGPUManager.cpp b/packages/webgpu/cpp/rnwgpu/RNWebGPUManager.cpp index bc6280be8..5a2decc09 100644 --- a/packages/webgpu/cpp/rnwgpu/RNWebGPUManager.cpp +++ b/packages/webgpu/cpp/rnwgpu/RNWebGPUManager.cpp @@ -35,6 +35,7 @@ #include "GPUSupportedLimits.h" #include "GPUTexture.h" #include "GPUTextureView.h" +#include "GPUUncapturedErrorEvent.h" #include "GPUValidationError.h" // Enums @@ -60,7 +61,8 @@ RNWebGPUManager::RNWebGPUManager( BaseRuntimeAwareCache::setMainJsRuntime(_jsRuntime); auto gpu = std::make_shared(*_jsRuntime); - auto rnWebGPU = std::make_shared(gpu, _platformContext, _jsCallInvoker); + auto rnWebGPU = + std::make_shared(gpu, _platformContext, _jsCallInvoker); _gpu = gpu->get(); _jsRuntime->global().setProperty(*_jsRuntime, "RNWebGPU", RNWebGPU::create(*_jsRuntime, rnWebGPU)); @@ -86,6 +88,7 @@ RNWebGPUManager::RNWebGPUManager( GPUInternalError::installConstructor(*_jsRuntime); GPUOutOfMemoryError::installConstructor(*_jsRuntime); GPUValidationError::installConstructor(*_jsRuntime); + GPUUncapturedErrorEvent::installConstructor(*_jsRuntime); GPUPipelineLayout::installConstructor(*_jsRuntime); GPUQuerySet::installConstructor(*_jsRuntime); GPUQueue::installConstructor(*_jsRuntime); diff --git a/packages/webgpu/cpp/rnwgpu/api/GPUAdapter.cpp b/packages/webgpu/cpp/rnwgpu/api/GPUAdapter.cpp index 85e8d3908..27bf14f8b 100644 --- a/packages/webgpu/cpp/rnwgpu/api/GPUAdapter.cpp +++ b/packages/webgpu/cpp/rnwgpu/api/GPUAdapter.cpp @@ -50,6 +50,8 @@ async::AsyncTaskHandle GPUAdapter::requestDevice( }); // Set uncaptured error callback using new template API + // Note: This callback cannot capture variables, so we use a static registry + // to look up the GPUDevice from the wgpu::Device handle. aDescriptor.SetUncapturedErrorCallback([](const wgpu::Device &device, wgpu::ErrorType type, wgpu::StringView message) { @@ -70,11 +72,16 @@ async::AsyncTaskHandle GPUAdapter::requestDevice( default: errorType = "Unknown"; } + std::string msg = + message.length > 0 ? std::string(message.data, message.length) : ""; std::string fullMessage = - message.length > 0 ? std::string(errorType) + ": " + - std::string(message.data, message.length) - : "no message"; - fprintf(stderr, "%s", fullMessage.c_str()); + msg.length() > 0 ? std::string(errorType) + ": " + msg : "no message"; + fprintf(stderr, "%s\n", fullMessage.c_str()); + + // Look up the GPUDevice from the registry and notify it + if (auto gpuDevice = GPUDevice::lookupDevice(device.Get())) { + gpuDevice->notifyUncapturedError(type, std::move(msg)); + } }); std::string label = descriptor.has_value() ? descriptor.value()->label.value_or("") : ""; @@ -138,6 +145,11 @@ async::AsyncTaskHandle GPUAdapter::requestDevice( auto deviceHost = std::make_shared(std::move(device), asyncRunner, label); *deviceLostBinding = deviceHost; + + // Register the device in the static registry so the uncaptured + // error callback can find it + GPUDevice::registerDevice(deviceHost->get().Get(), deviceHost); + resolve([deviceHost = std::move(deviceHost)]( jsi::Runtime &runtime) mutable { return JSIConverter>::toJSI( diff --git a/packages/webgpu/cpp/rnwgpu/api/GPUDevice.cpp b/packages/webgpu/cpp/rnwgpu/api/GPUDevice.cpp index c467b750d..909c4555a 100644 --- a/packages/webgpu/cpp/rnwgpu/api/GPUDevice.cpp +++ b/packages/webgpu/cpp/rnwgpu/api/GPUDevice.cpp @@ -431,4 +431,62 @@ async::AsyncTaskHandle GPUDevice::getLost() { _lostHandle = handle; return handle; } +void GPUDevice::addEventListener(std::string type, jsi::Function callback) { + auto funcPtr = std::make_shared(std::move(callback)); + _eventListeners[type].push_back(funcPtr); +} + +void GPUDevice::removeEventListener(std::string type, jsi::Function callback) { + // Note: Since jsi::Function doesn't support equality comparison, + // we cannot reliably remove a specific listener. This is a no-op. + // Most use cases (like BabylonJS) only need addEventListener to work. + (void)type; + (void)callback; +} + +void GPUDevice::notifyUncapturedError(wgpu::ErrorType type, + std::string message) { + auto it = _eventListeners.find("uncapturederror"); + if (it == _eventListeners.end() || it->second.empty()) { + return; + } + + auto runtime = getCreationRuntime(); + if (runtime == nullptr) { + return; + } + + // Create the appropriate error object based on type + GPUErrorVariant error; + switch (type) { + case wgpu::ErrorType::Validation: + error = std::make_shared(message); + break; + case wgpu::ErrorType::OutOfMemory: + error = std::make_shared(message); + break; + case wgpu::ErrorType::Internal: + case wgpu::ErrorType::Unknown: + default: + error = std::make_shared(message); + break; + } + + // Create the event object + auto event = std::make_shared(std::move(error)); + auto eventValue = + JSIConverter>::toJSI(*runtime, + event); + + // Call all registered listeners + for (const auto &listener : it->second) { + try { + listener->call(*runtime, eventValue); + } catch (const std::exception &e) { + // Log but don't throw - we don't want one listener to break others + fprintf(stderr, "Error in uncapturederror listener: %s\n", e.what()); + } + } +} + } // namespace rnwgpu diff --git a/packages/webgpu/cpp/rnwgpu/api/GPUDevice.h b/packages/webgpu/cpp/rnwgpu/api/GPUDevice.h index 8ab921e16..9b0681c2f 100644 --- a/packages/webgpu/cpp/rnwgpu/api/GPUDevice.h +++ b/packages/webgpu/cpp/rnwgpu/api/GPUDevice.h @@ -1,11 +1,15 @@ #pragma once +#include #include +#include #include #include +#include #include #include #include +#include #include "Unions.h" @@ -46,6 +50,7 @@ #include "GPUSupportedLimits.h" #include "GPUTexture.h" #include "GPUTextureDescriptor.h" +#include "GPUUncapturedErrorEvent.h" namespace rnwgpu { @@ -61,6 +66,44 @@ class GPUDevice : public NativeObject { : NativeObject(CLASS_NAME), _instance(instance), _async(async), _label(label) {} + ~GPUDevice() override { + // Unregister from the static registry + unregisterDevice(_instance.Get()); + } + + // Static registry for looking up GPUDevice from wgpu::Device in callbacks + static void registerDevice(WGPUDevice handle, + std::weak_ptr device) { + std::lock_guard lock(getRegistryMutex()); + getRegistry()[handle] = device; + } + + static void unregisterDevice(WGPUDevice handle) { + std::lock_guard lock(getRegistryMutex()); + getRegistry().erase(handle); + } + + static std::shared_ptr lookupDevice(WGPUDevice handle) { + std::lock_guard lock(getRegistryMutex()); + auto it = getRegistry().find(handle); + if (it != getRegistry().end()) { + return it->second.lock(); + } + return nullptr; + } + +private: + static std::unordered_map> & + getRegistry() { + static std::unordered_map> registry; + return registry; + } + + static std::mutex &getRegistryMutex() { + static std::mutex mutex; + return mutex; + } + public: std::string getBrand() { return CLASS_NAME; } @@ -103,8 +146,13 @@ class GPUDevice : public NativeObject { std::shared_ptr getQueue(); async::AsyncTaskHandle getLost(); void notifyDeviceLost(wgpu::DeviceLostReason reason, std::string message); + void notifyUncapturedError(wgpu::ErrorType type, std::string message); void forceLossForTesting(); + // EventTarget methods + void addEventListener(std::string type, jsi::Function callback); + void removeEventListener(std::string type, jsi::Function callback); + std::string getLabel() { return _label; } void setLabel(const std::string &label) { _label = label; @@ -155,6 +203,38 @@ class GPUDevice : public NativeObject { &GPUDevice::setLabel); installMethod(runtime, prototype, "forceLossForTesting", &GPUDevice::forceLossForTesting); + + // EventTarget methods - installed manually since they take jsi::Function + auto addEventListenerFunc = jsi::Function::createFromHostFunction( + runtime, jsi::PropNameID::forUtf8(runtime, "addEventListener"), 2, + [](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, + size_t count) -> jsi::Value { + if (count < 2 || !args[0].isString() || !args[1].isObject() || + !args[1].getObject(rt).isFunction(rt)) { + return jsi::Value::undefined(); + } + auto native = GPUDevice::fromValue(rt, thisVal); + native->addEventListener(args[0].getString(rt).utf8(rt), + args[1].getObject(rt).getFunction(rt)); + return jsi::Value::undefined(); + }); + prototype.setProperty(runtime, "addEventListener", addEventListenerFunc); + + auto removeEventListenerFunc = jsi::Function::createFromHostFunction( + runtime, jsi::PropNameID::forUtf8(runtime, "removeEventListener"), 2, + [](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, + size_t count) -> jsi::Value { + if (count < 2 || !args[0].isString() || !args[1].isObject() || + !args[1].getObject(rt).isFunction(rt)) { + return jsi::Value::undefined(); + } + auto native = GPUDevice::fromValue(rt, thisVal); + native->removeEventListener(args[0].getString(rt).utf8(rt), + args[1].getObject(rt).getFunction(rt)); + return jsi::Value::undefined(); + }); + prototype.setProperty(runtime, "removeEventListener", + removeEventListenerFunc); } inline const wgpu::Device get() { return _instance; } @@ -169,6 +249,11 @@ class GPUDevice : public NativeObject { std::shared_ptr _lostInfo; bool _lostSettled = false; std::optional _lostResolve; + + // Event listeners storage - keyed by event type + // Each entry contains a vector of shared_ptr to functions + std::unordered_map>> + _eventListeners; }; } // namespace rnwgpu diff --git a/packages/webgpu/cpp/rnwgpu/api/GPUUncapturedErrorEvent.h b/packages/webgpu/cpp/rnwgpu/api/GPUUncapturedErrorEvent.h new file mode 100644 index 000000000..e780ca085 --- /dev/null +++ b/packages/webgpu/cpp/rnwgpu/api/GPUUncapturedErrorEvent.h @@ -0,0 +1,71 @@ +#pragma once + +#include +#include +#include + +#include "JSIConverter.h" +#include "NativeObject.h" + +#include "GPUInternalError.h" +#include "GPUOutOfMemoryError.h" +#include "GPUValidationError.h" + +namespace rnwgpu { + +namespace jsi = facebook::jsi; + +using GPUErrorVariant = std::variant, + std::shared_ptr, + std::shared_ptr>; + +class GPUUncapturedErrorEvent : public NativeObject { +public: + static constexpr const char *CLASS_NAME = "GPUUncapturedErrorEvent"; + + explicit GPUUncapturedErrorEvent(GPUErrorVariant error) + : NativeObject(CLASS_NAME), _error(std::move(error)) {} + +public: + std::string getBrand() { return CLASS_NAME; } + std::string getType() { return "uncapturederror"; } + + static void definePrototype(jsi::Runtime &runtime, jsi::Object &prototype) { + installGetter(runtime, prototype, "__brand", + &GPUUncapturedErrorEvent::getBrand); + installGetter(runtime, prototype, "type", + &GPUUncapturedErrorEvent::getType); + + // Custom getter for error that handles the variant conversion + auto errorGetter = jsi::Function::createFromHostFunction( + runtime, jsi::PropNameID::forUtf8(runtime, "get_error"), 0, + [](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, + size_t count) -> jsi::Value { + auto native = GPUUncapturedErrorEvent::fromValue(rt, thisVal); + return std::visit( + [&rt](auto &&err) -> jsi::Value { + using T = std::decay_t; + return JSIConverter::toJSI(rt, err); + }, + native->_error); + }); + + auto objectCtor = runtime.global().getPropertyAsObject(runtime, "Object"); + auto defineProperty = + objectCtor.getPropertyAsFunction(runtime, "defineProperty"); + + jsi::Object descriptor(runtime); + descriptor.setProperty(runtime, "get", errorGetter); + descriptor.setProperty(runtime, "enumerable", true); + descriptor.setProperty(runtime, "configurable", true); + + defineProperty.call(runtime, prototype, + jsi::String::createFromUtf8(runtime, "error"), + descriptor); + } + +private: + GPUErrorVariant _error; +}; + +} // namespace rnwgpu diff --git a/packages/webgpu/package.json b/packages/webgpu/package.json index 44656386c..d174b8f56 100644 --- a/packages/webgpu/package.json +++ b/packages/webgpu/package.json @@ -1,6 +1,6 @@ { "name": "react-native-wgpu", - "version": "0.5.3", + "version": "0.5.4", "description": "React Native WebGPU", "main": "lib/commonjs/index", "module": "lib/module/index", diff --git a/packages/webgpu/src/__tests__/Device.spec.ts b/packages/webgpu/src/__tests__/Device.spec.ts index b5d85de74..ea6dd2758 100644 --- a/packages/webgpu/src/__tests__/Device.spec.ts +++ b/packages/webgpu/src/__tests__/Device.spec.ts @@ -81,4 +81,93 @@ describe("Device", () => { expect(["unknown", "destroyed"].includes(result.reason)).toBeTruthy(); }); + + it("should have addEventListener method", async () => { + const result = await client.eval(({ device }) => { + return typeof device.addEventListener === "function"; + }); + expect(result).toBe(true); + }); + + it("should have removeEventListener method", async () => { + const result = await client.eval(({ device }) => { + return typeof device.removeEventListener === "function"; + }); + expect(result).toBe(true); + }); + + it("should call addEventListener without throwing", async () => { + const result = await client.eval(({ device }) => { + try { + device.addEventListener("uncapturederror", () => {}); + return true; + } catch { + return false; + } + }); + expect(result).toBe(true); + }); + + it("should call removeEventListener without throwing", async () => { + const result = await client.eval(({ device }) => { + try { + const listener = () => {}; + device.addEventListener("uncapturederror", listener); + device.removeEventListener("uncapturederror", listener); + return true; + } catch { + return false; + } + }); + expect(result).toBe(true); + }); + + it("should receive uncapturederror event when validation error occurs", async () => { + const result = await client.eval(({ gpu }) => + gpu.requestAdapter().then((adapter) => + adapter!.requestDevice().then((device) => { + return new Promise<{ + received: boolean; + eventType: string; + hasError: boolean; + errorMessage: string; + }>((resolve) => { + // Set a timeout in case the event never fires + const timeout = setTimeout(() => { + resolve({ + received: false, + eventType: "", + hasError: false, + errorMessage: "", + }); + }, 1000); + + device.addEventListener( + "uncapturederror", + (event: GPUUncapturedErrorEvent) => { + clearTimeout(timeout); + resolve({ + received: true, + eventType: event.type, + hasError: event.error !== null && event.error !== undefined, + errorMessage: event.error?.message ?? "", + }); + }, + ); + + // Create an uncaptured validation error by using invalid parameters + // Without pushErrorScope, this error becomes uncaptured + device.createSampler({ + maxAnisotropy: 0, // Invalid: must be at least 1 + }); + }); + }), + ), + ); + + expect(result.received).toBe(true); + expect(result.eventType).toBe("uncapturederror"); + expect(result.hasError).toBe(true); + expect(result.errorMessage.length).toBeGreaterThan(0); + }); });