win_desktop_duplication/
duplication.rs

1//! # Windows Desktop Duplication
2//! Module provides a convenient wrapper for [windows desktop duplication api](https://docs.microsoft.com/en-us/windows/win32/direct3ddxgi/desktop-dup-api)
3//! while adding few features to it.
4//!
5//! For more information on how to use check [DesktopDuplicationApi]
6
7use std::mem::size_of;
8use std::ptr::null;
9use std::time::Duration;
10
11use futures::StreamExt;
12use log::{debug, error, trace, warn};
13use tokio::time;
14use tokio::time::{Interval, MissedTickBehavior, sleep};
15use windows::core::Interface;
16use windows::core::Result as WinResult;
17use windows::Win32::Foundation::{BOOL, E_ACCESSDENIED, E_INVALIDARG, GENERIC_READ, GetLastError, POINT};
18use windows::Win32::Graphics::Direct3D::{D3D_DRIVER_TYPE_UNKNOWN, D3D_FEATURE_LEVEL, D3D_FEATURE_LEVEL_11_1};
19use windows::Win32::Graphics::Direct3D11::{D3D11_BIND_FLAG, D3D11_BIND_RENDER_TARGET, D3D11_CREATE_DEVICE_FLAG, D3D11_RESOURCE_MISC_FLAG, D3D11_RESOURCE_MISC_GDI_COMPATIBLE, D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC, D3D11_USAGE, D3D11_USAGE_DEFAULT, D3D11CreateDevice, ID3D11Device4, ID3D11DeviceContext4};
20use windows::Win32::Graphics::Dxgi::{DXGI_ERROR_ACCESS_DENIED, DXGI_ERROR_ACCESS_LOST, DXGI_ERROR_INVALID_CALL, DXGI_ERROR_SESSION_DISCONNECTED, DXGI_ERROR_UNSUPPORTED, DXGI_ERROR_WAIT_TIMEOUT, IDXGIDevice4, IDXGIOutputDuplication, IDXGIResource, IDXGISurface1};
21use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_R10G10B10A2_UNORM, DXGI_FORMAT_R16G16B16A16_FLOAT, DXGI_SAMPLE_DESC};
22use windows::Win32::Graphics::Gdi::DeleteObject;
23use windows::Win32::System::StationsAndDesktops::{DESKTOP_ACCESS_FLAGS, OpenInputDesktop, SetThreadDesktop};
24use windows::Win32::System::StationsAndDesktops::DF_ALLOWOTHERACCOUNTHOOK;
25use windows::Win32::UI::WindowsAndMessaging::{CURSOR_SHOWING, CURSORINFO, DI_NORMAL, DrawIconEx, GetCursorInfo, GetIconInfo, HCURSOR};
26
27use crate::devices::Adapter;
28use crate::errors::DDApiError;
29use crate::outputs::{Display, DisplayVSyncStream};
30use crate::Result;
31use crate::texture::{Texture, TextureDesc};
32
33#[cfg(test)]
34mod test {
35    use std::sync::Once;
36    use std::time::{Duration, Instant};
37
38    use futures::FutureExt;
39    use futures::select;
40    use log::LevelFilter::Debug;
41    use tokio::time::interval;
42
43    use crate::{DDApiError, DuplicationApiOptions};
44    use crate::devices::AdapterFactory;
45    use crate::duplication::DesktopDuplicationApi;
46    use crate::outputs::DisplayMode;
47    use crate::utils::{co_init, set_process_dpi_awareness};
48
49    static INIT: Once = Once::new();
50
51    pub fn initialize() {
52        INIT.call_once(|| {
53            let _ = env_logger::builder().is_test(true).filter_level(Debug).try_init();
54        });
55    }
56
57    #[test]
58    fn test_duplication() {
59        initialize();
60
61        let rt = tokio::runtime::Builder::new_current_thread()
62            .thread_name("graphics_thread".to_owned()).enable_time().build().unwrap();
63
64        rt.block_on(async {
65            set_process_dpi_awareness();
66            co_init();
67
68            let adapter = AdapterFactory::new().get_adapter_by_idx(0).unwrap();
69            let output = adapter.get_display_by_idx(0).unwrap();
70            let mut dupl = DesktopDuplicationApi::new(adapter, output.clone()).unwrap();
71            let curr_mode = output.get_current_display_mode().unwrap();
72            dupl.configure(DuplicationApiOptions {
73                skip_cursor: true
74            });
75            let new_mode = DisplayMode {
76                width: 1920,
77                height: 1080,
78                orientation: Default::default(),
79                refresh_num: curr_mode.refresh_num,
80                refresh_den: curr_mode.refresh_den,
81                hdr: false,
82            };
83
84            let mut counter = 0;
85            let mut secs = 0;
86            let mut interval = interval(Duration::from_secs(1));
87            output.set_display_mode(&new_mode).unwrap();
88            loop {
89                select! {
90                    tex = dupl.acquire_next_vsync_frame().fuse()=>{
91                        match &tex {
92                            Err(DDApiError::AccessDenied)| Err(DDApiError::AccessLost)  =>  {
93                                println!("error: {:?}",tex.err())
94                            }
95                            Err(e)=>{
96                                println!("error: {:?}",e)
97                            }
98                            Ok(_)=>{
99                                counter += 1;
100                            }
101                        }
102                    },
103                    _ = interval.tick().fuse() => {
104                        println!("fps: {}",counter);
105                        counter = 0;
106                        secs+=1;
107                        if secs == 5 {
108                            output.set_display_mode(&curr_mode).unwrap();
109                            println!("5 secs");
110                        } else if secs ==10 {
111                            break;
112                        }
113                    }
114                }
115                ;
116            };
117        });
118    }
119
120    #[test]
121    fn test_duplication_blocking() {
122        initialize();
123
124        set_process_dpi_awareness();
125        co_init();
126
127        let adapter = AdapterFactory::new().get_adapter_by_idx(0).unwrap();
128        let output = adapter.get_display_by_idx(0).unwrap();
129        let mut dupl = DesktopDuplicationApi::new(adapter, output.clone()).unwrap();
130        let curr_mode = output.get_current_display_mode().unwrap();
131        let new_mode = DisplayMode {
132            width: 1920,
133            height: 1080,
134            orientation: Default::default(),
135            refresh_num: curr_mode.refresh_num,
136            refresh_den: curr_mode.refresh_den,
137            hdr: false,
138        };
139
140        let mut counter = 0;
141        let mut secs = 0;
142        let instant = Instant::now();
143        loop {
144            let _ = output.wait_for_vsync();
145            let tex = dupl.acquire_next_frame_now();
146            if let Err(e) = tex {
147                println!("error: {:?}", e)
148            } else {
149                counter += 1;
150            };
151            if secs != instant.elapsed().as_secs() {
152                println!("fps: {}", counter);
153                counter = 0;
154                secs += 1;
155                if secs == 1 {
156                    println!("1 secs");
157                    output.set_display_mode(&new_mode).unwrap();
158                } else if secs == 5 {
159                    output.set_display_mode(&curr_mode).unwrap();
160                    break;
161                }
162            }
163        }
164    }
165}
166
167
168/// Provides asynchronous, synchronous api for windows desktop duplication with additional features such as
169/// cursor pre-drawn, frame rate synced to desktop refresh rate.
170///
171/// please note that this api works best if created and called from a single thread.
172/// Ideal scenario would be to maintain a "Graphics thread" in your application where all the
173/// Graphics related tasks are performed asynchronously.
174///
175/// acquire_next_frame_now especially should be called from only one thread because it only works if the
176/// thread calling it is marked as desktop thread. Although the application attempts to set any
177/// thread you call this method from as desktop thread, it's not usually a good idea.
178///
179/// # Async Example
180/// ```
181/// use win_desktop_duplication::duplication::DesktopDuplicationApi;
182/// async {
183///     let mut duplication = DesktopDuplicationApi::new(adapter, output)?;
184///     loop {
185///         let tex = duplication.acquire_next_vsync_frame().await?;
186///         // use the texture to encode video
187///     }
188///
189/// }
190///
191/// ```
192///
193/// # Sync Example
194/// ```
195///     use win_desktop_duplication::DesktopDuplicationApi;
196///     // ....
197///     {
198///         let mut duplication = DesktopDuplicationApi::new(adapter, output)?;
199///         loop {
200///             output.wait_for_vsync();
201///             let tex = duplication.acquire_next_frame_now()?;
202///             // use the texture to encode video
203///             //...
204///         }
205///     }
206/// ```
207pub struct DesktopDuplicationApi {
208    d3d_device: ID3D11Device4,
209    d3d_ctx: ID3D11DeviceContext4,
210    output: Display,
211    vsync_stream: DisplayVSyncStream,
212    dupl: Option<IDXGIOutputDuplication>,
213
214    options: DuplicationApiOptions,
215
216    state: DuplicationState,
217
218}
219
220unsafe impl Send for DesktopDuplicationApi {}
221
222unsafe impl Sync for DesktopDuplicationApi {}
223
224
225impl DesktopDuplicationApi {
226    /// Create a new instance of Desktop Duplication api from the provided [adapter][Adapter] and
227    /// [display][Display]. The application auto creates directx device and context from provided
228    /// adapter.
229    ///
230    /// If you wish to use your own directx device, context, use [new_with][Self::new_with] method
231    ///
232    /// this method fails with
233    /// * [DDApiError::Unsupported] when the application's dpi awareness is not set. use [crate::set_process_dpi_awareness]
234    pub fn new(adapter: Adapter, output: Display) -> Result<Self> {
235        let (device, ctx) = Self::create_device(&adapter)?;
236        Self::new_with(device, ctx, output)
237    }
238
239    /// Creates a new instance of the api from provided device and context.
240    pub fn new_with(d3d_device: ID3D11Device4, ctx: ID3D11DeviceContext4, output: Display) -> Result<Self> {
241        let dupl = Self::create_dupl_output(&d3d_device, &output)?;
242        Ok(Self {
243            d3d_device,
244            d3d_ctx: ctx,
245            vsync_stream: output.get_vsync_stream(),
246            output,
247            dupl: Some(dupl),
248            options: Default::default(),
249            state: Default::default(),
250        })
251    }
252
253    /// Acquire next frame from the desktop duplication api after waiting for vsync refresh.
254    /// this helps application acquire frames with same rate as display's native refresh-rate.
255    ///
256    /// this is an asynchronous method. check example in the [doc][DesktopDuplicationApi] for more details
257    ///
258    /// This method fails with following errors
259    ///
260    /// ## Recoverable errors
261    /// these can be recovered by just calling the function again after this error.
262    /// * [DDApiError::AccessLost] - when desktop mode switch happens (resolution change) or desktop
263    /// changes. (going to lock screen etc).
264    /// * [DDApiError::AccessDenied] - when windows opens a secure environment, this application
265    /// will be denied access.
266    ///
267    /// ## Non-recoverable errors
268    /// * [DDApiError::Unexpected] - this type of error cant be recovered from. the application should
269    /// drop the struct and re create a new instance.
270    pub async fn acquire_next_vsync_frame(&mut self) -> Result<Texture> {
271        // wait for vsync
272        if (self.vsync_stream.next().await).is_some_and(|r| r.is_err()) {
273            return Err(DDApiError::Unexpected("DisplayVSyncStream failed unexpectedly".to_owned()));
274        }
275        // acquire next_frame
276        let res = self.acquire_next_frame_now();
277        if res.is_err() {
278            trace!("something went wrong with acquiring next frame. probably desktop duplication \
279            instance failed");
280        }
281        res
282    }
283
284    fn create_device(adapter: &Adapter) -> Result<(ID3D11Device4, ID3D11DeviceContext4)> {
285        let feature_levels = [D3D_FEATURE_LEVEL_11_1];
286        let mut feature_level: D3D_FEATURE_LEVEL = Default::default();
287        let mut d3d_device = None;
288        let mut d3d_ctx = None;
289
290        let resp = unsafe {
291            D3D11CreateDevice(adapter.as_raw_ref(), D3D_DRIVER_TYPE_UNKNOWN,
292                              None, D3D11_CREATE_DEVICE_FLAG(0),
293                              Some(&feature_levels), D3D11_SDK_VERSION,
294                              Some(&mut d3d_device), Some(&mut feature_level),
295                              Some(&mut d3d_ctx))
296        };
297        if resp.is_err() {
298            Err(DDApiError::Unexpected(format!("faild d3d11 create device. {:?}", resp)))
299        } else {
300            Ok((d3d_device.unwrap().cast().unwrap(), d3d_ctx.unwrap().cast().unwrap()))
301        }
302    }
303
304    fn create_dupl_output(dev: &ID3D11Device4, output: &Display) -> Result<IDXGIOutputDuplication> {
305        let supported_formats = [DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_R10G10B10A2_UNORM, DXGI_FORMAT_R16G16B16A16_FLOAT];
306        let device: IDXGIDevice4 = dev.cast().unwrap();
307        let dupl: WinResult<IDXGIOutputDuplication> = unsafe { output.as_raw_ref().DuplicateOutput1(&device, 0, &supported_formats) };
308
309        if let Err(err) = dupl {
310            return match err.code() {
311                E_INVALIDARG => {
312                    Err(DDApiError::BadParam(format!("failed to create duplicate output. {:?}", err)))
313                }
314                E_ACCESSDENIED => {
315                    Err(DDApiError::AccessDenied)
316                }
317                DXGI_ERROR_UNSUPPORTED => {
318                    Err(DDApiError::Unsupported)
319                }
320                DXGI_ERROR_SESSION_DISCONNECTED => {
321                    Err(DDApiError::Disconnected)
322                }
323                _ => {
324                    Err(DDApiError::Unexpected(err.to_string()))
325                }
326            };
327        }
328        Ok(dupl.unwrap())
329    }
330
331    /// unlike [acquire_next_vsync_frame][Self::acquire_next_vsync_frame], this is a blocking call and immediately returns the texture
332    /// without waiting for vsync.
333    ///
334    /// the method handles any switches in desktop automatically.
335    ///
336    /// this fails with following results:
337    ///
338    /// ## Recoverable errors
339    /// these can be recovered by just calling the function again after this error.
340    /// * [DDApiError::AccessLost] - when desktop mode switch happens (resolution change) or desktop
341    /// changes. (going to lock screen etc).
342    /// * [DDApiError::AccessDenied] - when windows opens a secure environment, this application
343    /// will be denied access.
344    ///
345    /// ## Non-recoverable errors
346    /// * [DDApiError::Unexpected] - this type of error cant be recovered from. the application should
347    /// drop the struct and re create a new instance.
348    pub fn acquire_next_frame_now(&mut self) -> Result<Texture> {
349        let mut frame_info = Default::default();
350
351        if self.dupl.is_none() {
352            self.reacquire_dup()?;
353        }
354        let dupl = self.dupl.as_ref().unwrap();
355        let status = unsafe { dupl.AcquireNextFrame(0, &mut frame_info, &mut self.state.last_resource) };
356        if let Err(e) = status {
357            match e.code() {
358                DXGI_ERROR_ACCESS_LOST => {
359                    warn!("display access lost. maybe desktop mode switch?, {:?}",e);
360                    self.reacquire_dup()?;
361                    return Err(DDApiError::AccessLost);
362                }
363                DXGI_ERROR_ACCESS_DENIED => {
364                    warn!("display access is denied. Maybe running in a secure environment?");
365                    self.reacquire_dup()?;
366                    return Err(DDApiError::AccessDenied);
367                }
368                DXGI_ERROR_INVALID_CALL => {
369                    warn!("dxgi_error_invalid_call. maybe forgot to ReleaseFrame()?");
370                    self.reacquire_dup()?;
371                    return Err(DDApiError::AccessLost);
372                }
373                DXGI_ERROR_WAIT_TIMEOUT => {
374                    trace!("no new frame is available");
375                }
376                _ => {
377                    return Err(DDApiError::Unexpected(format!("acquire frame failed {:?}", e)));
378                }
379            }
380        }
381
382
383        if let Some(resource) = self.state.last_resource.as_ref() {
384            debug!("got fresh resource. accumulated {} frames",frame_info.AccumulatedFrames);
385            self.state.frame_locked = true;
386            let new_frame = Texture::new(resource.cast().unwrap());
387            self.ensure_cache_frame(&new_frame).inspect_err(|_| {
388                self.release_locked_frame();
389            })?;
390            unsafe { self.d3d_ctx.CopyResource(self.state.frame.as_ref().unwrap().as_raw_ref(), new_frame.as_raw_ref()); }
391            self.release_locked_frame();
392        } else {
393            debug!("no fresh resource. accumulated {} frames",frame_info.AccumulatedFrames);
394        }
395        if self.state.frame.is_none() {
396            return Err(DDApiError::AccessLost);
397        }
398
399        let cache_frame = self.state.frame.clone().unwrap();
400
401        if !self.options.skip_cursor {
402            self.ensure_cache_cursor_frame(&cache_frame)?;
403            let cache_cursor_frame = self.state.cursor_frame.clone().unwrap();
404
405            unsafe {
406                self.d3d_ctx.CopyResource(
407                    cache_cursor_frame.as_raw_ref(),
408                    cache_frame.as_raw_ref())
409            }
410
411            self.draw_cursor(&cache_cursor_frame)?;
412            Ok(cache_cursor_frame)
413        } else {
414            Ok(cache_frame)
415        }
416    }
417
418
419    /// this method is used to retrieve device and context used in this api. These can be used
420    /// to build directx color conversion and image scale.
421    pub fn get_device_and_ctx(&self) -> (ID3D11Device4, ID3D11DeviceContext4) {
422        return (self.d3d_device.clone(), self.d3d_ctx.clone());
423    }
424
425    /// configure duplication manager with given options.
426    pub fn configure(&mut self, opt: DuplicationApiOptions) {
427        self.options = opt;
428    }
429
430    fn draw_cursor(&mut self, tex: &Texture) -> Result<()> {
431        trace!("drawing cursor");
432        let mut cursor_info = CURSORINFO {
433            cbSize: size_of::<CURSORINFO>() as u32,
434            ..Default::default()
435        };
436        let cursor_present = unsafe { GetCursorInfo(&mut cursor_info as *mut CURSORINFO) };
437
438        // if cursor is not present, return raw frame.
439        if cursor_present.is_err()
440            || (cursor_info.flags.0 & CURSOR_SHOWING.0 != CURSOR_SHOWING.0)
441        {
442            debug!("cursor is absent so not drawing anything");
443            return Ok(());
444        }
445
446        if self.state.cursor.is_none() || cursor_info.hCursor != *self.state.cursor.as_ref().unwrap() {
447            self.state.cursor = Some(cursor_info.hCursor);
448            let point = Self::get_icon_hotspot(cursor_info.hCursor)?;
449            self.state.hotspot_x = point.x as _;
450            self.state.hotspot_y = point.y as _;
451        }
452
453        let surface: IDXGISurface1 = tex.as_raw_ref().cast().unwrap();
454        let hdc = unsafe { surface.GetDC(BOOL::from(false)) };
455        if let Err(err) = hdc {
456            return Err(DDApiError::Unexpected(format!("failed to get DC for cursor image. {:?}", err)));
457        }
458        let hdc = hdc.unwrap();
459
460        let result = unsafe {
461            DrawIconEx(
462                hdc,
463                cursor_info.ptScreenPos.x - self.state.hotspot_x,
464                cursor_info.ptScreenPos.y - self.state.hotspot_y,
465                self.state.cursor.unwrap(),
466                0, 0, 0, None, DI_NORMAL,
467            )
468        };
469
470        if result.is_err() {
471            unsafe { return Err(DDApiError::Unexpected(format!("failed to draw icon. {:?}", GetLastError()))); }
472        }
473
474        let _ = unsafe { surface.ReleaseDC(None) };
475        Ok(())
476    }
477
478    fn get_icon_hotspot(cursor: HCURSOR) -> Result<POINT> {
479        // get icon information
480        let mut icon_info = Default::default();
481        let result = unsafe { GetIconInfo(cursor, &mut icon_info) };
482        if result.is_err() {
483            unsafe { return Err(DDApiError::Unexpected(format!("failed to get icon info. `{:?}`", GetLastError()))); }
484        }
485
486        if !icon_info.hbmMask.is_invalid() {
487            unsafe { DeleteObject(icon_info.hbmMask); }
488        }
489        if !icon_info.hbmColor.is_invalid() {
490            unsafe { DeleteObject(icon_info.hbmColor); }
491        }
492
493        Ok(POINT { x: icon_info.xHotspot as _, y: icon_info.yHotspot as _ })
494    }
495
496    fn reacquire_dup(&mut self) -> Result<()> {
497        self.state.reset();
498        self.dupl = None;
499
500        let dupl = Self::create_dupl_output(&self.d3d_device, &self.output);
501        if dupl.is_err() {
502            let _ = Self::switch_thread_desktop();
503        }
504        let dupl = dupl?;
505        debug!("successfully acquired new duplication instance");
506        self.dupl = Some(dupl);
507        Ok(())
508    }
509
510    fn release_locked_frame(&mut self) {
511        if self.state.last_resource.is_some() {
512            self.state.last_resource = None;
513        }
514        if self.dupl.is_some() {
515            if self.state.frame_locked {
516                let _ = unsafe { self.dupl.as_ref().unwrap().ReleaseFrame() };
517                self.state.frame_locked = false;
518            }
519        }
520    }
521
522    fn ensure_cache_frame(&mut self, frame: &Texture) -> Result<()> {
523        if self.state.frame.is_none() {
524            let tex = self.create_texture(frame.desc(), D3D11_USAGE_DEFAULT,
525                                          D3D11_BIND_RENDER_TARGET,
526                                          Default::default())?;
527            self.state.frame = Some(tex);
528        }
529        Ok(())
530    }
531
532    fn ensure_cache_cursor_frame(&mut self, frame: &Texture) -> Result<()> {
533        if self.state.cursor_frame.is_none() {
534            let tex = self.create_texture(frame.desc(), D3D11_USAGE_DEFAULT,
535                                          D3D11_BIND_RENDER_TARGET,
536                                          D3D11_RESOURCE_MISC_GDI_COMPATIBLE)?;
537            self.state.cursor_frame = Some(tex);
538        }
539        Ok(())
540    }
541
542    fn create_texture(&self, tex_desc: TextureDesc, usage: D3D11_USAGE, bind_flags: D3D11_BIND_FLAG,
543                      misc_flag: D3D11_RESOURCE_MISC_FLAG) -> Result<Texture> {
544        let desc = D3D11_TEXTURE2D_DESC {
545            Width: tex_desc.width,
546            Height: tex_desc.height,
547            MipLevels: 1,
548            ArraySize: 1,
549            Format: tex_desc.format.into(),
550            SampleDesc: DXGI_SAMPLE_DESC {
551                Count: 1,
552                Quality: 0,
553            },
554            Usage: usage,
555            BindFlags: bind_flags.0 as u32,
556            CPUAccessFlags: Default::default(),
557            MiscFlags: misc_flag.0 as u32,
558        };
559        let mut tex = None;
560        let result = unsafe { self.d3d_device.CreateTexture2D(&desc, None, Some(&mut tex)) };
561        if let Err(e) = result {
562            Err(DDApiError::Unexpected(format!("failed to create texture. {:?}", e)))
563        } else {
564            Ok(Texture::new(tex.unwrap()))
565        }
566    }
567
568    fn switch_thread_desktop() -> Result<()> {
569        debug!("trying to switch Thread desktop");
570        let desk = unsafe { OpenInputDesktop(DF_ALLOWOTHERACCOUNTHOOK as _, true, DESKTOP_ACCESS_FLAGS(GENERIC_READ.0)) };
571        if let Err(err) = desk {
572            error!("dint get desktop : {:?}", err);
573            return Err(DDApiError::AccessDenied);
574        }
575        let result = unsafe { SetThreadDesktop(desk.unwrap()) };
576        if result.is_err() {
577            error!("dint switch desktop: {:?}",unsafe{GetLastError().to_hresult()});
578            return Err(DDApiError::AccessDenied);
579        }
580        Ok(())
581    }
582}
583
584
585/// Settings to configure Desktop duplication api. these can be configured even after initialized.
586///
587/// currently it only supports option to skip drawing cursor
588#[derive(Default)]
589pub struct DuplicationApiOptions {
590    pub skip_cursor: bool,
591}
592
593// these are state variables for duplication sync stream
594#[derive(Default)]
595struct DuplicationState {
596    frame_locked: bool,
597    last_resource: Option<IDXGIResource>,
598
599    frame: Option<Texture>,
600    cursor_frame: Option<Texture>,
601
602    cursor: Option<HCURSOR>,
603    hotspot_x: i32,
604    hotspot_y: i32,
605}
606
607impl DuplicationState {
608    pub fn reset(&mut self) {
609        self.frame = None;
610        self.last_resource = None;
611        self.cursor_frame = None;
612        self.frame_locked = false;
613    }
614}