1use edgefirst_tensor::{DetectBox, ProtoData, Segmentation};
5use std::panic::AssertUnwindSafe;
6use std::ptr::NonNull;
7use std::thread::JoinHandle;
8use tokio::sync::mpsc::{Sender, WeakSender};
9
10use super::processor::GLProcessorST;
11use super::shaders::check_gl_error;
12use super::{EglDisplayKind, Int8InterpolationMode, TransferBackend};
13use crate::{Crop, Error, Flip, ImageProcessorTrait, Rotation};
14use edgefirst_tensor::TensorDyn;
15
16#[allow(clippy::type_complexity)]
17enum GLProcessorMessage {
18 ImageConvert(
19 SendablePtr<TensorDyn>,
20 SendablePtr<TensorDyn>,
21 Rotation,
22 Flip,
23 Crop,
24 bool, tokio::sync::oneshot::Sender<Result<(), Error>>,
26 ),
27 ImageConvertFenced(
31 SendablePtr<TensorDyn>,
32 SendablePtr<TensorDyn>,
33 Rotation,
34 Flip,
35 Crop,
36 tokio::sync::oneshot::Sender<Result<Option<super::CompletionFence>, Error>>,
37 ),
38 Flush(tokio::sync::oneshot::Sender<Result<(), Error>>),
40 SetColors(
41 Vec<[u8; 4]>,
42 tokio::sync::oneshot::Sender<Result<(), Error>>,
43 ),
44 DrawDecodedMasks(
45 SendablePtr<TensorDyn>,
46 SendablePtr<DetectBox>,
47 SendablePtr<Segmentation>,
48 f32, Option<SendablePtr<TensorDyn>>, Option<[f32; 4]>, crate::ColorMode,
52 tokio::sync::oneshot::Sender<Result<(), Error>>,
53 ),
54 DrawProtoMasks(
55 SendablePtr<TensorDyn>,
56 SendablePtr<DetectBox>,
57 SendablePtr<ProtoData>,
58 f32, Option<SendablePtr<TensorDyn>>, Option<[f32; 4]>, crate::ColorMode,
62 tokio::sync::oneshot::Sender<Result<(), Error>>,
63 ),
64 SetInt8Interpolation(
65 Int8InterpolationMode,
66 tokio::sync::oneshot::Sender<Result<(), Error>>,
67 ),
68 SetColorimetryMode(
70 crate::ColorimetryMode,
71 tokio::sync::oneshot::Sender<Result<(), Error>>,
72 ),
73 #[cfg(test)]
75 SetFailDstImport(bool, tokio::sync::oneshot::Sender<Result<(), Error>>),
76 EglCacheStats(tokio::sync::oneshot::Sender<Result<super::cache::GlCacheStats, Error>>),
78 ConvertStats(tokio::sync::oneshot::Sender<Result<super::cache::ConvertStats, Error>>),
80 PboCreate(
81 usize, tokio::sync::oneshot::Sender<Result<u32, Error>>,
83 ),
84 PboMap(
85 u32, usize, tokio::sync::oneshot::Sender<Result<edgefirst_tensor::PboMapping, Error>>,
88 ),
89 PboUnmap(
90 u32, tokio::sync::oneshot::Sender<Result<(), Error>>,
92 ),
93 PboDelete(u32), CudaRegisterBuffer(u32, tokio::sync::oneshot::Sender<Option<usize>>),
95 CudaMap(usize, tokio::sync::oneshot::Sender<Option<(usize, usize)>>),
96 CudaUnmap(usize), CudaUnregister(usize), }
99
100fn pbo_elem_count(
110 width: usize,
111 height: usize,
112 format: edgefirst_tensor::PixelFormat,
113) -> Option<usize> {
114 format
121 .allocation_shape(width, height)?
122 .iter()
123 .try_fold(1usize, |acc, &d| acc.checked_mul(d))
124}
125
126fn pbo_shape(width: usize, height: usize, format: edgefirst_tensor::PixelFormat) -> Vec<usize> {
130 format
131 .allocation_shape(width, height)
132 .unwrap_or_else(|| vec![height, width, format.channels()])
133}
134
135fn register_pbo_cuda<T>(
144 tensor: &mut edgefirst_tensor::Tensor<T>,
145 buffer_id: u32,
146 size: usize,
147 sender: &Sender<GLProcessorMessage>,
148) where
149 T: num_traits::Num + Clone + std::fmt::Debug + Send + Sync + edgefirst_tensor::Element,
150{
151 if !edgefirst_tensor::is_cuda_available() {
152 return;
153 }
154 let (tx, rx) = tokio::sync::oneshot::channel();
155 if sender
156 .blocking_send(GLProcessorMessage::CudaRegisterBuffer(buffer_id, tx))
157 .is_ok()
158 {
159 if let Ok(Some(resource)) = rx.blocking_recv() {
160 let ops = std::sync::Arc::new(GlCudaOps {
161 sender: sender.downgrade(),
162 });
163 tensor.set_cuda_handle(edgefirst_tensor::CudaHandle::new_gl(
164 resource as *mut std::ffi::c_void,
165 size,
166 ops,
167 ));
168 }
169 }
170}
171
172struct GlPboOps {
179 sender: WeakSender<GLProcessorMessage>,
180 worker_id: u64,
185}
186
187static NEXT_GL_WORKER_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
189
190struct GlWorkerState {
192 id: u64,
193 serializes: bool,
196 mapped: std::collections::HashSet<u32>,
198}
199
200thread_local! {
201 static GL_WORKER: std::cell::RefCell<Option<GlWorkerState>> =
217 const { std::cell::RefCell::new(None) };
218}
219
220fn with_worker_mapped<R>(f: impl FnOnce(&mut std::collections::HashSet<u32>) -> R) -> Option<R> {
224 GL_WORKER.with(|c| c.borrow_mut().as_mut().map(|s| f(&mut s.mapped)))
225}
226
227fn on_owning_gl_worker(worker_id: u64) -> bool {
229 GL_WORKER.with(|c| matches!(*c.borrow(), Some(ref s) if s.id == worker_id))
230}
231
232fn on_serializing_gl_worker() -> bool {
235 GL_WORKER.with(|c| matches!(*c.borrow(), Some(ref s) if s.serializes))
236}
237
238fn cross_context_pbo_error(op: &str, buffer_id: u32) -> edgefirst_tensor::Error {
242 edgefirst_tensor::Error::InvalidOperation(format!(
243 "PBO {buffer_id}: cannot {op} a buffer owned by another GL context while \
244 this one holds the serialization lock; convert it through the processor \
245 that created it"
246 ))
247}
248
249thread_local! {
250 static GL_WORKER_POISON: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
253}
254
255fn inline_gl_op<T>(what: &str, f: impl FnOnce() -> Result<T, Error>) -> Result<T, Error> {
263 match std::panic::catch_unwind(AssertUnwindSafe(f)) {
264 Ok(result) => result,
265 Err(e) => {
266 GL_WORKER_POISON.with(|p| p.set(true));
267 Err(Error::Internal(format!(
268 "GL thread panicked during {what}: {}",
269 panic_message(e.as_ref()),
270 )))
271 }
272 }
273}
274
275unsafe impl edgefirst_tensor::PboOps for GlPboOps {
281 fn map_buffer(
282 &self,
283 buffer_id: u32,
284 size: usize,
285 ) -> edgefirst_tensor::Result<edgefirst_tensor::PboMapping> {
286 if on_owning_gl_worker(self.worker_id) {
291 return inline_gl_op("PboMap", || pbo_map_on_gl_thread(buffer_id, size)).map_err(|e| {
292 edgefirst_tensor::Error::NotImplemented(format!("GL PBO map failed: {e:?}"))
293 });
294 }
295 if on_serializing_gl_worker() {
297 return Err(cross_context_pbo_error("map", buffer_id));
298 }
299 let sender = self
300 .sender
301 .upgrade()
302 .ok_or(edgefirst_tensor::Error::PboDisconnected)?;
303 let (tx, rx) = tokio::sync::oneshot::channel();
304 sender
305 .blocking_send(GLProcessorMessage::PboMap(buffer_id, size, tx))
306 .map_err(|_| edgefirst_tensor::Error::PboDisconnected)?;
307 rx.blocking_recv()
308 .map_err(|_| edgefirst_tensor::Error::PboDisconnected)?
309 .map_err(|e| {
310 edgefirst_tensor::Error::NotImplemented(format!("GL PBO map failed: {e:?}"))
311 })
312 }
313
314 fn unmap_buffer(&self, buffer_id: u32) -> edgefirst_tensor::Result<()> {
315 if on_owning_gl_worker(self.worker_id) {
317 return inline_gl_op("PboUnmap", || pbo_unmap_on_gl_thread(buffer_id)).map_err(|e| {
318 edgefirst_tensor::Error::NotImplemented(format!("GL PBO unmap failed: {e:?}"))
319 });
320 }
321 if on_serializing_gl_worker() {
323 return Err(cross_context_pbo_error("unmap", buffer_id));
324 }
325 let sender = self
326 .sender
327 .upgrade()
328 .ok_or(edgefirst_tensor::Error::PboDisconnected)?;
329 let (tx, rx) = tokio::sync::oneshot::channel();
330 sender
331 .blocking_send(GLProcessorMessage::PboUnmap(buffer_id, tx))
332 .map_err(|_| edgefirst_tensor::Error::PboDisconnected)?;
333 rx.blocking_recv()
334 .map_err(|_| edgefirst_tensor::Error::PboDisconnected)?
335 .map_err(|e| {
336 edgefirst_tensor::Error::NotImplemented(format!("GL PBO unmap failed: {e:?}"))
337 })
338 }
339
340 fn delete_buffer(&self, buffer_id: u32) {
341 if on_owning_gl_worker(self.worker_id) {
343 with_worker_mapped(|set| set.remove(&buffer_id));
344 let _ = inline_gl_op("PboDelete", || {
347 unsafe { edgefirst_gl::gl::DeleteBuffers(1, &buffer_id) };
348 Ok(())
349 });
350 return;
351 }
352 if let Some(sender) = self.sender.upgrade() {
353 if on_serializing_gl_worker() {
355 if sender
356 .try_send(GLProcessorMessage::PboDelete(buffer_id))
357 .is_err()
358 {
359 log::warn!(
360 "PBO {buffer_id}: cross-context delete skipped to avoid a deadlock; \
361 the buffer is freed when its context is destroyed"
362 );
363 }
364 return;
365 }
366 let _ = sender.blocking_send(GLProcessorMessage::PboDelete(buffer_id));
367 }
368 }
369}
370
371struct GlCudaOps {
378 sender: WeakSender<GLProcessorMessage>,
379}
380
381impl edgefirst_tensor::CudaGlOps for GlCudaOps {
382 fn map(&self, resource: *mut std::ffi::c_void) -> Option<(*mut std::ffi::c_void, usize)> {
383 let sender = self.sender.upgrade()?;
384 let (tx, rx) = tokio::sync::oneshot::channel();
385 sender
386 .blocking_send(GLProcessorMessage::CudaMap(resource as usize, tx))
387 .ok()?;
388 rx.blocking_recv()
389 .ok()?
390 .map(|(p, l)| (p as *mut std::ffi::c_void, l))
391 }
392
393 fn unmap(&self, resource: *mut std::ffi::c_void) {
394 if let Some(sender) = self.sender.upgrade() {
395 let _ = sender.blocking_send(GLProcessorMessage::CudaUnmap(resource as usize));
396 }
397 }
398
399 fn unregister(&self, resource: *mut std::ffi::c_void) {
400 if let Some(sender) = self.sender.upgrade() {
401 let _ = sender.blocking_send(GLProcessorMessage::CudaUnregister(resource as usize));
402 }
403 }
404}
405
406#[derive(Debug)]
411pub struct GLProcessorThreaded {
412 handle: Option<JoinHandle<()>>,
414
415 sender: Option<Sender<GLProcessorMessage>>,
417 worker_id: u64,
420 caps: super::platform::PlatformCaps,
424}
425
426unsafe impl Send for GLProcessorThreaded {}
427unsafe impl Sync for GLProcessorThreaded {}
428
429struct SendablePtr<T: Send> {
430 ptr: NonNull<T>,
431 len: usize,
432}
433
434unsafe impl<T> Send for SendablePtr<T> where T: Send {}
435
436fn panic_message(info: &(dyn std::any::Any + Send)) -> String {
438 if let Some(s) = info.downcast_ref::<&str>() {
439 s.to_string()
440 } else if let Some(s) = info.downcast_ref::<String>() {
441 s.clone()
442 } else {
443 "unknown panic".to_string()
444 }
445}
446
447fn reply_caught<T>(
448 result: std::thread::Result<crate::Result<T>>,
449 resp: tokio::sync::oneshot::Sender<crate::Result<T>>,
450 poisoned: &mut bool,
451 what: &str,
452) {
453 let _ = resp.send(match result {
454 Ok(res) => res,
455 Err(e) => {
456 *poisoned = true;
457 Err(crate::Error::Internal(format!(
458 "GL thread panicked during {what}: {}",
459 panic_message(e.as_ref()),
460 )))
461 }
462 });
463}
464
465fn reject_poisoned_message(msg: GLProcessorMessage) {
466 let poison_err =
467 crate::Error::Internal("GL context is poisoned after a prior panic".to_string());
468 match msg {
469 GLProcessorMessage::ImageConvertFenced(.., resp) => {
470 let _ = resp.send(Err(poison_err));
471 }
472 GLProcessorMessage::ImageConvert(.., resp) => {
473 let _ = resp.send(Err(poison_err));
474 }
475 GLProcessorMessage::Flush(resp) => {
476 let _ = resp.send(Err(poison_err));
477 }
478 GLProcessorMessage::DrawDecodedMasks(.., resp) => {
479 let _ = resp.send(Err(poison_err));
480 }
481 GLProcessorMessage::DrawProtoMasks(.., resp) => {
482 let _ = resp.send(Err(poison_err));
483 }
484 GLProcessorMessage::SetColors(_, resp) => {
485 let _ = resp.send(Err(poison_err));
486 }
487 GLProcessorMessage::SetInt8Interpolation(_, resp) => {
488 let _ = resp.send(Err(poison_err));
489 }
490 GLProcessorMessage::SetColorimetryMode(_, resp) => {
491 let _ = resp.send(Err(poison_err));
492 }
493 #[cfg(test)]
494 GLProcessorMessage::SetFailDstImport(_, resp) => {
495 let _ = resp.send(Err(poison_err));
496 }
497 GLProcessorMessage::EglCacheStats(resp) => {
498 let _ = resp.send(Err(poison_err));
499 }
500 GLProcessorMessage::ConvertStats(resp) => {
501 let _ = resp.send(Err(poison_err));
502 }
503 GLProcessorMessage::PboCreate(_, resp) => {
504 let _ = resp.send(Err(poison_err));
505 }
506 GLProcessorMessage::PboMap(_, _, resp) => {
507 let _ = resp.send(Err(poison_err));
508 }
509 GLProcessorMessage::PboUnmap(_, resp) => {
510 let _ = resp.send(Err(poison_err));
511 }
512 GLProcessorMessage::PboDelete(_) => {}
513 GLProcessorMessage::CudaRegisterBuffer(_, resp) => {
514 let _ = resp.send(None);
515 }
516 GLProcessorMessage::CudaMap(_, resp) => {
517 let _ = resp.send(None);
518 }
519 GLProcessorMessage::CudaUnmap(_) => {}
523 GLProcessorMessage::CudaUnregister(_) => {}
524 }
525}
526
527fn run_gl_worker(
528 worker_id: u64,
529 kind: Option<EglDisplayKind>,
530 capacity: Option<usize>,
531 mut recv: tokio::sync::mpsc::Receiver<GLProcessorMessage>,
532 create_ctx_send: tokio::sync::oneshot::Sender<Result<super::platform::PlatformCaps, Error>>,
533) {
534 #[cfg(target_os = "linux")]
539 let init_result = {
540 let _guard = super::context::GL_MUTEX
541 .lock()
542 .unwrap_or_else(|e| e.into_inner());
543 GLProcessorST::new(kind, capacity)
544 };
545 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
549 let init_result = GLProcessorST::new(kind, capacity);
550 #[cfg(target_os = "windows")]
558 let init_result = {
559 let _guard = NON_LINUX_GL_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
560 GLProcessorST::new(kind, capacity)
561 };
562 let mut gl_converter = match init_result {
563 Ok(gl) => gl,
564 Err(e) => {
565 let _ = create_ctx_send.send(Err(e));
566 return;
567 }
568 };
569 let caps = gl_converter.platform_caps();
572 let _ = create_ctx_send.send(Ok(caps));
573 let mut poisoned = false;
574 GL_WORKER.with(|c| {
596 *c.borrow_mut() = Some(GlWorkerState {
597 id: worker_id,
598 serializes: false,
600 mapped: std::collections::HashSet::new(),
601 });
602 });
603 struct GlWorkerSlot;
606 impl Drop for GlWorkerSlot {
607 fn drop(&mut self) {
608 GL_WORKER.with(|c| *c.borrow_mut() = None);
609 GL_WORKER_POISON.with(|p| p.set(false));
610 }
611 }
612 let _gl_worker_slot = GlWorkerSlot;
613 let serialize_per_msg = match std::env::var("EDGEFIRST_GL_SERIALIZE").as_deref() {
622 Ok("full") => true,
623 Ok("lifecycle") => {
624 #[cfg(target_os = "windows")]
629 log::warn!(
630 "EDGEFIRST_GL_SERIALIZE=lifecycle drops the per-message lock the ANGLE/D3D11 context re-sync relies on; concurrent GL across processors is not supported on this backend"
631 );
632 false
633 }
634 _ => caps.serialize_gl,
635 };
636 log::debug!(
637 "GL serialization policy: {}",
638 if serialize_per_msg {
639 "Full (per-message global lock)"
640 } else {
641 "LifecycleOnly (parallel across processors)"
642 }
643 );
644 GL_WORKER.with(|c| {
645 if let Some(state) = c.borrow_mut().as_mut() {
646 state.serializes = serialize_per_msg;
647 }
648 });
649 while let Some(msg) = recv.blocking_recv() {
650 #[cfg(target_os = "linux")]
659 let _guard = serialize_per_msg.then(|| {
660 super::context::GL_MUTEX
661 .lock()
662 .unwrap_or_else(|e| e.into_inner())
663 });
664 #[cfg(any(
665 target_os = "macos",
666 target_os = "ios",
667 target_os = "android",
668 target_os = "windows"
669 ))]
670 let _guard =
671 serialize_per_msg.then(|| NON_LINUX_GL_MUTEX.lock().unwrap_or_else(|e| e.into_inner()));
672
673 if poisoned {
678 reject_poisoned_message(msg);
679 continue;
680 }
681
682 gl_converter.begin_gpu_pass();
686 handle_gl_message(msg, &mut gl_converter, &mut poisoned);
687 poisoned |= GL_WORKER_POISON.with(|p| p.replace(false));
691 gl_converter.end_gpu_pass_if_synced();
695 }
696 #[cfg(target_os = "linux")]
700 let _guard = super::context::GL_MUTEX
701 .lock()
702 .unwrap_or_else(|e| e.into_inner());
703 #[cfg(target_os = "windows")]
704 let _guard = NON_LINUX_GL_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
705 drop(gl_converter);
706}
707
708#[cfg(not(target_os = "linux"))]
712static NON_LINUX_GL_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
713
714#[cfg(target_os = "windows")]
725pub(super) fn lifecycle_guard() -> std::sync::MutexGuard<'static, ()> {
726 NON_LINUX_GL_MUTEX.lock().unwrap_or_else(|e| e.into_inner())
727}
728
729fn handle_gl_message(
730 msg: GLProcessorMessage,
731 gl_converter: &mut GLProcessorST,
732 poisoned: &mut bool,
733) {
734 match msg {
735 GLProcessorMessage::ImageConvert(src, dst, rotation, flip, crop, defer, resp) => {
736 handle_image_convert(
737 src,
738 dst,
739 (rotation, flip, crop, defer),
740 resp,
741 gl_converter,
742 poisoned,
743 );
744 }
745 GLProcessorMessage::ImageConvertFenced(src, dst, rotation, flip, crop, resp) => {
746 handle_image_convert_fenced(
747 src,
748 dst,
749 (rotation, flip, crop),
750 resp,
751 gl_converter,
752 poisoned,
753 );
754 }
755 GLProcessorMessage::Flush(resp) => {
756 let result = std::panic::catch_unwind(AssertUnwindSafe(|| gl_converter.flush()));
757 reply_caught(result, resp, poisoned, "Flush");
758 }
759 GLProcessorMessage::DrawDecodedMasks(
760 dst,
761 det,
762 seg,
763 opacity,
764 bg,
765 letterbox,
766 color_mode,
767 resp,
768 ) => {
769 handle_draw_decoded_masks(
770 dst,
771 det,
772 seg,
773 (opacity, bg, letterbox, color_mode),
774 resp,
775 gl_converter,
776 poisoned,
777 );
778 }
779 GLProcessorMessage::DrawProtoMasks(
780 dst,
781 det,
782 proto_data,
783 opacity,
784 bg,
785 letterbox,
786 color_mode,
787 resp,
788 ) => {
789 handle_draw_proto_masks(
790 dst,
791 det,
792 proto_data,
793 (opacity, bg, letterbox, color_mode),
794 resp,
795 gl_converter,
796 poisoned,
797 );
798 }
799 GLProcessorMessage::SetColors(colors, resp) => {
800 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
801 gl_converter.set_class_colors(&colors)
802 }));
803 reply_caught(result, resp, poisoned, "SetColors");
804 }
805 GLProcessorMessage::SetColorimetryMode(mode, resp) => {
806 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
807 gl_converter.set_colorimetry_mode(mode);
808 Ok(())
809 }));
810 reply_caught(result, resp, poisoned, "SetColorimetryMode");
811 }
812 GLProcessorMessage::SetInt8Interpolation(mode, resp) => {
813 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
814 gl_converter.set_int8_interpolation_mode(mode);
815 Ok(())
816 }));
817 reply_caught(result, resp, poisoned, "SetInt8Interpolation");
818 }
819 #[cfg(test)]
820 GLProcessorMessage::SetFailDstImport(fail, resp) => {
821 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
822 gl_converter.fail_dst_import = fail;
823 Ok(())
824 }));
825 reply_caught(result, resp, poisoned, "SetFailDstImport");
826 }
827 GLProcessorMessage::EglCacheStats(resp) => {
828 let result =
829 std::panic::catch_unwind(AssertUnwindSafe(|| Ok(gl_converter.egl_cache_stats())));
830 reply_caught(result, resp, poisoned, "EglCacheStats");
831 }
832 GLProcessorMessage::ConvertStats(resp) => {
833 let result =
834 std::panic::catch_unwind(AssertUnwindSafe(|| Ok(gl_converter.convert_stats())));
835 reply_caught(result, resp, poisoned, "ConvertStats");
836 }
837 GLProcessorMessage::PboCreate(size, resp) => {
838 handle_pbo_create(size, resp, poisoned);
839 }
840 GLProcessorMessage::PboMap(buffer_id, size, resp) => {
841 handle_pbo_map(buffer_id, size, resp, poisoned);
842 }
843 GLProcessorMessage::PboUnmap(buffer_id, resp) => {
844 handle_pbo_unmap(buffer_id, resp, poisoned);
845 }
846 GLProcessorMessage::PboDelete(buffer_id) => {
847 handle_pbo_delete(buffer_id, poisoned);
848 }
849 GLProcessorMessage::CudaRegisterBuffer(buffer_id, resp) => {
850 let _ = resp.send(edgefirst_tensor::gl_register_buffer(buffer_id));
852 }
853 GLProcessorMessage::CudaMap(resource, resp) => {
854 gl_converter.flush_pending();
861 let _ = resp.send(edgefirst_tensor::gl_map_resource(resource));
862 }
863 GLProcessorMessage::CudaUnmap(resource) => {
864 edgefirst_tensor::gl_unmap_resource(resource);
865 }
866 GLProcessorMessage::CudaUnregister(resource) => {
867 edgefirst_tensor::gl_unregister_resource(resource);
868 }
869 }
870}
871
872fn handle_image_convert(
873 src: SendablePtr<TensorDyn>,
874 mut dst: SendablePtr<TensorDyn>,
875 geom: (Rotation, Flip, Crop, bool),
876 resp: tokio::sync::oneshot::Sender<Result<(), Error>>,
877 gl_converter: &mut GLProcessorST,
878 poisoned: &mut bool,
879) {
880 let (rotation, flip, crop, defer) = geom;
881 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
884 let src = unsafe { src.ptr.as_ref() };
885 let dst = unsafe { dst.ptr.as_mut() };
886 if defer {
890 gl_converter.convert_deferred(src, dst, rotation, flip, crop)
891 } else {
892 gl_converter.convert(src, dst, rotation, flip, crop)
893 }
894 }));
895 reply_caught(result, resp, poisoned, "ImageConvert");
896}
897
898fn handle_image_convert_fenced(
899 src: SendablePtr<TensorDyn>,
900 mut dst: SendablePtr<TensorDyn>,
901 geom: (Rotation, Flip, Crop),
902 resp: tokio::sync::oneshot::Sender<Result<Option<super::CompletionFence>, Error>>,
903 gl_converter: &mut GLProcessorST,
904 poisoned: &mut bool,
905) {
906 let (rotation, flip, crop) = geom;
907 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
910 let src = unsafe { src.ptr.as_ref() };
911 let dst = unsafe { dst.ptr.as_mut() };
912 gl_converter.convert_fenced(src, dst, rotation, flip, crop)
913 }));
914 reply_caught(result, resp, poisoned, "ImageConvertFenced");
915}
916
917fn handle_draw_decoded_masks(
918 mut dst: SendablePtr<TensorDyn>,
919 det: SendablePtr<DetectBox>,
920 seg: SendablePtr<Segmentation>,
921 overlay: (
922 f32,
923 Option<SendablePtr<TensorDyn>>,
924 Option<[f32; 4]>,
925 crate::ColorMode,
926 ),
927 resp: tokio::sync::oneshot::Sender<Result<(), Error>>,
928 gl_converter: &mut GLProcessorST,
929 poisoned: &mut bool,
930) {
931 let (opacity, bg, letterbox, color_mode) = overlay;
932 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
936 let dst = unsafe { dst.ptr.as_mut() };
937 let det = unsafe { std::slice::from_raw_parts(det.ptr.as_ptr(), det.len) };
938 let seg = unsafe { std::slice::from_raw_parts(seg.ptr.as_ptr(), seg.len) };
939 let bg_ref = bg.map(|p| unsafe { &*p.ptr.as_ptr() });
940 gl_converter.draw_decoded_masks(
941 dst,
942 det,
943 seg,
944 crate::MaskOverlay {
945 background: bg_ref,
946 opacity,
947 letterbox,
948 color_mode,
949 },
950 )
951 }));
952 reply_caught(result, resp, poisoned, "DrawDecodedMasks");
953}
954
955fn handle_draw_proto_masks(
956 mut dst: SendablePtr<TensorDyn>,
957 det: SendablePtr<DetectBox>,
958 proto_data: SendablePtr<ProtoData>,
959 overlay: (
960 f32,
961 Option<SendablePtr<TensorDyn>>,
962 Option<[f32; 4]>,
963 crate::ColorMode,
964 ),
965 resp: tokio::sync::oneshot::Sender<Result<(), Error>>,
966 gl_converter: &mut GLProcessorST,
967 poisoned: &mut bool,
968) {
969 let (opacity, bg, letterbox, color_mode) = overlay;
970 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
973 let dst = unsafe { dst.ptr.as_mut() };
974 let det = unsafe { std::slice::from_raw_parts(det.ptr.as_ptr(), det.len) };
975 let bg_ref = bg.map(|p| unsafe { &*p.ptr.as_ptr() });
976 let proto_data = unsafe { proto_data.ptr.as_ref() };
977 gl_converter.draw_proto_masks(
978 dst,
979 det,
980 proto_data,
981 crate::MaskOverlay {
982 background: bg_ref,
983 opacity,
984 letterbox,
985 color_mode,
986 },
987 )
988 }));
989 reply_caught(result, resp, poisoned, "DrawProtoMasks");
990}
991
992fn handle_pbo_create(
993 size: usize,
994 resp: tokio::sync::oneshot::Sender<Result<u32, Error>>,
995 poisoned: &mut bool,
996) {
997 let result = std::panic::catch_unwind(AssertUnwindSafe(|| unsafe {
998 let mut id: u32 = 0;
999 edgefirst_gl::gl::GenBuffers(1, &mut id);
1000 edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, id);
1001 edgefirst_gl::gl::BufferData(
1002 edgefirst_gl::gl::PIXEL_PACK_BUFFER,
1003 size as isize,
1004 std::ptr::null(),
1005 edgefirst_gl::gl::STREAM_COPY,
1006 );
1007 edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, 0);
1008 match check_gl_error("PboCreate", 0) {
1009 Ok(()) => Ok(id),
1010 Err(e) => {
1011 edgefirst_gl::gl::DeleteBuffers(1, &id);
1012 Err(e)
1013 }
1014 }
1015 }));
1016 reply_caught(result, resp, poisoned, "PboCreate");
1017}
1018
1019fn pbo_map_on_gl_thread(
1023 buffer_id: u32,
1024 size: usize,
1025) -> Result<edgefirst_tensor::PboMapping, Error> {
1026 let already = with_worker_mapped(|set| !set.insert(buffer_id)).unwrap_or(false);
1027 if already {
1028 return Err(Error::OpenGl(format!(
1029 "PBO buffer {buffer_id} is already mapped in this GL context \
1030 (a producer and a reconstructed cross-package consumer may \
1031 both hold a live tensor over it)"
1032 )));
1033 }
1034 let result = std::panic::catch_unwind(AssertUnwindSafe(|| unsafe {
1035 edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, buffer_id);
1036 let ptr = edgefirst_gl::gl::MapBufferRange(
1037 edgefirst_gl::gl::PIXEL_PACK_BUFFER,
1038 0,
1039 size as isize,
1040 edgefirst_gl::gl::MAP_READ_BIT | edgefirst_gl::gl::MAP_WRITE_BIT,
1041 );
1042 edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, 0);
1043 if ptr.is_null() {
1044 Err(Error::OpenGl("glMapBufferRange returned null".to_string()))
1045 } else {
1046 Ok(edgefirst_tensor::PboMapping {
1047 ptr: ptr as *mut u8,
1048 size,
1049 })
1050 }
1051 }));
1052 if !matches!(result, Ok(Ok(_))) {
1053 with_worker_mapped(|set| set.remove(&buffer_id));
1056 }
1057 match result {
1058 Ok(result) => result,
1059 Err(panic) => std::panic::resume_unwind(panic),
1062 }
1063}
1064
1065fn pbo_unmap_on_gl_thread(buffer_id: u32) -> Result<(), Error> {
1067 with_worker_mapped(|set| set.remove(&buffer_id));
1070 unsafe {
1071 edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, buffer_id);
1072 let ok = edgefirst_gl::gl::UnmapBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER);
1073 edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, 0);
1074 if ok == edgefirst_gl::gl::FALSE {
1075 Err(Error::OpenGl(
1076 "PBO data was corrupted during mapping".into(),
1077 ))
1078 } else {
1079 check_gl_error("PboUnmap", 0)
1080 }
1081 }
1082}
1083
1084fn handle_pbo_map(
1085 buffer_id: u32,
1086 size: usize,
1087 resp: tokio::sync::oneshot::Sender<Result<edgefirst_tensor::PboMapping, Error>>,
1088 poisoned: &mut bool,
1089) {
1090 let result =
1091 std::panic::catch_unwind(AssertUnwindSafe(|| pbo_map_on_gl_thread(buffer_id, size)));
1092 reply_caught(result, resp, poisoned, "PboMap");
1093}
1094
1095fn handle_pbo_unmap(
1096 buffer_id: u32,
1097 resp: tokio::sync::oneshot::Sender<Result<(), Error>>,
1098 poisoned: &mut bool,
1099) {
1100 let result = std::panic::catch_unwind(AssertUnwindSafe(|| pbo_unmap_on_gl_thread(buffer_id)));
1101 reply_caught(result, resp, poisoned, "PboUnmap");
1102}
1103
1104fn handle_pbo_delete(buffer_id: u32, poisoned: &mut bool) {
1105 with_worker_mapped(|set| set.remove(&buffer_id));
1112 if let Err(e) = std::panic::catch_unwind(AssertUnwindSafe(|| unsafe {
1113 edgefirst_gl::gl::DeleteBuffers(1, &buffer_id);
1114 })) {
1115 *poisoned = true;
1116 log::error!(
1117 "GL thread panicked during PboDelete: {}",
1118 panic_message(e.as_ref()),
1119 );
1120 }
1121}
1122
1123impl GLProcessorThreaded {
1124 pub fn new(kind: Option<EglDisplayKind>) -> Result<Self, Error> {
1127 Self::with_cache_capacity(kind, None)
1128 }
1129
1130 pub fn with_cache_capacity(
1136 kind: Option<EglDisplayKind>,
1137 capacity: Option<usize>,
1138 ) -> Result<Self, Error> {
1139 let worker_id = NEXT_GL_WORKER_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1140 let (send, recv) = tokio::sync::mpsc::channel::<GLProcessorMessage>(1);
1141
1142 let (create_ctx_send, create_ctx_recv) = tokio::sync::oneshot::channel();
1143
1144 let handle = std::thread::spawn(move || {
1145 run_gl_worker(worker_id, kind, capacity, recv, create_ctx_send);
1146 });
1147
1148 let caps = match create_ctx_recv.blocking_recv() {
1149 Ok(Err(e)) => return Err(e),
1150 Err(_) => {
1151 return Err(Error::Internal(
1152 "GL converter error messaging closed without update".to_string(),
1153 ));
1154 }
1155 Ok(Ok(caps)) => caps,
1156 };
1157
1158 Ok(Self {
1159 handle: Some(handle),
1160 sender: Some(send),
1161 worker_id,
1162 caps,
1163 })
1164 }
1165}
1166
1167impl ImageProcessorTrait for GLProcessorThreaded {
1168 fn convert(
1169 &mut self,
1170 src: &TensorDyn,
1171 dst: &mut TensorDyn,
1172 rotation: crate::Rotation,
1173 flip: Flip,
1174 crop: Crop,
1175 ) -> crate::Result<()> {
1176 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1177 self.sender
1178 .as_ref()
1179 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1180 .blocking_send(GLProcessorMessage::ImageConvert(
1181 SendablePtr {
1182 ptr: NonNull::from(src),
1183 len: 1,
1184 },
1185 SendablePtr {
1186 ptr: NonNull::from(dst),
1187 len: 1,
1188 },
1189 rotation,
1190 flip,
1191 crop,
1192 false, err_send,
1194 ))
1195 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1196 err_recv.blocking_recv().map_err(|_| {
1197 Error::Internal("GL converter error messaging closed without update".to_string())
1198 })?
1199 }
1200
1201 fn convert_deferred(
1202 &mut self,
1203 src: &TensorDyn,
1204 dst: &mut TensorDyn,
1205 rotation: crate::Rotation,
1206 flip: Flip,
1207 crop: Crop,
1208 ) -> crate::Result<()> {
1209 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1210 self.sender
1211 .as_ref()
1212 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1213 .blocking_send(GLProcessorMessage::ImageConvert(
1214 SendablePtr {
1215 ptr: NonNull::from(src),
1216 len: 1,
1217 },
1218 SendablePtr {
1219 ptr: NonNull::from(dst),
1220 len: 1,
1221 },
1222 rotation,
1223 flip,
1224 crop,
1225 true, err_send,
1227 ))
1228 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1229 err_recv.blocking_recv().map_err(|_| {
1230 Error::Internal("GL converter error messaging closed without update".to_string())
1231 })?
1232 }
1233
1234 fn flush(&mut self) -> crate::Result<()> {
1235 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1236 self.sender
1237 .as_ref()
1238 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1239 .blocking_send(GLProcessorMessage::Flush(err_send))
1240 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1241 err_recv.blocking_recv().map_err(|_| {
1242 Error::Internal("GL converter error messaging closed without update".to_string())
1243 })?
1244 }
1245
1246 fn draw_decoded_masks(
1247 &mut self,
1248 dst: &mut TensorDyn,
1249 detect: &[crate::DetectBox],
1250 segmentation: &[crate::Segmentation],
1251 overlay: crate::MaskOverlay<'_>,
1252 ) -> crate::Result<()> {
1253 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1254 self.sender
1255 .as_ref()
1256 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1257 .blocking_send(GLProcessorMessage::DrawDecodedMasks(
1258 SendablePtr {
1259 ptr: NonNull::from(dst),
1260 len: 1,
1261 },
1262 SendablePtr {
1263 ptr: NonNull::new(detect.as_ptr() as *mut DetectBox).unwrap(),
1264 len: detect.len(),
1265 },
1266 SendablePtr {
1267 ptr: NonNull::new(segmentation.as_ptr() as *mut Segmentation).unwrap(),
1268 len: segmentation.len(),
1269 },
1270 overlay.opacity,
1271 overlay.background.map(|bg| SendablePtr {
1272 ptr: NonNull::from(bg).cast::<TensorDyn>(),
1273 len: 1,
1274 }),
1275 overlay.letterbox,
1276 overlay.color_mode,
1277 err_send,
1278 ))
1279 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1280 err_recv.blocking_recv().map_err(|_| {
1281 Error::Internal("GL converter error messaging closed without update".to_string())
1282 })?
1283 }
1284
1285 fn draw_proto_masks(
1286 &mut self,
1287 dst: &mut TensorDyn,
1288 detect: &[DetectBox],
1289 proto_data: &ProtoData,
1290 overlay: crate::MaskOverlay<'_>,
1291 ) -> crate::Result<()> {
1292 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1293 self.sender
1294 .as_ref()
1295 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1296 .blocking_send(GLProcessorMessage::DrawProtoMasks(
1297 SendablePtr {
1298 ptr: NonNull::from(dst),
1299 len: 1,
1300 },
1301 SendablePtr {
1302 ptr: NonNull::new(detect.as_ptr() as *mut DetectBox).unwrap(),
1303 len: detect.len(),
1304 },
1305 SendablePtr {
1306 ptr: NonNull::from(proto_data).cast::<ProtoData>(),
1307 len: 1,
1308 },
1309 overlay.opacity,
1310 overlay.background.map(|bg| SendablePtr {
1311 ptr: NonNull::from(bg).cast::<TensorDyn>(),
1312 len: 1,
1313 }),
1314 overlay.letterbox,
1315 overlay.color_mode,
1316 err_send,
1317 ))
1318 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1319 err_recv.blocking_recv().map_err(|_| {
1320 Error::Internal("GL converter error messaging closed without update".to_string())
1321 })?
1322 }
1323
1324 fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<(), crate::Error> {
1325 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1326 self.sender
1327 .as_ref()
1328 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1329 .blocking_send(GLProcessorMessage::SetColors(colors.to_vec(), err_send))
1330 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1331 err_recv.blocking_recv().map_err(|_| {
1332 Error::Internal("GL converter error messaging closed without update".to_string())
1333 })?
1334 }
1335}
1336
1337impl GLProcessorThreaded {
1338 pub fn set_colorimetry_mode(&mut self, mode: crate::ColorimetryMode) -> Result<(), Error> {
1343 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1344 self.sender
1345 .as_ref()
1346 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1347 .blocking_send(GLProcessorMessage::SetColorimetryMode(mode, err_send))
1348 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1349 err_recv.blocking_recv().map_err(|_| {
1350 Error::Internal("GL converter error messaging closed without update".to_string())
1351 })?
1352 }
1353
1354 #[cfg(test)]
1360 pub(crate) fn set_fail_dst_import(&mut self, fail: bool) -> Result<(), Error> {
1361 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1362 self.sender
1363 .as_ref()
1364 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1365 .blocking_send(GLProcessorMessage::SetFailDstImport(fail, err_send))
1366 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1367 err_recv.blocking_recv().map_err(|_| {
1368 Error::Internal("GL converter error messaging closed without update".to_string())
1369 })?
1370 }
1371
1372 pub fn set_int8_interpolation_mode(
1374 &mut self,
1375 mode: Int8InterpolationMode,
1376 ) -> Result<(), crate::Error> {
1377 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1378 self.sender
1379 .as_ref()
1380 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1381 .blocking_send(GLProcessorMessage::SetInt8Interpolation(mode, err_send))
1382 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1383 err_recv.blocking_recv().map_err(|_| {
1384 Error::Internal("GL converter error messaging closed without update".to_string())
1385 })?
1386 }
1387
1388 pub fn egl_cache_stats(&self) -> Result<super::cache::GlCacheStats, Error> {
1395 let (send, recv) = tokio::sync::oneshot::channel();
1396 self.sender
1397 .as_ref()
1398 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1399 .blocking_send(GLProcessorMessage::EglCacheStats(send))
1400 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1401 recv.blocking_recv().map_err(|_| {
1402 Error::Internal("GL converter error messaging closed without update".to_string())
1403 })?
1404 }
1405
1406 pub fn convert_with_fence(
1417 &mut self,
1418 src: &TensorDyn,
1419 dst: &mut TensorDyn,
1420 rotation: crate::Rotation,
1421 flip: Flip,
1422 crop: Crop,
1423 ) -> Result<Option<super::CompletionFence>, Error> {
1424 use crate::ImageProcessorTrait as _;
1425 if !self.caps.native_fence_sync {
1426 self.convert(src, dst, rotation, flip, crop)?;
1429 return Ok(None);
1430 }
1431 let (send, recv) = tokio::sync::oneshot::channel();
1432 self.sender
1433 .as_ref()
1434 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1435 .blocking_send(GLProcessorMessage::ImageConvertFenced(
1436 SendablePtr {
1437 ptr: NonNull::from(src),
1438 len: 1,
1439 },
1440 SendablePtr {
1441 ptr: NonNull::from(dst),
1442 len: 1,
1443 },
1444 rotation,
1445 flip,
1446 crop,
1447 send,
1448 ))
1449 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1450 recv.blocking_recv().map_err(|_| {
1451 Error::Internal("GL converter error messaging closed without update".to_string())
1452 })?
1453 }
1454
1455 pub fn convert_stats(&self) -> Result<super::cache::ConvertStats, Error> {
1460 let (send, recv) = tokio::sync::oneshot::channel();
1461 self.sender
1462 .as_ref()
1463 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1464 .blocking_send(GLProcessorMessage::ConvertStats(send))
1465 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1466 recv.blocking_recv().map_err(|_| {
1467 Error::Internal("GL converter error messaging closed without update".to_string())
1468 })?
1469 }
1470
1471 pub fn create_pbo_image(
1473 &self,
1474 width: usize,
1475 height: usize,
1476 format: edgefirst_tensor::PixelFormat,
1477 ) -> Result<edgefirst_tensor::Tensor<u8>, Error> {
1478 let sender = self
1479 .sender
1480 .as_ref()
1481 .ok_or(Error::OpenGl("GL processor is shutting down".to_string()))?;
1482
1483 let size = pbo_elem_count(width, height, format)
1484 .filter(|&s| s != 0)
1485 .ok_or_else(|| Error::OpenGl("Invalid image dimensions".to_string()))?;
1486
1487 let (tx, rx) = tokio::sync::oneshot::channel();
1489 sender
1490 .blocking_send(GLProcessorMessage::PboCreate(size, tx))
1491 .map_err(|_| Error::OpenGl("GL thread channel closed".to_string()))?;
1492 let buffer_id = rx
1493 .blocking_recv()
1494 .map_err(|_| Error::OpenGl("GL thread did not respond".to_string()))??;
1495
1496 let ops: std::sync::Arc<dyn edgefirst_tensor::PboOps> = std::sync::Arc::new(GlPboOps {
1497 sender: sender.downgrade(),
1498 worker_id: self.worker_id,
1499 });
1500
1501 let shape = pbo_shape(width, height, format);
1502
1503 let pbo_tensor =
1504 edgefirst_tensor::PboTensor::<u8>::from_pbo(buffer_id, size, &shape, None, ops)
1505 .map_err(|e| Error::OpenGl(format!("PBO tensor creation failed: {e:?}")))?;
1506 let mut tensor = edgefirst_tensor::Tensor::from_pbo(pbo_tensor)
1507 .map_err(|e| Error::OpenGl(format!("PBO tensor wrap failed: {e:?}")))?;
1508 tensor
1509 .set_format(format)
1510 .map_err(|e| Error::OpenGl(format!("Failed to set format on PBO tensor: {e:?}")))?;
1511 register_pbo_cuda(&mut tensor, buffer_id, size, sender);
1518 Ok(tensor)
1519 }
1520
1521 pub(crate) fn create_pbo_image_dtype(
1527 &self,
1528 width: usize,
1529 height: usize,
1530 format: edgefirst_tensor::PixelFormat,
1531 dtype: edgefirst_tensor::DType,
1532 ) -> Result<TensorDyn, Error> {
1533 let sender = self
1534 .sender
1535 .as_ref()
1536 .ok_or(Error::OpenGl("GL processor is shutting down".to_string()))?;
1537
1538 let elems = pbo_elem_count(width, height, format)
1539 .filter(|&e| e != 0)
1540 .ok_or_else(|| Error::OpenGl("Invalid image dimensions".to_string()))?;
1541
1542 let size = elems
1543 .checked_mul(dtype.size())
1544 .ok_or_else(|| Error::OpenGl("PBO size overflow".to_string()))?;
1545
1546 let (tx, rx) = tokio::sync::oneshot::channel();
1548 sender
1549 .blocking_send(GLProcessorMessage::PboCreate(size, tx))
1550 .map_err(|_| Error::OpenGl("GL thread channel closed".to_string()))?;
1551 let buffer_id = rx
1552 .blocking_recv()
1553 .map_err(|_| Error::OpenGl("GL thread did not respond".to_string()))??;
1554
1555 let ops: std::sync::Arc<dyn edgefirst_tensor::PboOps> = std::sync::Arc::new(GlPboOps {
1556 sender: sender.downgrade(),
1557 worker_id: self.worker_id,
1558 });
1559
1560 let shape = pbo_shape(width, height, format);
1561
1562 let map_err = |e: edgefirst_tensor::Error| {
1563 Error::OpenGl(format!("PBO tensor creation failed: {e:?}"))
1564 };
1565 let set_err = |e: edgefirst_tensor::Error| {
1566 Error::OpenGl(format!("Failed to set format on PBO tensor: {e:?}"))
1567 };
1568
1569 match dtype {
1570 edgefirst_tensor::DType::U8 => {
1571 let pbo =
1572 edgefirst_tensor::PboTensor::<u8>::from_pbo(buffer_id, size, &shape, None, ops)
1573 .map_err(map_err)?;
1574 let mut t = edgefirst_tensor::Tensor::from_pbo(pbo).map_err(map_err)?;
1575 t.set_format(format).map_err(set_err)?;
1576 register_pbo_cuda(&mut t, buffer_id, size, sender);
1577 Ok(TensorDyn::from(t))
1578 }
1579 edgefirst_tensor::DType::F16 => {
1580 let pbo = edgefirst_tensor::PboTensor::<edgefirst_tensor::f16>::from_pbo(
1581 buffer_id, size, &shape, None, ops,
1582 )
1583 .map_err(map_err)?;
1584 let mut t = edgefirst_tensor::Tensor::from_pbo(pbo).map_err(map_err)?;
1585 t.set_format(format).map_err(set_err)?;
1586 register_pbo_cuda(&mut t, buffer_id, size, sender);
1587 Ok(TensorDyn::from(t))
1588 }
1589 edgefirst_tensor::DType::F32 => {
1590 let pbo = edgefirst_tensor::PboTensor::<f32>::from_pbo(
1591 buffer_id, size, &shape, None, ops,
1592 )
1593 .map_err(map_err)?;
1594 let mut t = edgefirst_tensor::Tensor::from_pbo(pbo).map_err(map_err)?;
1595 t.set_format(format).map_err(set_err)?;
1596 register_pbo_cuda(&mut t, buffer_id, size, sender);
1597 Ok(TensorDyn::from(t))
1598 }
1599 other => Err(Error::OpenGl(format!("unsupported PBO dtype {other:?}"))),
1600 }
1601 }
1602
1603 pub(crate) fn transfer_backend(&self) -> TransferBackend {
1605 self.caps.transfer_backend
1606 }
1607
1608 pub(crate) fn supported_render_dtypes(&self) -> crate::RenderDtypeSupport {
1615 self.caps.render_dtypes
1616 }
1617}
1618
1619impl Drop for GLProcessorThreaded {
1620 fn drop(&mut self) {
1621 drop(self.sender.take());
1627 let _ = self.handle.take().and_then(|h| h.join().ok());
1628 }
1629}
1630
1631#[cfg(test)]
1635#[cfg_attr(coverage_nightly, coverage(off))]
1636mod tests {
1637 use super::{pbo_elem_count, pbo_shape, GLProcessorThreaded, GlPboOps};
1638 use edgefirst_tensor::{PixelFormat, TensorTrait};
1639
1640 #[test]
1641 fn elem_count_per_format() {
1642 assert_eq!(pbo_elem_count(8, 4, PixelFormat::Rgba), Some(8 * 4 * 4));
1644 assert_eq!(pbo_elem_count(8, 4, PixelFormat::Rgb), Some(8 * 4 * 3));
1646 assert_eq!(pbo_elem_count(8, 4, PixelFormat::Nv12), Some(8 * 4 * 3 / 2));
1648 assert_eq!(pbo_elem_count(8, 4, PixelFormat::Nv16), Some(8 * 4 * 2));
1650 }
1651
1652 #[test]
1653 fn elem_count_overflow_is_none() {
1654 assert_eq!(pbo_elem_count(usize::MAX, 2, PixelFormat::Rgba), None);
1656 assert_eq!(pbo_elem_count(usize::MAX, 1, PixelFormat::Rgb), None);
1658 }
1659
1660 #[test]
1661 fn shape_per_format() {
1662 assert_eq!(pbo_shape(8, 4, PixelFormat::PlanarRgb), vec![3, 4, 8]);
1664 assert_eq!(pbo_shape(8, 4, PixelFormat::Nv12), vec![4 * 3 / 2, 8]);
1666 assert_eq!(pbo_shape(8, 4, PixelFormat::Nv16), vec![4 * 2, 8]);
1668 assert_eq!(pbo_shape(8, 4, PixelFormat::Rgba), vec![4, 8, 4]);
1670 }
1671
1672 #[test]
1690 #[cfg(any(target_os = "linux", target_os = "windows"))] fn concurrent_maps_from_two_independent_handles_on_one_buffer_do_not_corrupt_gl_state() {
1692 let gl = match GLProcessorThreaded::new(None) {
1693 Ok(gl) => gl,
1694 Err(e) => {
1695 crate::test_support::report_skip(&format!(
1696 "concurrent_maps_from_two_independent_handles... - \
1697 OpenGL not available: {e:?}"
1698 ));
1699 return;
1700 }
1701 };
1702 let tensor_a = match gl.create_pbo_image(64, 64, PixelFormat::Rgba) {
1703 Ok(t) => t,
1704 Err(e) => {
1705 crate::test_support::report_skip(&format!(
1706 "concurrent_maps_from_two_independent_handles... - \
1707 PBO not supported: {e:?}"
1708 ));
1709 return;
1710 }
1711 };
1712 let buffer_id = tensor_a.pbo_id().expect("just created a PBO tensor");
1713 let size = tensor_a.capacity_bytes();
1714 let shape = tensor_a.shape().to_vec();
1715
1716 let ops_b: std::sync::Arc<dyn edgefirst_tensor::PboOps> = std::sync::Arc::new(GlPboOps {
1720 sender: gl.sender.as_ref().unwrap().downgrade(),
1721 worker_id: gl.worker_id,
1722 });
1723 let tensor_b =
1724 edgefirst_tensor::PboTensor::<u8>::from_pbo(buffer_id, size, &shape, None, ops_b)
1725 .expect("from_pbo against a real, already-created buffer_id must succeed");
1726
1727 let map_a = tensor_a.map().expect("first map must succeed");
1729
1730 let map_b_result = tensor_b.map();
1734 let Err(map_b_err) = map_b_result else {
1742 panic!(
1743 "a second, independent handle mapping the SAME real buffer while the \
1744 first mapping is still open must be refused, not silently succeed"
1745 );
1746 };
1747 let msg = format!("{map_b_err:?}");
1748 assert!(
1749 msg.contains("already mapped"),
1750 "must be refused specifically by the GL-thread-level reservation check \
1751 (mapped_pbo_buffers), not some other error path: {msg}"
1752 );
1753
1754 drop(map_a);
1755
1756 let map_b_after = tensor_b.map();
1760 assert!(
1761 map_b_after.is_ok(),
1762 "after the first handle's mapping releases, the second handle must be able to \
1763 map the same buffer: {map_b_after:?}"
1764 );
1765 }
1766}