diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b1190c904..d69de7f0c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -247,6 +247,8 @@ By @teoxoy in [#4185](https://github.com/gfx-rs/wgpu/pull/4185) - Calls to lost devices now return `DeviceError::Lost` instead of `DeviceError::Invalid`. By @bradwerth in [#4238]([https://github.com/gfx-rs/wgpu/pull/4238]) - Let the `"strict_asserts"` feature enable check that wgpu-core's lock-ordering tokens are unique per thread. By @jimblandy in [#4258]([https://github.com/gfx-rs/wgpu/pull/4258]) - Allow filtering labels out before they are passed to GPU drivers by @nical in [https://github.com/gfx-rs/wgpu/pull/4246](4246) +- `DeviceLostClosure` callback mechanism provided so user agents can resolve `GPUDevice.lost` Promises at the appropriate time by @bradwerth in [#4645](https://github.com/gfx-rs/wgpu/pull/4645) + #### Vulkan diff --git a/tests/tests/device.rs b/tests/tests/device.rs index 108c7cc26a..2288fd0cb6 100644 --- a/tests/tests/device.rs +++ b/tests/tests/device.rs @@ -448,3 +448,35 @@ static DEVICE_DESTROY_THEN_MORE: GpuTestConfiguration = GpuTestConfiguration::ne buffer_for_unmap.unmap(); }); }); + +#[gpu_test] +static DEVICE_DESTROY_THEN_LOST: GpuTestConfiguration = GpuTestConfiguration::new() + .parameters(TestParameters::default()) + .run_sync(|ctx| { + // This test checks that when device.destroy is called, the provided + // DeviceLostClosure is called with reason DeviceLostReason::Destroyed. + let was_called = std::sync::Arc::::new(false.into()); + + // Set a LoseDeviceCallback on the device. + let was_called_clone = was_called.clone(); + let callback = Box::new(move |reason, _m| { + was_called_clone.store(true, std::sync::atomic::Ordering::SeqCst); + assert!( + matches!(reason, wgt::DeviceLostReason::Destroyed), + "Device lost info reason should match DeviceLostReason::Destroyed." + ); + }); + ctx.device.set_device_lost_callback(callback); + + // Destroy the device. + ctx.device.destroy(); + + // Make sure the device queues are empty, which ensures that the closure + // has been called. + assert!(ctx.device.poll(wgpu::Maintain::Wait)); + + assert!( + was_called.load(std::sync::atomic::Ordering::SeqCst), + "Device lost callback should have been called." + ); + }); diff --git a/wgpu-core/src/device/global.rs b/wgpu-core/src/device/global.rs index 9c2e51cb1a..c05e25f90d 100644 --- a/wgpu-core/src/device/global.rs +++ b/wgpu-core/src/device/global.rs @@ -3,7 +3,9 @@ use crate::device::trace; use crate::{ binding_model::{self, BindGroupLayout}, command, conv, - device::{life::WaitIdleError, map_buffer, queue, Device, DeviceError, HostMap}, + device::{ + life::WaitIdleError, map_buffer, queue, Device, DeviceError, DeviceLostClosure, HostMap, + }, global::Global, hal_api::HalApi, hub::Token, @@ -2672,6 +2674,21 @@ impl Global { } } + pub fn device_set_device_lost_closure( + &self, + device_id: DeviceId, + device_lost_closure: DeviceLostClosure, + ) { + let hub = A::hub(self); + let mut token = Token::root(); + + let (mut device_guard, mut token) = hub.devices.write(&mut token); + if let Ok(device) = device_guard.get_mut(device_id) { + let mut life_tracker = device.lock_life(&mut token); + life_tracker.device_lost_closure = Some(device_lost_closure); + } + } + pub fn device_destroy(&self, device_id: DeviceId) { log::trace!("Device::destroy {device_id:?}"); @@ -2683,36 +2700,26 @@ impl Global { // Follow the steps at // https://gpuweb.github.io/gpuweb/#dom-gpudevice-destroy. - // It's legal to call destroy multiple times, but if the device - // is already invalid, there's nothing more to do. There's also - // no need to return an error. - if !device.valid { - return; - } - // The last part of destroy is to lose the device. The spec says // delay that until all "currently-enqueued operations on any - // queue on this device are completed." - - // TODO: implement this delay. - - // Finish by losing the device. - - // TODO: associate this "destroyed" reason more tightly with - // the GPUDeviceLostReason defined in webgpu.idl. - device.lose(Some("destroyed")); + // queue on this device are completed." This is accomplished by + // setting valid to false, and then relying upon maintain to + // check for empty queues and a DeviceLostClosure. At that time, + // the DeviceLostClosure will be called with "destroyed" as the + // reason. + device.valid = false; } } - pub fn device_lose(&self, device_id: DeviceId, reason: Option<&str>) { - log::trace!("Device::lose {device_id:?}"); + pub fn device_mark_lost(&self, device_id: DeviceId, message: &str) { + log::trace!("Device::mark_lost {device_id:?}"); let hub = A::hub(self); let mut token = Token::root(); - let (mut device_guard, _) = hub.devices.write(&mut token); + let (mut device_guard, mut token) = hub.devices.write(&mut token); if let Ok(device) = device_guard.get_mut(device_id) { - device.lose(reason); + device.lose(&mut token, message); } } diff --git a/wgpu-core/src/device/life.rs b/wgpu-core/src/device/life.rs index e1d01ccaba..cb8dc43b7e 100644 --- a/wgpu-core/src/device/life.rs +++ b/wgpu-core/src/device/life.rs @@ -3,7 +3,7 @@ use crate::device::trace; use crate::{ device::{ queue::{EncoderInFlight, SubmittedWorkDoneClosure, TempResource}, - DeviceError, + DeviceError, DeviceLostClosure, }, hal_api::HalApi, hub::{Hub, Token}, @@ -313,6 +313,11 @@ pub(super) struct LifetimeTracker { /// must happen _after_ all mapped buffer callbacks are mapped, so we defer them /// here until the next time the device is maintained. work_done_closures: SmallVec<[SubmittedWorkDoneClosure; 1]>, + + /// Closure to be called on "lose the device". This is invoked directly by + /// device.lose or by the UserCallbacks returned from maintain when the device + /// has been destroyed and its queues are empty. + pub device_lost_closure: Option, } impl LifetimeTracker { @@ -326,6 +331,7 @@ impl LifetimeTracker { free_resources: NonReferencedResources::new(), ready_to_map: Vec::new(), work_done_closures: SmallVec::new(), + device_lost_closure: None, } } diff --git a/wgpu-core/src/device/mod.rs b/wgpu-core/src/device/mod.rs index eca1375604..1d89d54796 100644 --- a/wgpu-core/src/device/mod.rs +++ b/wgpu-core/src/device/mod.rs @@ -12,8 +12,9 @@ use crate::{ use arrayvec::ArrayVec; use hal::Device as _; use smallvec::SmallVec; +use std::os::raw::c_char; use thiserror::Error; -use wgt::{BufferAddress, TextureFormat}; +use wgt::{BufferAddress, DeviceLostReason, TextureFormat}; use std::{iter, num::NonZeroU32, ptr}; @@ -169,12 +170,15 @@ pub type BufferMapPendingClosure = (BufferMapOperation, BufferAccessResult); pub struct UserClosures { pub mappings: Vec, pub submissions: SmallVec<[queue::SubmittedWorkDoneClosure; 1]>, + pub device_lost_invocations: SmallVec<[DeviceLostInvocation; 1]>, } impl UserClosures { fn extend(&mut self, other: Self) { self.mappings.extend(other.mappings); self.submissions.extend(other.submissions); + self.device_lost_invocations + .extend(other.device_lost_invocations); } fn fire(self) { @@ -189,6 +193,98 @@ impl UserClosures { for closure in self.submissions { closure.call(); } + for invocation in self.device_lost_invocations { + invocation + .closure + .call(invocation.reason, invocation.message); + } + } +} + +#[cfg(any( + not(target_arch = "wasm32"), + all( + feature = "fragile-send-sync-non-atomic-wasm", + not(target_feature = "atomics") + ) +))] +pub type DeviceLostCallback = Box; +#[cfg(not(any( + not(target_arch = "wasm32"), + all( + feature = "fragile-send-sync-non-atomic-wasm", + not(target_feature = "atomics") + ) +)))] +pub type DeviceLostCallback = Box; + +#[repr(C)] +pub struct DeviceLostClosureC { + pub callback: unsafe extern "C" fn(user_data: *mut u8, reason: u8, message: *const c_char), + pub user_data: *mut u8, +} + +#[cfg(any( + not(target_arch = "wasm32"), + all( + feature = "fragile-send-sync-non-atomic-wasm", + not(target_feature = "atomics") + ) +))] +unsafe impl Send for DeviceLostClosureC {} + +pub struct DeviceLostClosure { + // We wrap this so creating the enum in the C variant can be unsafe, + // allowing our call function to be safe. + inner: DeviceLostClosureInner, +} + +pub struct DeviceLostInvocation { + closure: DeviceLostClosure, + reason: DeviceLostReason, + message: String, +} + +enum DeviceLostClosureInner { + Rust { callback: DeviceLostCallback }, + C { inner: DeviceLostClosureC }, +} + +impl DeviceLostClosure { + pub fn from_rust(callback: DeviceLostCallback) -> Self { + Self { + inner: DeviceLostClosureInner::Rust { callback }, + } + } + + /// # Safety + /// + /// - The callback pointer must be valid to call with the provided `user_data` + /// pointer. + /// + /// - Both pointers must point to `'static` data, as the callback may happen at + /// an unspecified time. + pub unsafe fn from_c(inner: DeviceLostClosureC) -> Self { + Self { + inner: DeviceLostClosureInner::C { inner }, + } + } + + #[allow(trivial_casts)] + pub(crate) fn call(self, reason: DeviceLostReason, message: String) { + match self.inner { + DeviceLostClosureInner::Rust { callback } => callback(reason, message), + // SAFETY: the contract of the call to from_c says that this unsafe is sound. + DeviceLostClosureInner::C { inner } => unsafe { + // We need to pass message as a c_char typed pointer. To avoid trivial + // conversion warnings on some platforms, we use the allow lint. + (inner.callback)( + inner.user_data, + reason as u8, + message.as_ptr() as *const c_char, + ) + }, + } } } diff --git a/wgpu-core/src/device/resource.rs b/wgpu-core/src/device/resource.rs index d61aa2c5a0..3d422a8e28 100644 --- a/wgpu-core/src/device/resource.rs +++ b/wgpu-core/src/device/resource.rs @@ -8,8 +8,8 @@ use crate::{ command, conv, device::life::WaitIdleError, device::{ - AttachmentData, CommandAllocator, MissingDownlevelFlags, MissingFeatures, - RenderPassContext, CLEANUP_WAIT_MS, + AttachmentData, CommandAllocator, DeviceLostInvocation, MissingDownlevelFlags, + MissingFeatures, RenderPassContext, CLEANUP_WAIT_MS, }, hal_api::HalApi, hal_label, @@ -34,7 +34,7 @@ use hal::{CommandEncoder as _, Device as _}; use parking_lot::{Mutex, MutexGuard}; use smallvec::SmallVec; use thiserror::Error; -use wgt::{TextureFormat, TextureSampleType, TextureViewDimension}; +use wgt::{DeviceLostReason, TextureFormat, TextureSampleType, TextureViewDimension}; use std::{borrow::Cow, iter, num::NonZeroU32}; @@ -315,9 +315,24 @@ impl Device { let mapping_closures = life_tracker.handle_mapping(hub, &self.raw, &self.trackers, token); life_tracker.cleanup(&self.raw); + // Detect if we have been destroyed and now need to lose the device. + // If we are invalid (set at start of destroy) and our queue is empty, + // and we have a DeviceLostClosure, return the closure to be called by + // our caller. This will complete the steps for both destroy and for + // "lose the device". + let mut device_lost_invocations = SmallVec::new(); + if !self.valid && life_tracker.queue_empty() && life_tracker.device_lost_closure.is_some() { + device_lost_invocations.push(DeviceLostInvocation { + closure: life_tracker.device_lost_closure.take().unwrap(), + reason: DeviceLostReason::Destroyed, + message: String::new(), + }); + } + let closures = UserClosures { mappings: mapping_closures, submissions: submission_closures, + device_lost_invocations, }; Ok((closures, life_tracker.queue_empty())) } @@ -3304,17 +3319,23 @@ impl Device { }) } - pub(crate) fn lose(&mut self, _reason: Option<&str>) { + pub(crate) fn lose<'this, 'token: 'this>( + &'this mut self, + token: &mut Token<'token, Self>, + message: &str, + ) { // Follow the steps at https://gpuweb.github.io/gpuweb/#lose-the-device. // Mark the device explicitly as invalid. This is checked in various // places to prevent new work from being submitted. self.valid = false; - // The following steps remain in "lose the device": // 1) Resolve the GPUDevice device.lost promise. - - // TODO: triggger this passively or actively, and supply the reason. + let mut life_tracker = self.lock_life(token); + if life_tracker.device_lost_closure.is_some() { + let device_lost_closure = life_tracker.device_lost_closure.take().unwrap(); + device_lost_closure.call(DeviceLostReason::Unknown, message.to_string()); + } // 2) Complete any outstanding mapAsync() steps. // 3) Complete any outstanding onSubmittedWorkDone() steps. diff --git a/wgpu-types/src/lib.rs b/wgpu-types/src/lib.rs index ce3ca05e6e..14f08ef2db 100644 --- a/wgpu-types/src/lib.rs +++ b/wgpu-types/src/lib.rs @@ -6707,3 +6707,15 @@ mod send_sync { )))] impl WasmNotSync for T {} } + +/// Reason for "lose the device". +/// +/// Corresponds to [WebGPU `GPUDeviceLostReason`](https://gpuweb.github.io/gpuweb/#enumdef-gpudevicelostreason). +#[repr(u8)] +#[derive(Debug, Copy, Clone)] +pub enum DeviceLostReason { + /// Triggered by driver + Unknown = 0, + /// After Device::destroy + Destroyed = 1, +} diff --git a/wgpu/src/backend/direct.rs b/wgpu/src/backend/direct.rs index 2804078068..42ab9cbcd3 100644 --- a/wgpu/src/backend/direct.rs +++ b/wgpu/src/backend/direct.rs @@ -22,6 +22,7 @@ use std::{ sync::Arc, }; use wgc::command::{bundle_ffi::*, compute_ffi::*, render_ffi::*}; +use wgc::device::DeviceLostClosure; use wgc::id::TypedId; use wgt::{WasmNotSend, WasmNotSync}; @@ -1455,14 +1456,30 @@ impl crate::Context for Context { wgc::gfx_select!(device => global.device_drop(*device)); } + fn device_set_device_lost_callback( + &self, + device: &Self::DeviceId, + _device_data: &Self::DeviceData, + device_lost_callback: crate::context::DeviceLostCallback, + ) { + let global = &self.0; + let device_lost_closure = DeviceLostClosure::from_rust(device_lost_callback); + wgc::gfx_select!(device => global.device_set_device_lost_closure(*device, device_lost_closure)); + } fn device_destroy(&self, device: &Self::DeviceId, _device_data: &Self::DeviceData) { let global = &self.0; wgc::gfx_select!(device => global.device_destroy(*device)); } - fn device_lose(&self, device: &Self::DeviceId, _device_data: &Self::DeviceData) { - // TODO: accept a reason, and pass it to device_lose. + fn device_mark_lost( + &self, + device: &Self::DeviceId, + _device_data: &Self::DeviceData, + message: &str, + ) { + // We do not provide a reason to device_lose, because all reasons other than + // destroyed (which this is not) are "unknown". let global = &self.0; - wgc::gfx_select!(device => global.device_lose(*device, None)); + wgc::gfx_select!(device => global.device_mark_lost(*device, message)); } fn device_poll( &self, diff --git a/wgpu/src/backend/web.rs b/wgpu/src/backend/web.rs index f07d6f620a..30d67d8643 100644 --- a/wgpu/src/backend/web.rs +++ b/wgpu/src/backend/web.rs @@ -1937,12 +1937,26 @@ impl crate::context::Context for Context { device_data.0.destroy(); } - fn device_lose(&self, _device: &Self::DeviceId, _device_data: &Self::DeviceData) { + fn device_mark_lost( + &self, + _device: &Self::DeviceId, + _device_data: &Self::DeviceData, + _message: &str, + ) { // TODO: figure out the GPUDevice implementation of this, including resolving // the device.lost promise, which will require a different invocation pattern // with a callback. } + fn device_set_device_lost_callback( + &self, + _device: &Self::DeviceId, + _device_data: &Self::DeviceData, + _device_lost_callback: crate::context::DeviceLostCallback, + ) { + unimplemented!(); + } + fn device_poll( &self, _device: &Self::DeviceId, diff --git a/wgpu/src/context.rs b/wgpu/src/context.rs index 52fbe14df3..0d1e95f3b9 100644 --- a/wgpu/src/context.rs +++ b/wgpu/src/context.rs @@ -2,7 +2,7 @@ use std::{any::Any, fmt::Debug, future::Future, num::NonZeroU64, ops::Range, pin use wgt::{ strict_assert, strict_assert_eq, AdapterInfo, BufferAddress, BufferSize, Color, - DownlevelCapabilities, DynamicOffset, Extent3d, Features, ImageDataLayout, + DeviceLostReason, DownlevelCapabilities, DynamicOffset, Extent3d, Features, ImageDataLayout, ImageSubresourceRange, IndexFormat, Limits, ShaderStages, SurfaceStatus, TextureFormat, TextureFormatFeatures, WasmNotSend, WasmNotSync, }; @@ -269,8 +269,19 @@ pub trait Context: Debug + WasmNotSend + WasmNotSync + Sized { desc: &RenderBundleEncoderDescriptor, ) -> (Self::RenderBundleEncoderId, Self::RenderBundleEncoderData); fn device_drop(&self, device: &Self::DeviceId, device_data: &Self::DeviceData); + fn device_set_device_lost_callback( + &self, + device: &Self::DeviceId, + device_data: &Self::DeviceData, + device_lost_callback: DeviceLostCallback, + ); fn device_destroy(&self, device: &Self::DeviceId, device_data: &Self::DeviceData); - fn device_lose(&self, device: &Self::DeviceId, device_data: &Self::DeviceData); + fn device_mark_lost( + &self, + device: &Self::DeviceId, + device_data: &Self::DeviceData, + message: &str, + ); fn device_poll( &self, device: &Self::DeviceId, @@ -1199,6 +1210,22 @@ pub type SubmittedWorkDoneCallback = Box; ) )))] pub type SubmittedWorkDoneCallback = Box; +#[cfg(any( + not(target_arch = "wasm32"), + all( + feature = "fragile-send-sync-non-atomic-wasm", + not(target_feature = "atomics") + ) +))] +pub type DeviceLostCallback = Box; +#[cfg(not(any( + not(target_arch = "wasm32"), + all( + feature = "fragile-send-sync-non-atomic-wasm", + not(target_feature = "atomics") + ) +)))] +pub type DeviceLostCallback = Box; /// An object safe variant of [`Context`] implemented by all types that implement [`Context`]. pub(crate) trait DynContext: Debug + WasmNotSend + WasmNotSync { @@ -1365,8 +1392,14 @@ pub(crate) trait DynContext: Debug + WasmNotSend + WasmNotSync { desc: &RenderBundleEncoderDescriptor, ) -> (ObjectId, Box); fn device_drop(&self, device: &ObjectId, device_data: &crate::Data); + fn device_set_device_lost_callback( + &self, + device: &ObjectId, + device_data: &crate::Data, + device_lost_callback: DeviceLostCallback, + ); fn device_destroy(&self, device: &ObjectId, device_data: &crate::Data); - fn device_lose(&self, device: &ObjectId, device_data: &crate::Data); + fn device_mark_lost(&self, device: &ObjectId, device_data: &crate::Data, message: &str); fn device_poll(&self, device: &ObjectId, device_data: &crate::Data, maintain: Maintain) -> bool; fn device_on_uncaptured_error( @@ -2428,16 +2461,27 @@ where Context::device_drop(self, &device, device_data) } + fn device_set_device_lost_callback( + &self, + device: &ObjectId, + device_data: &crate::Data, + device_lost_callback: DeviceLostCallback, + ) { + let device = ::from(*device); + let device_data = downcast_ref(device_data); + Context::device_set_device_lost_callback(self, &device, device_data, device_lost_callback) + } + fn device_destroy(&self, device: &ObjectId, device_data: &crate::Data) { let device = ::from(*device); let device_data = downcast_ref(device_data); Context::device_destroy(self, &device, device_data) } - fn device_lose(&self, device: &ObjectId, device_data: &crate::Data) { + fn device_mark_lost(&self, device: &ObjectId, device_data: &crate::Data, message: &str) { let device = ::from(*device); let device_data = downcast_ref(device_data); - Context::device_lose(self, &device, device_data) + Context::device_mark_lost(self, &device, device_data, message) } fn device_poll( diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 6c202a04a9..914e234f1d 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -32,19 +32,19 @@ pub use wgt::{ BindingType, BlendComponent, BlendFactor, BlendOperation, BlendState, BufferAddress, BufferBindingType, BufferSize, BufferUsages, Color, ColorTargetState, ColorWrites, CommandBufferDescriptor, CompareFunction, CompositeAlphaMode, DepthBiasState, - DepthStencilState, DeviceType, DownlevelCapabilities, DownlevelFlags, Dx12Compiler, - DynamicOffset, Extent3d, Face, Features, FilterMode, FrontFace, Gles3MinorVersion, - ImageDataLayout, ImageSubresourceRange, IndexFormat, InstanceDescriptor, InstanceFlags, Limits, - MultisampleState, Origin2d, Origin3d, PipelineStatisticsTypes, PolygonMode, PowerPreference, - PredefinedColorSpace, PresentMode, PresentationTimestamp, PrimitiveState, PrimitiveTopology, - PushConstantRange, QueryType, RenderBundleDepthStencil, SamplerBindingType, SamplerBorderColor, - ShaderLocation, ShaderModel, ShaderStages, StencilFaceState, StencilOperation, StencilState, - StorageTextureAccess, SurfaceCapabilities, SurfaceStatus, TextureAspect, TextureDimension, - TextureFormat, TextureFormatFeatureFlags, TextureFormatFeatures, TextureSampleType, - TextureUsages, TextureViewDimension, VertexAttribute, VertexFormat, VertexStepMode, - WasmNotSend, WasmNotSync, COPY_BUFFER_ALIGNMENT, COPY_BYTES_PER_ROW_ALIGNMENT, MAP_ALIGNMENT, - PUSH_CONSTANT_ALIGNMENT, QUERY_RESOLVE_BUFFER_ALIGNMENT, QUERY_SET_MAX_QUERIES, QUERY_SIZE, - VERTEX_STRIDE_ALIGNMENT, + DepthStencilState, DeviceLostReason, DeviceType, DownlevelCapabilities, DownlevelFlags, + Dx12Compiler, DynamicOffset, Extent3d, Face, Features, FilterMode, FrontFace, + Gles3MinorVersion, ImageDataLayout, ImageSubresourceRange, IndexFormat, InstanceDescriptor, + InstanceFlags, Limits, MultisampleState, Origin2d, Origin3d, PipelineStatisticsTypes, + PolygonMode, PowerPreference, PredefinedColorSpace, PresentMode, PresentationTimestamp, + PrimitiveState, PrimitiveTopology, PushConstantRange, QueryType, RenderBundleDepthStencil, + SamplerBindingType, SamplerBorderColor, ShaderLocation, ShaderModel, ShaderStages, + StencilFaceState, StencilOperation, StencilState, StorageTextureAccess, SurfaceCapabilities, + SurfaceStatus, TextureAspect, TextureDimension, TextureFormat, TextureFormatFeatureFlags, + TextureFormatFeatures, TextureSampleType, TextureUsages, TextureViewDimension, VertexAttribute, + VertexFormat, VertexStepMode, WasmNotSend, WasmNotSync, COPY_BUFFER_ALIGNMENT, + COPY_BYTES_PER_ROW_ALIGNMENT, MAP_ALIGNMENT, PUSH_CONSTANT_ALIGNMENT, + QUERY_RESOLVE_BUFFER_ALIGNMENT, QUERY_SET_MAX_QUERIES, QUERY_SIZE, VERTEX_STRIDE_ALIGNMENT, }; #[cfg(any( @@ -2811,6 +2811,19 @@ impl Device { pub fn destroy(&self) { DynContext::device_destroy(&*self.context, &self.id, self.data.as_ref()) } + + /// Set a DeviceLostCallback on this device. + pub fn set_device_lost_callback( + &self, + callback: impl FnOnce(DeviceLostReason, String) + Send + 'static, + ) { + DynContext::device_set_device_lost_callback( + &*self.context, + &self.id, + self.data.as_ref(), + Box::new(callback), + ) + } } impl Drop for Device {