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}
181
182unsafe impl edgefirst_tensor::PboOps for GlPboOps {
187 fn map_buffer(
188 &self,
189 buffer_id: u32,
190 size: usize,
191 ) -> edgefirst_tensor::Result<edgefirst_tensor::PboMapping> {
192 let sender = self
193 .sender
194 .upgrade()
195 .ok_or(edgefirst_tensor::Error::PboDisconnected)?;
196 let (tx, rx) = tokio::sync::oneshot::channel();
197 sender
198 .blocking_send(GLProcessorMessage::PboMap(buffer_id, size, tx))
199 .map_err(|_| edgefirst_tensor::Error::PboDisconnected)?;
200 rx.blocking_recv()
201 .map_err(|_| edgefirst_tensor::Error::PboDisconnected)?
202 .map_err(|e| {
203 edgefirst_tensor::Error::NotImplemented(format!("GL PBO map failed: {e:?}"))
204 })
205 }
206
207 fn unmap_buffer(&self, buffer_id: u32) -> edgefirst_tensor::Result<()> {
208 let sender = self
209 .sender
210 .upgrade()
211 .ok_or(edgefirst_tensor::Error::PboDisconnected)?;
212 let (tx, rx) = tokio::sync::oneshot::channel();
213 sender
214 .blocking_send(GLProcessorMessage::PboUnmap(buffer_id, tx))
215 .map_err(|_| edgefirst_tensor::Error::PboDisconnected)?;
216 rx.blocking_recv()
217 .map_err(|_| edgefirst_tensor::Error::PboDisconnected)?
218 .map_err(|e| {
219 edgefirst_tensor::Error::NotImplemented(format!("GL PBO unmap failed: {e:?}"))
220 })
221 }
222
223 fn delete_buffer(&self, buffer_id: u32) {
224 if let Some(sender) = self.sender.upgrade() {
225 let _ = sender.blocking_send(GLProcessorMessage::PboDelete(buffer_id));
226 }
227 }
228}
229
230struct GlCudaOps {
237 sender: WeakSender<GLProcessorMessage>,
238}
239
240impl edgefirst_tensor::CudaGlOps for GlCudaOps {
241 fn map(&self, resource: *mut std::ffi::c_void) -> Option<(*mut std::ffi::c_void, usize)> {
242 let sender = self.sender.upgrade()?;
243 let (tx, rx) = tokio::sync::oneshot::channel();
244 sender
245 .blocking_send(GLProcessorMessage::CudaMap(resource as usize, tx))
246 .ok()?;
247 rx.blocking_recv()
248 .ok()?
249 .map(|(p, l)| (p as *mut std::ffi::c_void, l))
250 }
251
252 fn unmap(&self, resource: *mut std::ffi::c_void) {
253 if let Some(sender) = self.sender.upgrade() {
254 let _ = sender.blocking_send(GLProcessorMessage::CudaUnmap(resource as usize));
255 }
256 }
257
258 fn unregister(&self, resource: *mut std::ffi::c_void) {
259 if let Some(sender) = self.sender.upgrade() {
260 let _ = sender.blocking_send(GLProcessorMessage::CudaUnregister(resource as usize));
261 }
262 }
263}
264
265#[derive(Debug)]
270pub struct GLProcessorThreaded {
271 handle: Option<JoinHandle<()>>,
273
274 sender: Option<Sender<GLProcessorMessage>>,
276 caps: super::platform::PlatformCaps,
280}
281
282unsafe impl Send for GLProcessorThreaded {}
283unsafe impl Sync for GLProcessorThreaded {}
284
285struct SendablePtr<T: Send> {
286 ptr: NonNull<T>,
287 len: usize,
288}
289
290unsafe impl<T> Send for SendablePtr<T> where T: Send {}
291
292fn panic_message(info: &(dyn std::any::Any + Send)) -> String {
294 if let Some(s) = info.downcast_ref::<&str>() {
295 s.to_string()
296 } else if let Some(s) = info.downcast_ref::<String>() {
297 s.clone()
298 } else {
299 "unknown panic".to_string()
300 }
301}
302
303fn reply_caught<T>(
304 result: std::thread::Result<crate::Result<T>>,
305 resp: tokio::sync::oneshot::Sender<crate::Result<T>>,
306 poisoned: &mut bool,
307 what: &str,
308) {
309 let _ = resp.send(match result {
310 Ok(res) => res,
311 Err(e) => {
312 *poisoned = true;
313 Err(crate::Error::Internal(format!(
314 "GL thread panicked during {what}: {}",
315 panic_message(e.as_ref()),
316 )))
317 }
318 });
319}
320
321fn reject_poisoned_message(msg: GLProcessorMessage) {
322 let poison_err =
323 crate::Error::Internal("GL context is poisoned after a prior panic".to_string());
324 match msg {
325 GLProcessorMessage::ImageConvertFenced(.., resp) => {
326 let _ = resp.send(Err(poison_err));
327 }
328 GLProcessorMessage::ImageConvert(.., resp) => {
329 let _ = resp.send(Err(poison_err));
330 }
331 GLProcessorMessage::Flush(resp) => {
332 let _ = resp.send(Err(poison_err));
333 }
334 GLProcessorMessage::DrawDecodedMasks(.., resp) => {
335 let _ = resp.send(Err(poison_err));
336 }
337 GLProcessorMessage::DrawProtoMasks(.., resp) => {
338 let _ = resp.send(Err(poison_err));
339 }
340 GLProcessorMessage::SetColors(_, resp) => {
341 let _ = resp.send(Err(poison_err));
342 }
343 GLProcessorMessage::SetInt8Interpolation(_, resp) => {
344 let _ = resp.send(Err(poison_err));
345 }
346 GLProcessorMessage::SetColorimetryMode(_, resp) => {
347 let _ = resp.send(Err(poison_err));
348 }
349 #[cfg(test)]
350 GLProcessorMessage::SetFailDstImport(_, resp) => {
351 let _ = resp.send(Err(poison_err));
352 }
353 GLProcessorMessage::EglCacheStats(resp) => {
354 let _ = resp.send(Err(poison_err));
355 }
356 GLProcessorMessage::ConvertStats(resp) => {
357 let _ = resp.send(Err(poison_err));
358 }
359 GLProcessorMessage::PboCreate(_, resp) => {
360 let _ = resp.send(Err(poison_err));
361 }
362 GLProcessorMessage::PboMap(_, _, resp) => {
363 let _ = resp.send(Err(poison_err));
364 }
365 GLProcessorMessage::PboUnmap(_, resp) => {
366 let _ = resp.send(Err(poison_err));
367 }
368 GLProcessorMessage::PboDelete(_) => {}
369 GLProcessorMessage::CudaRegisterBuffer(_, resp) => {
370 let _ = resp.send(None);
371 }
372 GLProcessorMessage::CudaMap(_, resp) => {
373 let _ = resp.send(None);
374 }
375 GLProcessorMessage::CudaUnmap(_) => {}
379 GLProcessorMessage::CudaUnregister(_) => {}
380 }
381}
382
383fn run_gl_worker(
384 kind: Option<EglDisplayKind>,
385 capacity: Option<usize>,
386 mut recv: tokio::sync::mpsc::Receiver<GLProcessorMessage>,
387 create_ctx_send: tokio::sync::oneshot::Sender<Result<super::platform::PlatformCaps, Error>>,
388) {
389 #[cfg(target_os = "linux")]
394 let init_result = {
395 let _guard = super::context::GL_MUTEX
396 .lock()
397 .unwrap_or_else(|e| e.into_inner());
398 GLProcessorST::new(kind, capacity)
399 };
400 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
404 let init_result = GLProcessorST::new(kind, capacity);
405 #[cfg(target_os = "windows")]
413 let init_result = {
414 let _guard = NON_LINUX_GL_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
415 GLProcessorST::new(kind, capacity)
416 };
417 let mut gl_converter = match init_result {
418 Ok(gl) => gl,
419 Err(e) => {
420 let _ = create_ctx_send.send(Err(e));
421 return;
422 }
423 };
424 let caps = gl_converter.platform_caps();
427 let _ = create_ctx_send.send(Ok(caps));
428 let mut poisoned = false;
429 let mut mapped_pbo_buffers: std::collections::HashSet<u32> = std::collections::HashSet::new();
448 let serialize_per_msg = match std::env::var("EDGEFIRST_GL_SERIALIZE").as_deref() {
457 Ok("full") => true,
458 Ok("lifecycle") => {
459 #[cfg(target_os = "windows")]
464 log::warn!(
465 "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"
466 );
467 false
468 }
469 _ => caps.serialize_gl,
470 };
471 log::debug!(
472 "GL serialization policy: {}",
473 if serialize_per_msg {
474 "Full (per-message global lock)"
475 } else {
476 "LifecycleOnly (parallel across processors)"
477 }
478 );
479 while let Some(msg) = recv.blocking_recv() {
480 #[cfg(target_os = "linux")]
489 let _guard = serialize_per_msg.then(|| {
490 super::context::GL_MUTEX
491 .lock()
492 .unwrap_or_else(|e| e.into_inner())
493 });
494 #[cfg(any(
495 target_os = "macos",
496 target_os = "ios",
497 target_os = "android",
498 target_os = "windows"
499 ))]
500 let _guard =
501 serialize_per_msg.then(|| NON_LINUX_GL_MUTEX.lock().unwrap_or_else(|e| e.into_inner()));
502
503 if poisoned {
508 reject_poisoned_message(msg);
509 continue;
510 }
511
512 gl_converter.begin_gpu_pass();
516 handle_gl_message(
517 msg,
518 &mut gl_converter,
519 &mut mapped_pbo_buffers,
520 &mut poisoned,
521 );
522 gl_converter.end_gpu_pass_if_synced();
526 }
527 #[cfg(target_os = "linux")]
531 let _guard = super::context::GL_MUTEX
532 .lock()
533 .unwrap_or_else(|e| e.into_inner());
534 #[cfg(target_os = "windows")]
535 let _guard = NON_LINUX_GL_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
536 drop(gl_converter);
537}
538
539#[cfg(not(target_os = "linux"))]
543static NON_LINUX_GL_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
544
545#[cfg(target_os = "windows")]
556pub(super) fn lifecycle_guard() -> std::sync::MutexGuard<'static, ()> {
557 NON_LINUX_GL_MUTEX.lock().unwrap_or_else(|e| e.into_inner())
558}
559
560fn handle_gl_message(
561 msg: GLProcessorMessage,
562 gl_converter: &mut GLProcessorST,
563 mapped_pbo_buffers: &mut std::collections::HashSet<u32>,
564 poisoned: &mut bool,
565) {
566 match msg {
567 GLProcessorMessage::ImageConvert(src, dst, rotation, flip, crop, defer, resp) => {
568 handle_image_convert(
569 src,
570 dst,
571 (rotation, flip, crop, defer),
572 resp,
573 gl_converter,
574 poisoned,
575 );
576 }
577 GLProcessorMessage::ImageConvertFenced(src, dst, rotation, flip, crop, resp) => {
578 handle_image_convert_fenced(
579 src,
580 dst,
581 (rotation, flip, crop),
582 resp,
583 gl_converter,
584 poisoned,
585 );
586 }
587 GLProcessorMessage::Flush(resp) => {
588 let result = std::panic::catch_unwind(AssertUnwindSafe(|| gl_converter.flush()));
589 reply_caught(result, resp, poisoned, "Flush");
590 }
591 GLProcessorMessage::DrawDecodedMasks(
592 dst,
593 det,
594 seg,
595 opacity,
596 bg,
597 letterbox,
598 color_mode,
599 resp,
600 ) => {
601 handle_draw_decoded_masks(
602 dst,
603 det,
604 seg,
605 (opacity, bg, letterbox, color_mode),
606 resp,
607 gl_converter,
608 poisoned,
609 );
610 }
611 GLProcessorMessage::DrawProtoMasks(
612 dst,
613 det,
614 proto_data,
615 opacity,
616 bg,
617 letterbox,
618 color_mode,
619 resp,
620 ) => {
621 handle_draw_proto_masks(
622 dst,
623 det,
624 proto_data,
625 (opacity, bg, letterbox, color_mode),
626 resp,
627 gl_converter,
628 poisoned,
629 );
630 }
631 GLProcessorMessage::SetColors(colors, resp) => {
632 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
633 gl_converter.set_class_colors(&colors)
634 }));
635 reply_caught(result, resp, poisoned, "SetColors");
636 }
637 GLProcessorMessage::SetColorimetryMode(mode, resp) => {
638 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
639 gl_converter.set_colorimetry_mode(mode);
640 Ok(())
641 }));
642 reply_caught(result, resp, poisoned, "SetColorimetryMode");
643 }
644 GLProcessorMessage::SetInt8Interpolation(mode, resp) => {
645 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
646 gl_converter.set_int8_interpolation_mode(mode);
647 Ok(())
648 }));
649 reply_caught(result, resp, poisoned, "SetInt8Interpolation");
650 }
651 #[cfg(test)]
652 GLProcessorMessage::SetFailDstImport(fail, resp) => {
653 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
654 gl_converter.fail_dst_import = fail;
655 Ok(())
656 }));
657 reply_caught(result, resp, poisoned, "SetFailDstImport");
658 }
659 GLProcessorMessage::EglCacheStats(resp) => {
660 let result =
661 std::panic::catch_unwind(AssertUnwindSafe(|| Ok(gl_converter.egl_cache_stats())));
662 reply_caught(result, resp, poisoned, "EglCacheStats");
663 }
664 GLProcessorMessage::ConvertStats(resp) => {
665 let result =
666 std::panic::catch_unwind(AssertUnwindSafe(|| Ok(gl_converter.convert_stats())));
667 reply_caught(result, resp, poisoned, "ConvertStats");
668 }
669 GLProcessorMessage::PboCreate(size, resp) => {
670 handle_pbo_create(size, resp, poisoned);
671 }
672 GLProcessorMessage::PboMap(buffer_id, size, resp) => {
673 handle_pbo_map(buffer_id, size, resp, mapped_pbo_buffers, poisoned);
674 }
675 GLProcessorMessage::PboUnmap(buffer_id, resp) => {
676 handle_pbo_unmap(buffer_id, resp, mapped_pbo_buffers, poisoned);
677 }
678 GLProcessorMessage::PboDelete(buffer_id) => {
679 handle_pbo_delete(buffer_id, mapped_pbo_buffers, poisoned);
680 }
681 GLProcessorMessage::CudaRegisterBuffer(buffer_id, resp) => {
682 let _ = resp.send(edgefirst_tensor::gl_register_buffer(buffer_id));
684 }
685 GLProcessorMessage::CudaMap(resource, resp) => {
686 gl_converter.flush_pending();
693 let _ = resp.send(edgefirst_tensor::gl_map_resource(resource));
694 }
695 GLProcessorMessage::CudaUnmap(resource) => {
696 edgefirst_tensor::gl_unmap_resource(resource);
697 }
698 GLProcessorMessage::CudaUnregister(resource) => {
699 edgefirst_tensor::gl_unregister_resource(resource);
700 }
701 }
702}
703
704fn handle_image_convert(
705 src: SendablePtr<TensorDyn>,
706 mut dst: SendablePtr<TensorDyn>,
707 geom: (Rotation, Flip, Crop, bool),
708 resp: tokio::sync::oneshot::Sender<Result<(), Error>>,
709 gl_converter: &mut GLProcessorST,
710 poisoned: &mut bool,
711) {
712 let (rotation, flip, crop, defer) = geom;
713 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
716 let src = unsafe { src.ptr.as_ref() };
717 let dst = unsafe { dst.ptr.as_mut() };
718 if defer {
722 gl_converter.convert_deferred(src, dst, rotation, flip, crop)
723 } else {
724 gl_converter.convert(src, dst, rotation, flip, crop)
725 }
726 }));
727 reply_caught(result, resp, poisoned, "ImageConvert");
728}
729
730fn handle_image_convert_fenced(
731 src: SendablePtr<TensorDyn>,
732 mut dst: SendablePtr<TensorDyn>,
733 geom: (Rotation, Flip, Crop),
734 resp: tokio::sync::oneshot::Sender<Result<Option<super::CompletionFence>, Error>>,
735 gl_converter: &mut GLProcessorST,
736 poisoned: &mut bool,
737) {
738 let (rotation, flip, crop) = geom;
739 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
742 let src = unsafe { src.ptr.as_ref() };
743 let dst = unsafe { dst.ptr.as_mut() };
744 gl_converter.convert_fenced(src, dst, rotation, flip, crop)
745 }));
746 reply_caught(result, resp, poisoned, "ImageConvertFenced");
747}
748
749fn handle_draw_decoded_masks(
750 mut dst: SendablePtr<TensorDyn>,
751 det: SendablePtr<DetectBox>,
752 seg: SendablePtr<Segmentation>,
753 overlay: (
754 f32,
755 Option<SendablePtr<TensorDyn>>,
756 Option<[f32; 4]>,
757 crate::ColorMode,
758 ),
759 resp: tokio::sync::oneshot::Sender<Result<(), Error>>,
760 gl_converter: &mut GLProcessorST,
761 poisoned: &mut bool,
762) {
763 let (opacity, bg, letterbox, color_mode) = overlay;
764 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
768 let dst = unsafe { dst.ptr.as_mut() };
769 let det = unsafe { std::slice::from_raw_parts(det.ptr.as_ptr(), det.len) };
770 let seg = unsafe { std::slice::from_raw_parts(seg.ptr.as_ptr(), seg.len) };
771 let bg_ref = bg.map(|p| unsafe { &*p.ptr.as_ptr() });
772 gl_converter.draw_decoded_masks(
773 dst,
774 det,
775 seg,
776 crate::MaskOverlay {
777 background: bg_ref,
778 opacity,
779 letterbox,
780 color_mode,
781 },
782 )
783 }));
784 reply_caught(result, resp, poisoned, "DrawDecodedMasks");
785}
786
787fn handle_draw_proto_masks(
788 mut dst: SendablePtr<TensorDyn>,
789 det: SendablePtr<DetectBox>,
790 proto_data: SendablePtr<ProtoData>,
791 overlay: (
792 f32,
793 Option<SendablePtr<TensorDyn>>,
794 Option<[f32; 4]>,
795 crate::ColorMode,
796 ),
797 resp: tokio::sync::oneshot::Sender<Result<(), Error>>,
798 gl_converter: &mut GLProcessorST,
799 poisoned: &mut bool,
800) {
801 let (opacity, bg, letterbox, color_mode) = overlay;
802 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
805 let dst = unsafe { dst.ptr.as_mut() };
806 let det = unsafe { std::slice::from_raw_parts(det.ptr.as_ptr(), det.len) };
807 let bg_ref = bg.map(|p| unsafe { &*p.ptr.as_ptr() });
808 let proto_data = unsafe { proto_data.ptr.as_ref() };
809 gl_converter.draw_proto_masks(
810 dst,
811 det,
812 proto_data,
813 crate::MaskOverlay {
814 background: bg_ref,
815 opacity,
816 letterbox,
817 color_mode,
818 },
819 )
820 }));
821 reply_caught(result, resp, poisoned, "DrawProtoMasks");
822}
823
824fn handle_pbo_create(
825 size: usize,
826 resp: tokio::sync::oneshot::Sender<Result<u32, Error>>,
827 poisoned: &mut bool,
828) {
829 let result = std::panic::catch_unwind(AssertUnwindSafe(|| unsafe {
830 let mut id: u32 = 0;
831 edgefirst_gl::gl::GenBuffers(1, &mut id);
832 edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, id);
833 edgefirst_gl::gl::BufferData(
834 edgefirst_gl::gl::PIXEL_PACK_BUFFER,
835 size as isize,
836 std::ptr::null(),
837 edgefirst_gl::gl::STREAM_COPY,
838 );
839 edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, 0);
840 match check_gl_error("PboCreate", 0) {
841 Ok(()) => Ok(id),
842 Err(e) => {
843 edgefirst_gl::gl::DeleteBuffers(1, &id);
844 Err(e)
845 }
846 }
847 }));
848 reply_caught(result, resp, poisoned, "PboCreate");
849}
850
851fn handle_pbo_map(
852 buffer_id: u32,
853 size: usize,
854 resp: tokio::sync::oneshot::Sender<Result<edgefirst_tensor::PboMapping, Error>>,
855 mapped_pbo_buffers: &mut std::collections::HashSet<u32>,
856 poisoned: &mut bool,
857) {
858 if !mapped_pbo_buffers.insert(buffer_id) {
859 let _ = resp.send(Err(crate::Error::OpenGl(format!(
865 "PBO buffer {buffer_id} is already mapped in this GL context \
866 (a producer and a reconstructed cross-package consumer may \
867 both hold a live tensor over it)"
868 ))));
869 return;
870 }
871 let result = std::panic::catch_unwind(AssertUnwindSafe(|| unsafe {
872 edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, buffer_id);
873 let ptr = edgefirst_gl::gl::MapBufferRange(
874 edgefirst_gl::gl::PIXEL_PACK_BUFFER,
875 0,
876 size as isize,
877 edgefirst_gl::gl::MAP_READ_BIT | edgefirst_gl::gl::MAP_WRITE_BIT,
878 );
879 edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, 0);
880 if ptr.is_null() {
881 Err(crate::Error::OpenGl(
882 "glMapBufferRange returned null".to_string(),
883 ))
884 } else {
885 Ok(edgefirst_tensor::PboMapping {
886 ptr: ptr as *mut u8,
887 size,
888 })
889 }
890 }));
891 let mapped_ok = matches!(result, Ok(Ok(_)));
892 if !mapped_ok {
893 mapped_pbo_buffers.remove(&buffer_id);
898 }
899 reply_caught(result, resp, poisoned, "PboMap");
900}
901
902fn handle_pbo_unmap(
903 buffer_id: u32,
904 resp: tokio::sync::oneshot::Sender<Result<(), Error>>,
905 mapped_pbo_buffers: &mut std::collections::HashSet<u32>,
906 poisoned: &mut bool,
907) {
908 mapped_pbo_buffers.remove(&buffer_id);
915 let result = std::panic::catch_unwind(AssertUnwindSafe(|| unsafe {
916 edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, buffer_id);
917 let ok = edgefirst_gl::gl::UnmapBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER);
918 edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, 0);
919 if ok == edgefirst_gl::gl::FALSE {
920 Err(Error::OpenGl(
921 "PBO data was corrupted during mapping".into(),
922 ))
923 } else {
924 check_gl_error("PboUnmap", 0)
925 }
926 }));
927 reply_caught(result, resp, poisoned, "PboUnmap");
928}
929
930fn handle_pbo_delete(
931 buffer_id: u32,
932 mapped_pbo_buffers: &mut std::collections::HashSet<u32>,
933 poisoned: &mut bool,
934) {
935 mapped_pbo_buffers.remove(&buffer_id);
942 if let Err(e) = std::panic::catch_unwind(AssertUnwindSafe(|| unsafe {
943 edgefirst_gl::gl::DeleteBuffers(1, &buffer_id);
944 })) {
945 *poisoned = true;
946 log::error!(
947 "GL thread panicked during PboDelete: {}",
948 panic_message(e.as_ref()),
949 );
950 }
951}
952
953impl GLProcessorThreaded {
954 pub fn new(kind: Option<EglDisplayKind>) -> Result<Self, Error> {
957 Self::with_cache_capacity(kind, None)
958 }
959
960 pub fn with_cache_capacity(
966 kind: Option<EglDisplayKind>,
967 capacity: Option<usize>,
968 ) -> Result<Self, Error> {
969 let (send, recv) = tokio::sync::mpsc::channel::<GLProcessorMessage>(1);
970
971 let (create_ctx_send, create_ctx_recv) = tokio::sync::oneshot::channel();
972
973 let handle = std::thread::spawn(move || {
974 run_gl_worker(kind, capacity, recv, create_ctx_send);
975 });
976
977 let caps = match create_ctx_recv.blocking_recv() {
978 Ok(Err(e)) => return Err(e),
979 Err(_) => {
980 return Err(Error::Internal(
981 "GL converter error messaging closed without update".to_string(),
982 ));
983 }
984 Ok(Ok(caps)) => caps,
985 };
986
987 Ok(Self {
988 handle: Some(handle),
989 sender: Some(send),
990 caps,
991 })
992 }
993}
994
995impl ImageProcessorTrait for GLProcessorThreaded {
996 fn convert(
997 &mut self,
998 src: &TensorDyn,
999 dst: &mut TensorDyn,
1000 rotation: crate::Rotation,
1001 flip: Flip,
1002 crop: Crop,
1003 ) -> crate::Result<()> {
1004 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1005 self.sender
1006 .as_ref()
1007 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1008 .blocking_send(GLProcessorMessage::ImageConvert(
1009 SendablePtr {
1010 ptr: NonNull::from(src),
1011 len: 1,
1012 },
1013 SendablePtr {
1014 ptr: NonNull::from(dst),
1015 len: 1,
1016 },
1017 rotation,
1018 flip,
1019 crop,
1020 false, err_send,
1022 ))
1023 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1024 err_recv.blocking_recv().map_err(|_| {
1025 Error::Internal("GL converter error messaging closed without update".to_string())
1026 })?
1027 }
1028
1029 fn convert_deferred(
1030 &mut self,
1031 src: &TensorDyn,
1032 dst: &mut TensorDyn,
1033 rotation: crate::Rotation,
1034 flip: Flip,
1035 crop: Crop,
1036 ) -> crate::Result<()> {
1037 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1038 self.sender
1039 .as_ref()
1040 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1041 .blocking_send(GLProcessorMessage::ImageConvert(
1042 SendablePtr {
1043 ptr: NonNull::from(src),
1044 len: 1,
1045 },
1046 SendablePtr {
1047 ptr: NonNull::from(dst),
1048 len: 1,
1049 },
1050 rotation,
1051 flip,
1052 crop,
1053 true, err_send,
1055 ))
1056 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1057 err_recv.blocking_recv().map_err(|_| {
1058 Error::Internal("GL converter error messaging closed without update".to_string())
1059 })?
1060 }
1061
1062 fn flush(&mut self) -> crate::Result<()> {
1063 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1064 self.sender
1065 .as_ref()
1066 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1067 .blocking_send(GLProcessorMessage::Flush(err_send))
1068 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1069 err_recv.blocking_recv().map_err(|_| {
1070 Error::Internal("GL converter error messaging closed without update".to_string())
1071 })?
1072 }
1073
1074 fn draw_decoded_masks(
1075 &mut self,
1076 dst: &mut TensorDyn,
1077 detect: &[crate::DetectBox],
1078 segmentation: &[crate::Segmentation],
1079 overlay: crate::MaskOverlay<'_>,
1080 ) -> crate::Result<()> {
1081 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1082 self.sender
1083 .as_ref()
1084 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1085 .blocking_send(GLProcessorMessage::DrawDecodedMasks(
1086 SendablePtr {
1087 ptr: NonNull::from(dst),
1088 len: 1,
1089 },
1090 SendablePtr {
1091 ptr: NonNull::new(detect.as_ptr() as *mut DetectBox).unwrap(),
1092 len: detect.len(),
1093 },
1094 SendablePtr {
1095 ptr: NonNull::new(segmentation.as_ptr() as *mut Segmentation).unwrap(),
1096 len: segmentation.len(),
1097 },
1098 overlay.opacity,
1099 overlay.background.map(|bg| SendablePtr {
1100 ptr: NonNull::from(bg).cast::<TensorDyn>(),
1101 len: 1,
1102 }),
1103 overlay.letterbox,
1104 overlay.color_mode,
1105 err_send,
1106 ))
1107 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1108 err_recv.blocking_recv().map_err(|_| {
1109 Error::Internal("GL converter error messaging closed without update".to_string())
1110 })?
1111 }
1112
1113 fn draw_proto_masks(
1114 &mut self,
1115 dst: &mut TensorDyn,
1116 detect: &[DetectBox],
1117 proto_data: &ProtoData,
1118 overlay: crate::MaskOverlay<'_>,
1119 ) -> crate::Result<()> {
1120 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1121 self.sender
1122 .as_ref()
1123 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1124 .blocking_send(GLProcessorMessage::DrawProtoMasks(
1125 SendablePtr {
1126 ptr: NonNull::from(dst),
1127 len: 1,
1128 },
1129 SendablePtr {
1130 ptr: NonNull::new(detect.as_ptr() as *mut DetectBox).unwrap(),
1131 len: detect.len(),
1132 },
1133 SendablePtr {
1134 ptr: NonNull::from(proto_data).cast::<ProtoData>(),
1135 len: 1,
1136 },
1137 overlay.opacity,
1138 overlay.background.map(|bg| SendablePtr {
1139 ptr: NonNull::from(bg).cast::<TensorDyn>(),
1140 len: 1,
1141 }),
1142 overlay.letterbox,
1143 overlay.color_mode,
1144 err_send,
1145 ))
1146 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1147 err_recv.blocking_recv().map_err(|_| {
1148 Error::Internal("GL converter error messaging closed without update".to_string())
1149 })?
1150 }
1151
1152 fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<(), crate::Error> {
1153 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1154 self.sender
1155 .as_ref()
1156 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1157 .blocking_send(GLProcessorMessage::SetColors(colors.to_vec(), err_send))
1158 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1159 err_recv.blocking_recv().map_err(|_| {
1160 Error::Internal("GL converter error messaging closed without update".to_string())
1161 })?
1162 }
1163}
1164
1165impl GLProcessorThreaded {
1166 pub fn set_colorimetry_mode(&mut self, mode: crate::ColorimetryMode) -> Result<(), Error> {
1171 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1172 self.sender
1173 .as_ref()
1174 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1175 .blocking_send(GLProcessorMessage::SetColorimetryMode(mode, err_send))
1176 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1177 err_recv.blocking_recv().map_err(|_| {
1178 Error::Internal("GL converter error messaging closed without update".to_string())
1179 })?
1180 }
1181
1182 #[cfg(test)]
1188 pub(crate) fn set_fail_dst_import(&mut self, fail: bool) -> Result<(), Error> {
1189 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1190 self.sender
1191 .as_ref()
1192 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1193 .blocking_send(GLProcessorMessage::SetFailDstImport(fail, err_send))
1194 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1195 err_recv.blocking_recv().map_err(|_| {
1196 Error::Internal("GL converter error messaging closed without update".to_string())
1197 })?
1198 }
1199
1200 pub fn set_int8_interpolation_mode(
1202 &mut self,
1203 mode: Int8InterpolationMode,
1204 ) -> Result<(), crate::Error> {
1205 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1206 self.sender
1207 .as_ref()
1208 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1209 .blocking_send(GLProcessorMessage::SetInt8Interpolation(mode, err_send))
1210 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1211 err_recv.blocking_recv().map_err(|_| {
1212 Error::Internal("GL converter error messaging closed without update".to_string())
1213 })?
1214 }
1215
1216 pub fn egl_cache_stats(&self) -> Result<super::cache::GlCacheStats, Error> {
1223 let (send, recv) = tokio::sync::oneshot::channel();
1224 self.sender
1225 .as_ref()
1226 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1227 .blocking_send(GLProcessorMessage::EglCacheStats(send))
1228 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1229 recv.blocking_recv().map_err(|_| {
1230 Error::Internal("GL converter error messaging closed without update".to_string())
1231 })?
1232 }
1233
1234 pub fn convert_with_fence(
1245 &mut self,
1246 src: &TensorDyn,
1247 dst: &mut TensorDyn,
1248 rotation: crate::Rotation,
1249 flip: Flip,
1250 crop: Crop,
1251 ) -> Result<Option<super::CompletionFence>, Error> {
1252 use crate::ImageProcessorTrait as _;
1253 if !self.caps.native_fence_sync {
1254 self.convert(src, dst, rotation, flip, crop)?;
1257 return Ok(None);
1258 }
1259 let (send, recv) = tokio::sync::oneshot::channel();
1260 self.sender
1261 .as_ref()
1262 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1263 .blocking_send(GLProcessorMessage::ImageConvertFenced(
1264 SendablePtr {
1265 ptr: NonNull::from(src),
1266 len: 1,
1267 },
1268 SendablePtr {
1269 ptr: NonNull::from(dst),
1270 len: 1,
1271 },
1272 rotation,
1273 flip,
1274 crop,
1275 send,
1276 ))
1277 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1278 recv.blocking_recv().map_err(|_| {
1279 Error::Internal("GL converter error messaging closed without update".to_string())
1280 })?
1281 }
1282
1283 pub fn convert_stats(&self) -> Result<super::cache::ConvertStats, Error> {
1288 let (send, recv) = tokio::sync::oneshot::channel();
1289 self.sender
1290 .as_ref()
1291 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1292 .blocking_send(GLProcessorMessage::ConvertStats(send))
1293 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1294 recv.blocking_recv().map_err(|_| {
1295 Error::Internal("GL converter error messaging closed without update".to_string())
1296 })?
1297 }
1298
1299 pub fn create_pbo_image(
1301 &self,
1302 width: usize,
1303 height: usize,
1304 format: edgefirst_tensor::PixelFormat,
1305 ) -> Result<edgefirst_tensor::Tensor<u8>, Error> {
1306 let sender = self
1307 .sender
1308 .as_ref()
1309 .ok_or(Error::OpenGl("GL processor is shutting down".to_string()))?;
1310
1311 let size = pbo_elem_count(width, height, format)
1312 .filter(|&s| s != 0)
1313 .ok_or_else(|| Error::OpenGl("Invalid image dimensions".to_string()))?;
1314
1315 let (tx, rx) = tokio::sync::oneshot::channel();
1317 sender
1318 .blocking_send(GLProcessorMessage::PboCreate(size, tx))
1319 .map_err(|_| Error::OpenGl("GL thread channel closed".to_string()))?;
1320 let buffer_id = rx
1321 .blocking_recv()
1322 .map_err(|_| Error::OpenGl("GL thread did not respond".to_string()))??;
1323
1324 let ops: std::sync::Arc<dyn edgefirst_tensor::PboOps> = std::sync::Arc::new(GlPboOps {
1325 sender: sender.downgrade(),
1326 });
1327
1328 let shape = pbo_shape(width, height, format);
1329
1330 let pbo_tensor =
1331 edgefirst_tensor::PboTensor::<u8>::from_pbo(buffer_id, size, &shape, None, ops)
1332 .map_err(|e| Error::OpenGl(format!("PBO tensor creation failed: {e:?}")))?;
1333 let mut tensor = edgefirst_tensor::Tensor::from_pbo(pbo_tensor)
1334 .map_err(|e| Error::OpenGl(format!("PBO tensor wrap failed: {e:?}")))?;
1335 tensor
1336 .set_format(format)
1337 .map_err(|e| Error::OpenGl(format!("Failed to set format on PBO tensor: {e:?}")))?;
1338 register_pbo_cuda(&mut tensor, buffer_id, size, sender);
1345 Ok(tensor)
1346 }
1347
1348 pub(crate) fn create_pbo_image_dtype(
1354 &self,
1355 width: usize,
1356 height: usize,
1357 format: edgefirst_tensor::PixelFormat,
1358 dtype: edgefirst_tensor::DType,
1359 ) -> Result<TensorDyn, Error> {
1360 let sender = self
1361 .sender
1362 .as_ref()
1363 .ok_or(Error::OpenGl("GL processor is shutting down".to_string()))?;
1364
1365 let elems = pbo_elem_count(width, height, format)
1366 .filter(|&e| e != 0)
1367 .ok_or_else(|| Error::OpenGl("Invalid image dimensions".to_string()))?;
1368
1369 let size = elems
1370 .checked_mul(dtype.size())
1371 .ok_or_else(|| Error::OpenGl("PBO size overflow".to_string()))?;
1372
1373 let (tx, rx) = tokio::sync::oneshot::channel();
1375 sender
1376 .blocking_send(GLProcessorMessage::PboCreate(size, tx))
1377 .map_err(|_| Error::OpenGl("GL thread channel closed".to_string()))?;
1378 let buffer_id = rx
1379 .blocking_recv()
1380 .map_err(|_| Error::OpenGl("GL thread did not respond".to_string()))??;
1381
1382 let ops: std::sync::Arc<dyn edgefirst_tensor::PboOps> = std::sync::Arc::new(GlPboOps {
1383 sender: sender.downgrade(),
1384 });
1385
1386 let shape = pbo_shape(width, height, format);
1387
1388 let map_err = |e: edgefirst_tensor::Error| {
1389 Error::OpenGl(format!("PBO tensor creation failed: {e:?}"))
1390 };
1391 let set_err = |e: edgefirst_tensor::Error| {
1392 Error::OpenGl(format!("Failed to set format on PBO tensor: {e:?}"))
1393 };
1394
1395 match dtype {
1396 edgefirst_tensor::DType::U8 => {
1397 let pbo =
1398 edgefirst_tensor::PboTensor::<u8>::from_pbo(buffer_id, size, &shape, None, ops)
1399 .map_err(map_err)?;
1400 let mut t = edgefirst_tensor::Tensor::from_pbo(pbo).map_err(map_err)?;
1401 t.set_format(format).map_err(set_err)?;
1402 register_pbo_cuda(&mut t, buffer_id, size, sender);
1403 Ok(TensorDyn::from(t))
1404 }
1405 edgefirst_tensor::DType::F16 => {
1406 let pbo = edgefirst_tensor::PboTensor::<edgefirst_tensor::f16>::from_pbo(
1407 buffer_id, size, &shape, None, ops,
1408 )
1409 .map_err(map_err)?;
1410 let mut t = edgefirst_tensor::Tensor::from_pbo(pbo).map_err(map_err)?;
1411 t.set_format(format).map_err(set_err)?;
1412 register_pbo_cuda(&mut t, buffer_id, size, sender);
1413 Ok(TensorDyn::from(t))
1414 }
1415 edgefirst_tensor::DType::F32 => {
1416 let pbo = edgefirst_tensor::PboTensor::<f32>::from_pbo(
1417 buffer_id, size, &shape, None, ops,
1418 )
1419 .map_err(map_err)?;
1420 let mut t = edgefirst_tensor::Tensor::from_pbo(pbo).map_err(map_err)?;
1421 t.set_format(format).map_err(set_err)?;
1422 register_pbo_cuda(&mut t, buffer_id, size, sender);
1423 Ok(TensorDyn::from(t))
1424 }
1425 other => Err(Error::OpenGl(format!("unsupported PBO dtype {other:?}"))),
1426 }
1427 }
1428
1429 pub(crate) fn transfer_backend(&self) -> TransferBackend {
1431 self.caps.transfer_backend
1432 }
1433
1434 pub(crate) fn supported_render_dtypes(&self) -> crate::RenderDtypeSupport {
1441 self.caps.render_dtypes
1442 }
1443}
1444
1445impl Drop for GLProcessorThreaded {
1446 fn drop(&mut self) {
1447 drop(self.sender.take());
1453 let _ = self.handle.take().and_then(|h| h.join().ok());
1454 }
1455}
1456
1457#[cfg(test)]
1461#[cfg_attr(coverage_nightly, coverage(off))]
1462mod tests {
1463 use super::{pbo_elem_count, pbo_shape, GLProcessorThreaded, GlPboOps};
1464 use edgefirst_tensor::{PixelFormat, TensorTrait};
1465
1466 #[test]
1467 fn elem_count_per_format() {
1468 assert_eq!(pbo_elem_count(8, 4, PixelFormat::Rgba), Some(8 * 4 * 4));
1470 assert_eq!(pbo_elem_count(8, 4, PixelFormat::Rgb), Some(8 * 4 * 3));
1472 assert_eq!(pbo_elem_count(8, 4, PixelFormat::Nv12), Some(8 * 4 * 3 / 2));
1474 assert_eq!(pbo_elem_count(8, 4, PixelFormat::Nv16), Some(8 * 4 * 2));
1476 }
1477
1478 #[test]
1479 fn elem_count_overflow_is_none() {
1480 assert_eq!(pbo_elem_count(usize::MAX, 2, PixelFormat::Rgba), None);
1482 assert_eq!(pbo_elem_count(usize::MAX, 1, PixelFormat::Rgb), None);
1484 }
1485
1486 #[test]
1487 fn shape_per_format() {
1488 assert_eq!(pbo_shape(8, 4, PixelFormat::PlanarRgb), vec![3, 4, 8]);
1490 assert_eq!(pbo_shape(8, 4, PixelFormat::Nv12), vec![4 * 3 / 2, 8]);
1492 assert_eq!(pbo_shape(8, 4, PixelFormat::Nv16), vec![4 * 2, 8]);
1494 assert_eq!(pbo_shape(8, 4, PixelFormat::Rgba), vec![4, 8, 4]);
1496 }
1497
1498 #[test]
1516 #[cfg(any(target_os = "linux", target_os = "windows"))] fn concurrent_maps_from_two_independent_handles_on_one_buffer_do_not_corrupt_gl_state() {
1518 let gl = match GLProcessorThreaded::new(None) {
1519 Ok(gl) => gl,
1520 Err(e) => {
1521 crate::test_support::report_skip(&format!(
1522 "concurrent_maps_from_two_independent_handles... - \
1523 OpenGL not available: {e:?}"
1524 ));
1525 return;
1526 }
1527 };
1528 let tensor_a = match gl.create_pbo_image(64, 64, PixelFormat::Rgba) {
1529 Ok(t) => t,
1530 Err(e) => {
1531 crate::test_support::report_skip(&format!(
1532 "concurrent_maps_from_two_independent_handles... - \
1533 PBO not supported: {e:?}"
1534 ));
1535 return;
1536 }
1537 };
1538 let buffer_id = tensor_a.pbo_id().expect("just created a PBO tensor");
1539 let size = tensor_a.capacity_bytes();
1540 let shape = tensor_a.shape().to_vec();
1541
1542 let ops_b: std::sync::Arc<dyn edgefirst_tensor::PboOps> = std::sync::Arc::new(GlPboOps {
1546 sender: gl.sender.as_ref().unwrap().downgrade(),
1547 });
1548 let tensor_b =
1549 edgefirst_tensor::PboTensor::<u8>::from_pbo(buffer_id, size, &shape, None, ops_b)
1550 .expect("from_pbo against a real, already-created buffer_id must succeed");
1551
1552 let map_a = tensor_a.map().expect("first map must succeed");
1554
1555 let map_b_result = tensor_b.map();
1559 let Err(map_b_err) = map_b_result else {
1567 panic!(
1568 "a second, independent handle mapping the SAME real buffer while the \
1569 first mapping is still open must be refused, not silently succeed"
1570 );
1571 };
1572 let msg = format!("{map_b_err:?}");
1573 assert!(
1574 msg.contains("already mapped"),
1575 "must be refused specifically by the GL-thread-level reservation check \
1576 (mapped_pbo_buffers), not some other error path: {msg}"
1577 );
1578
1579 drop(map_a);
1580
1581 let map_b_after = tensor_b.map();
1585 assert!(
1586 map_b_after.is_ok(),
1587 "after the first handle's mapping releases, the second handle must be able to \
1588 map the same buffer: {map_b_after:?}"
1589 );
1590 }
1591}