1#![forbid(unsafe_code)]
7
8use std::{
9 sync::{
10 Arc,
11 atomic::{AtomicU8, Ordering},
12 },
13 thread::ThreadId,
14};
15
16pub const PLUGIN_SDK_API_VERSION: u32 = 1;
18
19pub mod gpu {
23 pub use wgpu;
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum PluginError {
29 Unsupported,
31 InvalidDescriptor,
33 Busy,
35 NoFrame,
37 Shutdown,
39}
40
41pub type Result<T> = core::result::Result<T, PluginError>;
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum TextureFormat {
47 Rgba8Unorm,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub struct TextureDescriptor {
54 pub width: u32,
56 pub height: u32,
58 pub format: TextureFormat,
60}
61
62impl TextureDescriptor {
63 fn is_valid(self) -> bool {
64 self.width > 0 && self.height > 0
65 }
66}
67
68#[doc(hidden)]
70pub type WgpuRenderTask =
71 Box<dyn FnOnce(&wgpu::Device, &mut wgpu::CommandEncoder, &wgpu::TextureView) + Send + 'static>;
72
73#[doc(hidden)]
75#[async_trait::async_trait]
76pub trait WgpuTextureBackendHandle: Send + Sync {
77 fn texture_id(&self) -> i64;
79 fn try_next_frame(&self) -> Result<Arc<dyn WgpuTextureFrameBackend>>;
81 async fn next_frame(&self) -> Result<Arc<dyn WgpuTextureFrameBackend>>;
83}
84
85#[doc(hidden)]
87pub trait WgpuTextureFrameBackend: Send + Sync {
88 fn render(&self, task: WgpuRenderTask) -> Result<()>;
90 fn present(&self) -> Result<()>;
92}
93
94#[doc(hidden)]
96pub type PixelWriteTask = Box<dyn FnOnce(&mut [u8], usize) + Send + 'static>;
97
98#[doc(hidden)]
100#[async_trait::async_trait]
101pub trait PixelBufferTextureBackendHandle: Send + Sync {
102 fn texture_id(&self) -> i64;
104 fn try_next_frame(&self) -> Result<Arc<dyn PixelBufferTextureFrameBackend>>;
106 async fn next_frame(&self) -> Result<Arc<dyn PixelBufferTextureFrameBackend>>;
108}
109
110#[doc(hidden)]
112pub trait PixelBufferTextureFrameBackend: Send + Sync {
113 fn write_pixels(&self, task: PixelWriteTask) -> Result<()>;
115 fn present(&self) -> Result<()>;
117}
118
119#[doc(hidden)]
121pub trait WgpuTextureBackend: Send + Sync {
122 fn create_texture(
124 &self,
125 descriptor: TextureDescriptor,
126 ) -> Result<Arc<dyn WgpuTextureBackendHandle>>;
127 fn create_pixel_buffer_texture(
129 &self,
130 descriptor: TextureDescriptor,
131 ) -> Result<Arc<dyn PixelBufferTextureBackendHandle>>;
132}
133
134#[derive(Clone)]
136pub struct GpuTextures {
137 backend: Arc<dyn WgpuTextureBackend>,
138}
139
140impl GpuTextures {
141 pub fn create_texture(&self, descriptor: TextureDescriptor) -> Result<WgpuTexture> {
144 if !descriptor.is_valid() {
145 return Err(PluginError::InvalidDescriptor);
146 }
147 Ok(WgpuTexture {
148 backend: self.backend.create_texture(descriptor)?,
149 })
150 }
151
152 pub fn create_pixel_buffer_texture(
155 &self,
156 descriptor: TextureDescriptor,
157 ) -> Result<PixelBufferTexture> {
158 if !descriptor.is_valid() {
159 return Err(PluginError::InvalidDescriptor);
160 }
161 Ok(PixelBufferTexture {
162 backend: self.backend.create_pixel_buffer_texture(descriptor)?,
163 })
164 }
165
166 #[doc(hidden)]
168 pub fn for_shell(backend: Arc<dyn WgpuTextureBackend>) -> Self {
169 Self { backend }
170 }
171}
172
173pub struct PixelBufferTexture {
175 backend: Arc<dyn PixelBufferTextureBackendHandle>,
176}
177
178impl PixelBufferTexture {
179 pub fn texture_id(&self) -> i64 {
181 self.backend.texture_id()
182 }
183
184 pub fn try_next_frame(&self) -> Result<PixelBufferTextureFrame> {
186 Ok(PixelBufferTextureFrame::new(self.backend.try_next_frame()?))
187 }
188
189 pub async fn next_frame(&self) -> Result<PixelBufferTextureFrame> {
191 self.backend
192 .next_frame()
193 .await
194 .map(PixelBufferTextureFrame::new)
195 }
196}
197
198pub struct PixelBufferTextureFrame {
200 backend: Arc<dyn PixelBufferTextureFrameBackend>,
201 written: bool,
202}
203
204impl PixelBufferTextureFrame {
205 fn new(backend: Arc<dyn PixelBufferTextureFrameBackend>) -> Self {
206 Self {
207 backend,
208 written: false,
209 }
210 }
211
212 pub fn write_pixels(
216 &mut self,
217 writer: impl FnOnce(&mut [u8], usize) + Send + 'static,
218 ) -> Result<()> {
219 if self.written {
220 return Err(PluginError::Busy);
221 }
222 self.backend.write_pixels(Box::new(writer))?;
223 self.written = true;
224 Ok(())
225 }
226
227 pub fn present(self) -> Result<()> {
229 if !self.written {
230 return Err(PluginError::NoFrame);
231 }
232 self.backend.present()
233 }
234}
235
236pub struct WgpuTexture {
238 backend: Arc<dyn WgpuTextureBackendHandle>,
239}
240
241impl WgpuTexture {
242 pub fn texture_id(&self) -> i64 {
244 self.backend.texture_id()
245 }
246
247 pub fn try_next_frame(&self) -> Result<WgpuTextureFrame> {
249 Ok(WgpuTextureFrame::new(self.backend.try_next_frame()?))
250 }
251
252 pub async fn next_frame(&self) -> Result<WgpuTextureFrame> {
255 self.backend.next_frame().await.map(WgpuTextureFrame::new)
256 }
257}
258
259pub struct WgpuTextureFrame {
261 backend: Arc<dyn WgpuTextureFrameBackend>,
262 rendered: bool,
263}
264
265impl WgpuTextureFrame {
266 fn new(backend: Arc<dyn WgpuTextureFrameBackend>) -> Self {
267 Self {
268 backend,
269 rendered: false,
270 }
271 }
272
273 pub fn render(
275 &mut self,
276 task: impl FnOnce(&wgpu::Device, &mut wgpu::CommandEncoder, &wgpu::TextureView) + Send + 'static,
277 ) -> Result<()> {
278 if self.rendered {
279 return Err(PluginError::Busy);
280 }
281 self.backend.render(Box::new(task))?;
282 self.rendered = true;
283 Ok(())
284 }
285
286 pub fn present(self) -> Result<()> {
289 if !self.rendered {
290 return Err(PluginError::NoFrame);
291 }
292 self.backend.present()
293 }
294}
295
296#[doc(hidden)]
298pub type MainThreadTask = Box<dyn FnOnce() + Send + 'static>;
299
300#[derive(Debug, Clone, Copy, PartialEq, Eq)]
302pub enum DispatchError {
303 NotReady,
305 Shutdown,
307}
308
309const DISPATCHER_STARTING: u8 = 0;
310const DISPATCHER_RUNNING: u8 = 1;
311const DISPATCHER_SHUTDOWN: u8 = 2;
312
313#[derive(Clone)]
319pub struct MainThreadDispatcher {
320 post: Arc<dyn Fn(MainThreadTask) -> bool + Send + Sync>,
321 main_thread: ThreadId,
322 state: Arc<AtomicU8>,
323}
324
325impl MainThreadDispatcher {
326 pub fn dispatch(
328 &self,
329 task: impl FnOnce() + Send + 'static,
330 ) -> core::result::Result<(), DispatchError> {
331 match self.state.load(Ordering::Acquire) {
332 DISPATCHER_STARTING => return Err(DispatchError::NotReady),
333 DISPATCHER_SHUTDOWN => return Err(DispatchError::Shutdown),
334 DISPATCHER_RUNNING => {}
335 _ => unreachable!("invalid main-thread dispatcher state"),
336 }
337 let state = Arc::clone(&self.state);
338 let guarded_task = Box::new(move || {
339 if state.load(Ordering::Acquire) == DISPATCHER_RUNNING {
340 task();
341 }
342 });
343 if !(self.post)(guarded_task) {
344 return Err(DispatchError::Shutdown);
345 }
346 Ok(())
347 }
348
349 pub fn is_main_thread(&self) -> bool {
351 std::thread::current().id() == self.main_thread
352 }
353
354 #[doc(hidden)]
356 pub fn for_shell(
357 post: impl Fn(MainThreadTask) -> bool + Send + Sync + 'static,
358 main_thread: ThreadId,
359 ) -> Self {
360 Self::for_shell_with_state(post, main_thread, DISPATCHER_RUNNING)
361 }
362
363 #[doc(hidden)]
365 pub fn for_shell_inactive(
366 post: impl Fn(MainThreadTask) -> bool + Send + Sync + 'static,
367 main_thread: ThreadId,
368 ) -> Self {
369 Self::for_shell_with_state(post, main_thread, DISPATCHER_STARTING)
370 }
371
372 fn for_shell_with_state(
373 post: impl Fn(MainThreadTask) -> bool + Send + Sync + 'static,
374 main_thread: ThreadId,
375 state: u8,
376 ) -> Self {
377 Self {
378 post: Arc::new(post),
379 main_thread,
380 state: Arc::new(AtomicU8::new(state)),
381 }
382 }
383
384 #[doc(hidden)]
386 pub fn start_for_shell(&self) -> bool {
387 self.state
388 .compare_exchange(
389 DISPATCHER_STARTING,
390 DISPATCHER_RUNNING,
391 Ordering::AcqRel,
392 Ordering::Acquire,
393 )
394 .is_ok()
395 }
396
397 #[doc(hidden)]
399 pub fn shutdown_for_shell(&self) {
400 self.state.store(DISPATCHER_SHUTDOWN, Ordering::Release);
401 }
402}
403
404pub struct PluginRegistrar {
409 main_thread_dispatcher: MainThreadDispatcher,
410 gpu_textures: Option<GpuTextures>,
411}
412
413impl PluginRegistrar {
414 #[doc(hidden)]
420 pub fn for_shell(main_thread_dispatcher: MainThreadDispatcher) -> Self {
421 Self {
422 main_thread_dispatcher,
423 gpu_textures: None,
424 }
425 }
426
427 pub fn main_thread_dispatcher(&self) -> &MainThreadDispatcher {
429 &self.main_thread_dispatcher
430 }
431
432 pub fn gpu(&self) -> Result<&GpuTextures> {
434 self.gpu_textures.as_ref().ok_or(PluginError::Unsupported)
435 }
436
437 #[doc(hidden)]
440 pub fn install_gpu_for_shell(&mut self, gpu_textures: GpuTextures) -> bool {
441 if self.gpu_textures.is_some() {
442 return false;
443 }
444 self.gpu_textures = Some(gpu_textures);
445 true
446 }
447}
448
449pub trait FlutterRustPlugin: Send + Sync + 'static {
451 fn register(&self, registrar: &mut PluginRegistrar) -> Result<()>;
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458 use parking_lot::Mutex;
459 use std::collections::VecDeque;
460
461 struct FakeGpuBackend {
462 operations: Arc<Mutex<Vec<&'static str>>>,
463 }
464
465 struct FakeTextureBackend {
466 operations: Arc<Mutex<Vec<&'static str>>>,
467 }
468
469 struct FakeFrameBackend {
470 operations: Arc<Mutex<Vec<&'static str>>>,
471 }
472
473 struct FakePixelTextureBackend {
474 operations: Arc<Mutex<Vec<&'static str>>>,
475 }
476
477 struct FakePixelFrameBackend {
478 operations: Arc<Mutex<Vec<&'static str>>>,
479 pixels: Mutex<Vec<u8>>,
480 }
481
482 impl WgpuTextureBackend for FakeGpuBackend {
483 fn create_texture(
484 &self,
485 _descriptor: TextureDescriptor,
486 ) -> Result<Arc<dyn WgpuTextureBackendHandle>> {
487 self.operations.lock().push("create");
488 Ok(Arc::new(FakeTextureBackend {
489 operations: Arc::clone(&self.operations),
490 }))
491 }
492
493 fn create_pixel_buffer_texture(
494 &self,
495 _descriptor: TextureDescriptor,
496 ) -> Result<Arc<dyn PixelBufferTextureBackendHandle>> {
497 self.operations.lock().push("create_pixels");
498 Ok(Arc::new(FakePixelTextureBackend {
499 operations: Arc::clone(&self.operations),
500 }))
501 }
502 }
503
504 #[async_trait::async_trait]
505 impl PixelBufferTextureBackendHandle for FakePixelTextureBackend {
506 fn texture_id(&self) -> i64 {
507 23
508 }
509
510 fn try_next_frame(&self) -> Result<Arc<dyn PixelBufferTextureFrameBackend>> {
511 self.operations.lock().push("reserve_pixels");
512 Ok(Arc::new(FakePixelFrameBackend {
513 operations: Arc::clone(&self.operations),
514 pixels: Mutex::new(vec![0; 32]),
515 }))
516 }
517
518 async fn next_frame(&self) -> Result<Arc<dyn PixelBufferTextureFrameBackend>> {
519 self.try_next_frame()
520 }
521 }
522
523 impl PixelBufferTextureFrameBackend for FakePixelFrameBackend {
524 fn write_pixels(&self, task: PixelWriteTask) -> Result<()> {
525 self.operations.lock().push("write_pixels");
526 task(&mut self.pixels.lock(), 16);
527 Ok(())
528 }
529
530 fn present(&self) -> Result<()> {
531 self.operations.lock().push("present_pixels");
532 Ok(())
533 }
534 }
535
536 #[async_trait::async_trait]
537 impl WgpuTextureBackendHandle for FakeTextureBackend {
538 fn texture_id(&self) -> i64 {
539 17
540 }
541
542 fn try_next_frame(&self) -> Result<Arc<dyn WgpuTextureFrameBackend>> {
543 self.operations.lock().push("reserve");
544 Ok(Arc::new(FakeFrameBackend {
545 operations: Arc::clone(&self.operations),
546 }))
547 }
548
549 async fn next_frame(&self) -> Result<Arc<dyn WgpuTextureFrameBackend>> {
550 self.try_next_frame()
551 }
552 }
553
554 impl WgpuTextureFrameBackend for FakeFrameBackend {
555 fn render(&self, _task: WgpuRenderTask) -> Result<()> {
556 self.operations.lock().push("render");
557 Ok(())
558 }
559
560 fn present(&self) -> Result<()> {
561 self.operations.lock().push("present");
562 Ok(())
563 }
564 }
565
566 struct TestPlugin;
567
568 impl FlutterRustPlugin for TestPlugin {
569 fn register(&self, _registrar: &mut PluginRegistrar) -> Result<()> {
570 Ok(())
571 }
572 }
573
574 #[test]
575 fn plugins_register_with_the_shell_registrar() {
576 let queue = Arc::new(Mutex::new(VecDeque::<MainThreadTask>::new()));
577 let queue_for_post = Arc::clone(&queue);
578 let dispatcher = MainThreadDispatcher::for_shell(
579 move |task| {
580 queue_for_post.lock().push_back(task);
581 true
582 },
583 std::thread::current().id(),
584 );
585 let mut registrar = PluginRegistrar::for_shell(dispatcher);
586 TestPlugin.register(&mut registrar).unwrap();
587 }
588
589 #[test]
590 fn gpu_capability_validates_and_hides_the_runtime_backend() {
591 let dispatcher = MainThreadDispatcher::for_shell(|_| true, std::thread::current().id());
592 let mut registrar = PluginRegistrar::for_shell(dispatcher);
593 assert!(matches!(registrar.gpu(), Err(PluginError::Unsupported)));
594
595 let operations = Arc::new(Mutex::new(Vec::new()));
596 let capability = GpuTextures::for_shell(Arc::new(FakeGpuBackend {
597 operations: Arc::clone(&operations),
598 }));
599 assert!(registrar.install_gpu_for_shell(capability.clone()));
600 assert!(!registrar.install_gpu_for_shell(capability));
601 assert!(matches!(
602 registrar.gpu().unwrap().create_texture(TextureDescriptor {
603 width: 0,
604 height: 32,
605 format: TextureFormat::Rgba8Unorm,
606 }),
607 Err(PluginError::InvalidDescriptor)
608 ));
609
610 let texture = registrar
611 .gpu()
612 .unwrap()
613 .create_texture(TextureDescriptor {
614 width: 64,
615 height: 32,
616 format: TextureFormat::Rgba8Unorm,
617 })
618 .unwrap();
619 assert_eq!(texture.texture_id(), 17);
620 assert!(matches!(
621 texture.try_next_frame().unwrap().present(),
622 Err(PluginError::NoFrame)
623 ));
624 let mut frame = texture.try_next_frame().unwrap();
625 frame.render(|_, _, _| {}).unwrap();
626 assert_eq!(frame.render(|_, _, _| {}), Err(PluginError::Busy));
627 frame.present().unwrap();
628
629 assert_eq!(
630 *operations.lock(),
631 vec!["create", "reserve", "reserve", "render", "present"]
632 );
633 }
634
635 #[test]
636 fn pixel_buffer_frames_write_directly_into_shell_storage() {
637 let operations = Arc::new(Mutex::new(Vec::new()));
638 let gpu = GpuTextures::for_shell(Arc::new(FakeGpuBackend {
639 operations: Arc::clone(&operations),
640 }));
641 let texture = gpu
642 .create_pixel_buffer_texture(TextureDescriptor {
643 width: 4,
644 height: 2,
645 format: TextureFormat::Rgba8Unorm,
646 })
647 .unwrap();
648 assert_eq!(texture.texture_id(), 23);
649 assert_eq!(
650 texture.try_next_frame().unwrap().present(),
651 Err(PluginError::NoFrame)
652 );
653 let mut frame = texture.try_next_frame().unwrap();
654 frame
655 .write_pixels(|pixels, row_bytes| {
656 assert_eq!(row_bytes, 16);
657 assert_eq!(pixels.len(), 32);
658 pixels.fill(0x7f);
659 })
660 .unwrap();
661 assert_eq!(frame.write_pixels(|_, _| {}), Err(PluginError::Busy));
662 frame.present().unwrap();
663 assert_eq!(
664 *operations.lock(),
665 vec![
666 "create_pixels",
667 "reserve_pixels",
668 "reserve_pixels",
669 "write_pixels",
670 "present_pixels"
671 ]
672 );
673 }
674
675 #[test]
676 fn worker_dispatch_is_deferred_non_reentrant_and_rejects_shutdown() {
677 let queue = Arc::new(Mutex::new(VecDeque::<MainThreadTask>::new()));
678 let queue_for_post = Arc::clone(&queue);
679 let dispatcher = MainThreadDispatcher::for_shell(
680 move |task| {
681 queue_for_post.lock().push_back(task);
682 true
683 },
684 std::thread::current().id(),
685 );
686 let order = Arc::new(Mutex::new(Vec::new()));
687 let worker_dispatcher = dispatcher.clone();
688 let nested_dispatcher = dispatcher.clone();
689 let order_in_task = Arc::clone(&order);
690 std::thread::spawn(move || {
691 assert!(!worker_dispatcher.is_main_thread());
692 worker_dispatcher
693 .dispatch(move || {
694 assert!(nested_dispatcher.is_main_thread());
695 order_in_task.lock().push(1);
696 let nested_order = Arc::clone(&order_in_task);
697 nested_dispatcher
698 .dispatch(move || nested_order.lock().push(2))
699 .unwrap();
700 })
701 .unwrap();
702 })
703 .join()
704 .unwrap();
705
706 assert!(order.lock().is_empty());
707 let first = queue.lock().pop_front().unwrap();
708 first();
709 assert_eq!(*order.lock(), vec![1]);
710 let second = queue.lock().pop_front().unwrap();
711 second();
712 assert_eq!(*order.lock(), vec![1, 2]);
713 assert!(dispatcher.is_main_thread());
714
715 dispatcher.shutdown_for_shell();
716 assert_eq!(dispatcher.dispatch(|| {}), Err(DispatchError::Shutdown));
717 }
718
719 #[test]
720 fn startup_and_shutdown_gate_queued_callbacks_deterministically() {
721 for _ in 0..100 {
722 let queue = Arc::new(Mutex::new(VecDeque::<MainThreadTask>::new()));
723 let queue_for_post = Arc::clone(&queue);
724 let dispatcher = MainThreadDispatcher::for_shell_inactive(
725 move |task| {
726 queue_for_post.lock().push_back(task);
727 true
728 },
729 std::thread::current().id(),
730 );
731 assert_eq!(dispatcher.dispatch(|| {}), Err(DispatchError::NotReady));
732 assert!(queue.lock().is_empty());
733 assert!(dispatcher.start_for_shell());
734 assert!(!dispatcher.start_for_shell());
735
736 let ran = Arc::new(AtomicU8::new(0));
737 let ran_in_task = Arc::clone(&ran);
738 dispatcher
739 .dispatch(move || ran_in_task.store(1, Ordering::Release))
740 .unwrap();
741 dispatcher.shutdown_for_shell();
742 queue.lock().pop_front().unwrap()();
743 assert_eq!(ran.load(Ordering::Acquire), 0);
744 assert_eq!(dispatcher.dispatch(|| {}), Err(DispatchError::Shutdown));
745 }
746 }
747}