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 EglCacheStats(tokio::sync::oneshot::Sender<Result<super::cache::GlCacheStats, Error>>),
75 ConvertStats(tokio::sync::oneshot::Sender<Result<super::cache::ConvertStats, Error>>),
77 PboCreate(
78 usize, tokio::sync::oneshot::Sender<Result<u32, Error>>,
80 ),
81 PboMap(
82 u32, usize, tokio::sync::oneshot::Sender<Result<edgefirst_tensor::PboMapping, Error>>,
85 ),
86 PboUnmap(
87 u32, tokio::sync::oneshot::Sender<Result<(), Error>>,
89 ),
90 PboDelete(u32), CudaRegisterBuffer(u32, tokio::sync::oneshot::Sender<Option<usize>>),
92 CudaMap(usize, tokio::sync::oneshot::Sender<Option<(usize, usize)>>),
93 CudaUnmap(usize), CudaUnregister(usize), }
96
97fn pbo_elem_count(
107 width: usize,
108 height: usize,
109 format: edgefirst_tensor::PixelFormat,
110) -> Option<usize> {
111 format
118 .allocation_shape(width, height)?
119 .iter()
120 .try_fold(1usize, |acc, &d| acc.checked_mul(d))
121}
122
123fn pbo_shape(width: usize, height: usize, format: edgefirst_tensor::PixelFormat) -> Vec<usize> {
127 format
128 .allocation_shape(width, height)
129 .unwrap_or_else(|| vec![height, width, format.channels()])
130}
131
132fn register_pbo_cuda<T>(
141 tensor: &mut edgefirst_tensor::Tensor<T>,
142 buffer_id: u32,
143 size: usize,
144 sender: &Sender<GLProcessorMessage>,
145) where
146 T: num_traits::Num + Clone + std::fmt::Debug + Send + Sync + edgefirst_tensor::Element,
147{
148 if !edgefirst_tensor::is_cuda_available() {
149 return;
150 }
151 let (tx, rx) = tokio::sync::oneshot::channel();
152 if sender
153 .blocking_send(GLProcessorMessage::CudaRegisterBuffer(buffer_id, tx))
154 .is_ok()
155 {
156 if let Ok(Some(resource)) = rx.blocking_recv() {
157 let ops = std::sync::Arc::new(GlCudaOps {
158 sender: sender.downgrade(),
159 });
160 tensor.set_cuda_handle(edgefirst_tensor::CudaHandle::new_gl(
161 resource as *mut std::ffi::c_void,
162 size,
163 ops,
164 ));
165 }
166 }
167}
168
169struct GlPboOps {
176 sender: WeakSender<GLProcessorMessage>,
177}
178
179unsafe impl edgefirst_tensor::PboOps for GlPboOps {
184 fn map_buffer(
185 &self,
186 buffer_id: u32,
187 size: usize,
188 ) -> edgefirst_tensor::Result<edgefirst_tensor::PboMapping> {
189 let sender = self
190 .sender
191 .upgrade()
192 .ok_or(edgefirst_tensor::Error::PboDisconnected)?;
193 let (tx, rx) = tokio::sync::oneshot::channel();
194 sender
195 .blocking_send(GLProcessorMessage::PboMap(buffer_id, size, tx))
196 .map_err(|_| edgefirst_tensor::Error::PboDisconnected)?;
197 rx.blocking_recv()
198 .map_err(|_| edgefirst_tensor::Error::PboDisconnected)?
199 .map_err(|e| {
200 edgefirst_tensor::Error::NotImplemented(format!("GL PBO map failed: {e:?}"))
201 })
202 }
203
204 fn unmap_buffer(&self, buffer_id: u32) -> edgefirst_tensor::Result<()> {
205 let sender = self
206 .sender
207 .upgrade()
208 .ok_or(edgefirst_tensor::Error::PboDisconnected)?;
209 let (tx, rx) = tokio::sync::oneshot::channel();
210 sender
211 .blocking_send(GLProcessorMessage::PboUnmap(buffer_id, tx))
212 .map_err(|_| edgefirst_tensor::Error::PboDisconnected)?;
213 rx.blocking_recv()
214 .map_err(|_| edgefirst_tensor::Error::PboDisconnected)?
215 .map_err(|e| {
216 edgefirst_tensor::Error::NotImplemented(format!("GL PBO unmap failed: {e:?}"))
217 })
218 }
219
220 fn delete_buffer(&self, buffer_id: u32) {
221 if let Some(sender) = self.sender.upgrade() {
222 let _ = sender.blocking_send(GLProcessorMessage::PboDelete(buffer_id));
223 }
224 }
225}
226
227struct GlCudaOps {
234 sender: WeakSender<GLProcessorMessage>,
235}
236
237impl edgefirst_tensor::CudaGlOps for GlCudaOps {
238 fn map(&self, resource: *mut std::ffi::c_void) -> Option<(*mut std::ffi::c_void, usize)> {
239 let sender = self.sender.upgrade()?;
240 let (tx, rx) = tokio::sync::oneshot::channel();
241 sender
242 .blocking_send(GLProcessorMessage::CudaMap(resource as usize, tx))
243 .ok()?;
244 rx.blocking_recv()
245 .ok()?
246 .map(|(p, l)| (p as *mut std::ffi::c_void, l))
247 }
248
249 fn unmap(&self, resource: *mut std::ffi::c_void) {
250 if let Some(sender) = self.sender.upgrade() {
251 let _ = sender.blocking_send(GLProcessorMessage::CudaUnmap(resource as usize));
252 }
253 }
254
255 fn unregister(&self, resource: *mut std::ffi::c_void) {
256 if let Some(sender) = self.sender.upgrade() {
257 let _ = sender.blocking_send(GLProcessorMessage::CudaUnregister(resource as usize));
258 }
259 }
260}
261
262#[derive(Debug)]
267pub struct GLProcessorThreaded {
268 handle: Option<JoinHandle<()>>,
270
271 sender: Option<Sender<GLProcessorMessage>>,
273 caps: super::platform::PlatformCaps,
277}
278
279unsafe impl Send for GLProcessorThreaded {}
280unsafe impl Sync for GLProcessorThreaded {}
281
282struct SendablePtr<T: Send> {
283 ptr: NonNull<T>,
284 len: usize,
285}
286
287unsafe impl<T> Send for SendablePtr<T> where T: Send {}
288
289fn panic_message(info: &(dyn std::any::Any + Send)) -> String {
291 if let Some(s) = info.downcast_ref::<&str>() {
292 s.to_string()
293 } else if let Some(s) = info.downcast_ref::<String>() {
294 s.clone()
295 } else {
296 "unknown panic".to_string()
297 }
298}
299
300fn reply_caught<T>(
301 result: std::thread::Result<crate::Result<T>>,
302 resp: tokio::sync::oneshot::Sender<crate::Result<T>>,
303 poisoned: &mut bool,
304 what: &str,
305) {
306 let _ = resp.send(match result {
307 Ok(res) => res,
308 Err(e) => {
309 *poisoned = true;
310 Err(crate::Error::Internal(format!(
311 "GL thread panicked during {what}: {}",
312 panic_message(e.as_ref()),
313 )))
314 }
315 });
316}
317
318fn reject_poisoned_message(msg: GLProcessorMessage) {
319 let poison_err =
320 crate::Error::Internal("GL context is poisoned after a prior panic".to_string());
321 match msg {
322 GLProcessorMessage::ImageConvertFenced(.., resp) => {
323 let _ = resp.send(Err(poison_err));
324 }
325 GLProcessorMessage::ImageConvert(.., resp) => {
326 let _ = resp.send(Err(poison_err));
327 }
328 GLProcessorMessage::Flush(resp) => {
329 let _ = resp.send(Err(poison_err));
330 }
331 GLProcessorMessage::DrawDecodedMasks(.., resp) => {
332 let _ = resp.send(Err(poison_err));
333 }
334 GLProcessorMessage::DrawProtoMasks(.., resp) => {
335 let _ = resp.send(Err(poison_err));
336 }
337 GLProcessorMessage::SetColors(_, resp) => {
338 let _ = resp.send(Err(poison_err));
339 }
340 GLProcessorMessage::SetInt8Interpolation(_, resp) => {
341 let _ = resp.send(Err(poison_err));
342 }
343 GLProcessorMessage::SetColorimetryMode(_, resp) => {
344 let _ = resp.send(Err(poison_err));
345 }
346 GLProcessorMessage::EglCacheStats(resp) => {
347 let _ = resp.send(Err(poison_err));
348 }
349 GLProcessorMessage::ConvertStats(resp) => {
350 let _ = resp.send(Err(poison_err));
351 }
352 GLProcessorMessage::PboCreate(_, resp) => {
353 let _ = resp.send(Err(poison_err));
354 }
355 GLProcessorMessage::PboMap(_, _, resp) => {
356 let _ = resp.send(Err(poison_err));
357 }
358 GLProcessorMessage::PboUnmap(_, resp) => {
359 let _ = resp.send(Err(poison_err));
360 }
361 GLProcessorMessage::PboDelete(_) => {}
362 GLProcessorMessage::CudaRegisterBuffer(_, resp) => {
363 let _ = resp.send(None);
364 }
365 GLProcessorMessage::CudaMap(_, resp) => {
366 let _ = resp.send(None);
367 }
368 GLProcessorMessage::CudaUnmap(_) => {}
372 GLProcessorMessage::CudaUnregister(_) => {}
373 }
374}
375
376fn run_gl_worker(
377 kind: Option<EglDisplayKind>,
378 capacity: Option<usize>,
379 mut recv: tokio::sync::mpsc::Receiver<GLProcessorMessage>,
380 create_ctx_send: tokio::sync::oneshot::Sender<Result<super::platform::PlatformCaps, Error>>,
381) {
382 #[cfg(target_os = "linux")]
387 let init_result = {
388 let _guard = super::context::GL_MUTEX
389 .lock()
390 .unwrap_or_else(|e| e.into_inner());
391 GLProcessorST::new(kind, capacity)
392 };
393 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
397 let init_result = GLProcessorST::new(kind, capacity);
398 #[cfg(target_os = "windows")]
406 let init_result = {
407 let _guard = NON_LINUX_GL_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
408 GLProcessorST::new(kind, capacity)
409 };
410 let mut gl_converter = match init_result {
411 Ok(gl) => gl,
412 Err(e) => {
413 let _ = create_ctx_send.send(Err(e));
414 return;
415 }
416 };
417 let caps = gl_converter.platform_caps();
420 let _ = create_ctx_send.send(Ok(caps));
421 let mut poisoned = false;
422 let mut mapped_pbo_buffers: std::collections::HashSet<u32> = std::collections::HashSet::new();
441 let serialize_per_msg = match std::env::var("EDGEFIRST_GL_SERIALIZE").as_deref() {
450 Ok("full") => true,
451 Ok("lifecycle") => {
452 #[cfg(target_os = "windows")]
457 log::warn!(
458 "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"
459 );
460 false
461 }
462 _ => caps.serialize_gl,
463 };
464 log::debug!(
465 "GL serialization policy: {}",
466 if serialize_per_msg {
467 "Full (per-message global lock)"
468 } else {
469 "LifecycleOnly (parallel across processors)"
470 }
471 );
472 while let Some(msg) = recv.blocking_recv() {
473 #[cfg(target_os = "linux")]
482 let _guard = serialize_per_msg.then(|| {
483 super::context::GL_MUTEX
484 .lock()
485 .unwrap_or_else(|e| e.into_inner())
486 });
487 #[cfg(any(
488 target_os = "macos",
489 target_os = "ios",
490 target_os = "android",
491 target_os = "windows"
492 ))]
493 let _guard =
494 serialize_per_msg.then(|| NON_LINUX_GL_MUTEX.lock().unwrap_or_else(|e| e.into_inner()));
495
496 if poisoned {
501 reject_poisoned_message(msg);
502 continue;
503 }
504
505 gl_converter.begin_gpu_pass();
509 handle_gl_message(
510 msg,
511 &mut gl_converter,
512 &mut mapped_pbo_buffers,
513 &mut poisoned,
514 );
515 gl_converter.end_gpu_pass_if_synced();
519 }
520 #[cfg(target_os = "linux")]
524 let _guard = super::context::GL_MUTEX
525 .lock()
526 .unwrap_or_else(|e| e.into_inner());
527 #[cfg(target_os = "windows")]
528 let _guard = NON_LINUX_GL_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
529 drop(gl_converter);
530}
531
532#[cfg(not(target_os = "linux"))]
536static NON_LINUX_GL_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
537
538#[cfg(target_os = "windows")]
549pub(super) fn lifecycle_guard() -> std::sync::MutexGuard<'static, ()> {
550 NON_LINUX_GL_MUTEX.lock().unwrap_or_else(|e| e.into_inner())
551}
552
553fn handle_gl_message(
554 msg: GLProcessorMessage,
555 gl_converter: &mut GLProcessorST,
556 mapped_pbo_buffers: &mut std::collections::HashSet<u32>,
557 poisoned: &mut bool,
558) {
559 match msg {
560 GLProcessorMessage::ImageConvert(src, dst, rotation, flip, crop, defer, resp) => {
561 handle_image_convert(
562 src,
563 dst,
564 (rotation, flip, crop, defer),
565 resp,
566 gl_converter,
567 poisoned,
568 );
569 }
570 GLProcessorMessage::ImageConvertFenced(src, dst, rotation, flip, crop, resp) => {
571 handle_image_convert_fenced(
572 src,
573 dst,
574 (rotation, flip, crop),
575 resp,
576 gl_converter,
577 poisoned,
578 );
579 }
580 GLProcessorMessage::Flush(resp) => {
581 let result = std::panic::catch_unwind(AssertUnwindSafe(|| gl_converter.flush()));
582 reply_caught(result, resp, poisoned, "Flush");
583 }
584 GLProcessorMessage::DrawDecodedMasks(
585 dst,
586 det,
587 seg,
588 opacity,
589 bg,
590 letterbox,
591 color_mode,
592 resp,
593 ) => {
594 handle_draw_decoded_masks(
595 dst,
596 det,
597 seg,
598 (opacity, bg, letterbox, color_mode),
599 resp,
600 gl_converter,
601 poisoned,
602 );
603 }
604 GLProcessorMessage::DrawProtoMasks(
605 dst,
606 det,
607 proto_data,
608 opacity,
609 bg,
610 letterbox,
611 color_mode,
612 resp,
613 ) => {
614 handle_draw_proto_masks(
615 dst,
616 det,
617 proto_data,
618 (opacity, bg, letterbox, color_mode),
619 resp,
620 gl_converter,
621 poisoned,
622 );
623 }
624 GLProcessorMessage::SetColors(colors, resp) => {
625 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
626 gl_converter.set_class_colors(&colors)
627 }));
628 reply_caught(result, resp, poisoned, "SetColors");
629 }
630 GLProcessorMessage::SetColorimetryMode(mode, resp) => {
631 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
632 gl_converter.set_colorimetry_mode(mode);
633 Ok(())
634 }));
635 reply_caught(result, resp, poisoned, "SetColorimetryMode");
636 }
637 GLProcessorMessage::SetInt8Interpolation(mode, resp) => {
638 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
639 gl_converter.set_int8_interpolation_mode(mode);
640 Ok(())
641 }));
642 reply_caught(result, resp, poisoned, "SetInt8Interpolation");
643 }
644 GLProcessorMessage::EglCacheStats(resp) => {
645 let result =
646 std::panic::catch_unwind(AssertUnwindSafe(|| Ok(gl_converter.egl_cache_stats())));
647 reply_caught(result, resp, poisoned, "EglCacheStats");
648 }
649 GLProcessorMessage::ConvertStats(resp) => {
650 let result =
651 std::panic::catch_unwind(AssertUnwindSafe(|| Ok(gl_converter.convert_stats())));
652 reply_caught(result, resp, poisoned, "ConvertStats");
653 }
654 GLProcessorMessage::PboCreate(size, resp) => {
655 handle_pbo_create(size, resp, poisoned);
656 }
657 GLProcessorMessage::PboMap(buffer_id, size, resp) => {
658 handle_pbo_map(buffer_id, size, resp, mapped_pbo_buffers, poisoned);
659 }
660 GLProcessorMessage::PboUnmap(buffer_id, resp) => {
661 handle_pbo_unmap(buffer_id, resp, mapped_pbo_buffers, poisoned);
662 }
663 GLProcessorMessage::PboDelete(buffer_id) => {
664 handle_pbo_delete(buffer_id, mapped_pbo_buffers, poisoned);
665 }
666 GLProcessorMessage::CudaRegisterBuffer(buffer_id, resp) => {
667 let _ = resp.send(edgefirst_tensor::gl_register_buffer(buffer_id));
669 }
670 GLProcessorMessage::CudaMap(resource, resp) => {
671 gl_converter.flush_pending();
678 let _ = resp.send(edgefirst_tensor::gl_map_resource(resource));
679 }
680 GLProcessorMessage::CudaUnmap(resource) => {
681 edgefirst_tensor::gl_unmap_resource(resource);
682 }
683 GLProcessorMessage::CudaUnregister(resource) => {
684 edgefirst_tensor::gl_unregister_resource(resource);
685 }
686 }
687}
688
689fn handle_image_convert(
690 src: SendablePtr<TensorDyn>,
691 mut dst: SendablePtr<TensorDyn>,
692 geom: (Rotation, Flip, Crop, bool),
693 resp: tokio::sync::oneshot::Sender<Result<(), Error>>,
694 gl_converter: &mut GLProcessorST,
695 poisoned: &mut bool,
696) {
697 let (rotation, flip, crop, defer) = geom;
698 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
701 let src = unsafe { src.ptr.as_ref() };
702 let dst = unsafe { dst.ptr.as_mut() };
703 if defer {
707 gl_converter.convert_deferred(src, dst, rotation, flip, crop)
708 } else {
709 gl_converter.convert(src, dst, rotation, flip, crop)
710 }
711 }));
712 reply_caught(result, resp, poisoned, "ImageConvert");
713}
714
715fn handle_image_convert_fenced(
716 src: SendablePtr<TensorDyn>,
717 mut dst: SendablePtr<TensorDyn>,
718 geom: (Rotation, Flip, Crop),
719 resp: tokio::sync::oneshot::Sender<Result<Option<super::CompletionFence>, Error>>,
720 gl_converter: &mut GLProcessorST,
721 poisoned: &mut bool,
722) {
723 let (rotation, flip, crop) = geom;
724 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
727 let src = unsafe { src.ptr.as_ref() };
728 let dst = unsafe { dst.ptr.as_mut() };
729 gl_converter.convert_fenced(src, dst, rotation, flip, crop)
730 }));
731 reply_caught(result, resp, poisoned, "ImageConvertFenced");
732}
733
734fn handle_draw_decoded_masks(
735 mut dst: SendablePtr<TensorDyn>,
736 det: SendablePtr<DetectBox>,
737 seg: SendablePtr<Segmentation>,
738 overlay: (
739 f32,
740 Option<SendablePtr<TensorDyn>>,
741 Option<[f32; 4]>,
742 crate::ColorMode,
743 ),
744 resp: tokio::sync::oneshot::Sender<Result<(), Error>>,
745 gl_converter: &mut GLProcessorST,
746 poisoned: &mut bool,
747) {
748 let (opacity, bg, letterbox, color_mode) = overlay;
749 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
753 let dst = unsafe { dst.ptr.as_mut() };
754 let det = unsafe { std::slice::from_raw_parts(det.ptr.as_ptr(), det.len) };
755 let seg = unsafe { std::slice::from_raw_parts(seg.ptr.as_ptr(), seg.len) };
756 let bg_ref = bg.map(|p| unsafe { &*p.ptr.as_ptr() });
757 gl_converter.draw_decoded_masks(
758 dst,
759 det,
760 seg,
761 crate::MaskOverlay {
762 background: bg_ref,
763 opacity,
764 letterbox,
765 color_mode,
766 },
767 )
768 }));
769 reply_caught(result, resp, poisoned, "DrawDecodedMasks");
770}
771
772fn handle_draw_proto_masks(
773 mut dst: SendablePtr<TensorDyn>,
774 det: SendablePtr<DetectBox>,
775 proto_data: SendablePtr<ProtoData>,
776 overlay: (
777 f32,
778 Option<SendablePtr<TensorDyn>>,
779 Option<[f32; 4]>,
780 crate::ColorMode,
781 ),
782 resp: tokio::sync::oneshot::Sender<Result<(), Error>>,
783 gl_converter: &mut GLProcessorST,
784 poisoned: &mut bool,
785) {
786 let (opacity, bg, letterbox, color_mode) = overlay;
787 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
790 let dst = unsafe { dst.ptr.as_mut() };
791 let det = unsafe { std::slice::from_raw_parts(det.ptr.as_ptr(), det.len) };
792 let bg_ref = bg.map(|p| unsafe { &*p.ptr.as_ptr() });
793 let proto_data = unsafe { proto_data.ptr.as_ref() };
794 gl_converter.draw_proto_masks(
795 dst,
796 det,
797 proto_data,
798 crate::MaskOverlay {
799 background: bg_ref,
800 opacity,
801 letterbox,
802 color_mode,
803 },
804 )
805 }));
806 reply_caught(result, resp, poisoned, "DrawProtoMasks");
807}
808
809fn handle_pbo_create(
810 size: usize,
811 resp: tokio::sync::oneshot::Sender<Result<u32, Error>>,
812 poisoned: &mut bool,
813) {
814 let result = std::panic::catch_unwind(AssertUnwindSafe(|| unsafe {
815 let mut id: u32 = 0;
816 edgefirst_gl::gl::GenBuffers(1, &mut id);
817 edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, id);
818 edgefirst_gl::gl::BufferData(
819 edgefirst_gl::gl::PIXEL_PACK_BUFFER,
820 size as isize,
821 std::ptr::null(),
822 edgefirst_gl::gl::STREAM_COPY,
823 );
824 edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, 0);
825 match check_gl_error("PboCreate", 0) {
826 Ok(()) => Ok(id),
827 Err(e) => {
828 edgefirst_gl::gl::DeleteBuffers(1, &id);
829 Err(e)
830 }
831 }
832 }));
833 reply_caught(result, resp, poisoned, "PboCreate");
834}
835
836fn handle_pbo_map(
837 buffer_id: u32,
838 size: usize,
839 resp: tokio::sync::oneshot::Sender<Result<edgefirst_tensor::PboMapping, Error>>,
840 mapped_pbo_buffers: &mut std::collections::HashSet<u32>,
841 poisoned: &mut bool,
842) {
843 if !mapped_pbo_buffers.insert(buffer_id) {
844 let _ = resp.send(Err(crate::Error::OpenGl(format!(
850 "PBO buffer {buffer_id} is already mapped in this GL context \
851 (a producer and a reconstructed cross-package consumer may \
852 both hold a live tensor over it)"
853 ))));
854 return;
855 }
856 let result = std::panic::catch_unwind(AssertUnwindSafe(|| unsafe {
857 edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, buffer_id);
858 let ptr = edgefirst_gl::gl::MapBufferRange(
859 edgefirst_gl::gl::PIXEL_PACK_BUFFER,
860 0,
861 size as isize,
862 edgefirst_gl::gl::MAP_READ_BIT | edgefirst_gl::gl::MAP_WRITE_BIT,
863 );
864 edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, 0);
865 if ptr.is_null() {
866 Err(crate::Error::OpenGl(
867 "glMapBufferRange returned null".to_string(),
868 ))
869 } else {
870 Ok(edgefirst_tensor::PboMapping {
871 ptr: ptr as *mut u8,
872 size,
873 })
874 }
875 }));
876 let mapped_ok = matches!(result, Ok(Ok(_)));
877 if !mapped_ok {
878 mapped_pbo_buffers.remove(&buffer_id);
883 }
884 reply_caught(result, resp, poisoned, "PboMap");
885}
886
887fn handle_pbo_unmap(
888 buffer_id: u32,
889 resp: tokio::sync::oneshot::Sender<Result<(), Error>>,
890 mapped_pbo_buffers: &mut std::collections::HashSet<u32>,
891 poisoned: &mut bool,
892) {
893 mapped_pbo_buffers.remove(&buffer_id);
900 let result = std::panic::catch_unwind(AssertUnwindSafe(|| unsafe {
901 edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, buffer_id);
902 let ok = edgefirst_gl::gl::UnmapBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER);
903 edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, 0);
904 if ok == edgefirst_gl::gl::FALSE {
905 Err(Error::OpenGl(
906 "PBO data was corrupted during mapping".into(),
907 ))
908 } else {
909 check_gl_error("PboUnmap", 0)
910 }
911 }));
912 reply_caught(result, resp, poisoned, "PboUnmap");
913}
914
915fn handle_pbo_delete(
916 buffer_id: u32,
917 mapped_pbo_buffers: &mut std::collections::HashSet<u32>,
918 poisoned: &mut bool,
919) {
920 mapped_pbo_buffers.remove(&buffer_id);
927 if let Err(e) = std::panic::catch_unwind(AssertUnwindSafe(|| unsafe {
928 edgefirst_gl::gl::DeleteBuffers(1, &buffer_id);
929 })) {
930 *poisoned = true;
931 log::error!(
932 "GL thread panicked during PboDelete: {}",
933 panic_message(e.as_ref()),
934 );
935 }
936}
937
938impl GLProcessorThreaded {
939 pub fn new(kind: Option<EglDisplayKind>) -> Result<Self, Error> {
942 Self::with_cache_capacity(kind, None)
943 }
944
945 pub fn with_cache_capacity(
951 kind: Option<EglDisplayKind>,
952 capacity: Option<usize>,
953 ) -> Result<Self, Error> {
954 let (send, recv) = tokio::sync::mpsc::channel::<GLProcessorMessage>(1);
955
956 let (create_ctx_send, create_ctx_recv) = tokio::sync::oneshot::channel();
957
958 let handle = std::thread::spawn(move || {
959 run_gl_worker(kind, capacity, recv, create_ctx_send);
960 });
961
962 let caps = match create_ctx_recv.blocking_recv() {
963 Ok(Err(e)) => return Err(e),
964 Err(_) => {
965 return Err(Error::Internal(
966 "GL converter error messaging closed without update".to_string(),
967 ));
968 }
969 Ok(Ok(caps)) => caps,
970 };
971
972 Ok(Self {
973 handle: Some(handle),
974 sender: Some(send),
975 caps,
976 })
977 }
978}
979
980impl ImageProcessorTrait for GLProcessorThreaded {
981 fn convert(
982 &mut self,
983 src: &TensorDyn,
984 dst: &mut TensorDyn,
985 rotation: crate::Rotation,
986 flip: Flip,
987 crop: Crop,
988 ) -> crate::Result<()> {
989 let (err_send, err_recv) = tokio::sync::oneshot::channel();
990 self.sender
991 .as_ref()
992 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
993 .blocking_send(GLProcessorMessage::ImageConvert(
994 SendablePtr {
995 ptr: NonNull::from(src),
996 len: 1,
997 },
998 SendablePtr {
999 ptr: NonNull::from(dst),
1000 len: 1,
1001 },
1002 rotation,
1003 flip,
1004 crop,
1005 false, err_send,
1007 ))
1008 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1009 err_recv.blocking_recv().map_err(|_| {
1010 Error::Internal("GL converter error messaging closed without update".to_string())
1011 })?
1012 }
1013
1014 fn convert_deferred(
1015 &mut self,
1016 src: &TensorDyn,
1017 dst: &mut TensorDyn,
1018 rotation: crate::Rotation,
1019 flip: Flip,
1020 crop: Crop,
1021 ) -> crate::Result<()> {
1022 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1023 self.sender
1024 .as_ref()
1025 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1026 .blocking_send(GLProcessorMessage::ImageConvert(
1027 SendablePtr {
1028 ptr: NonNull::from(src),
1029 len: 1,
1030 },
1031 SendablePtr {
1032 ptr: NonNull::from(dst),
1033 len: 1,
1034 },
1035 rotation,
1036 flip,
1037 crop,
1038 true, err_send,
1040 ))
1041 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1042 err_recv.blocking_recv().map_err(|_| {
1043 Error::Internal("GL converter error messaging closed without update".to_string())
1044 })?
1045 }
1046
1047 fn flush(&mut self) -> crate::Result<()> {
1048 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1049 self.sender
1050 .as_ref()
1051 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1052 .blocking_send(GLProcessorMessage::Flush(err_send))
1053 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1054 err_recv.blocking_recv().map_err(|_| {
1055 Error::Internal("GL converter error messaging closed without update".to_string())
1056 })?
1057 }
1058
1059 fn draw_decoded_masks(
1060 &mut self,
1061 dst: &mut TensorDyn,
1062 detect: &[crate::DetectBox],
1063 segmentation: &[crate::Segmentation],
1064 overlay: crate::MaskOverlay<'_>,
1065 ) -> crate::Result<()> {
1066 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1067 self.sender
1068 .as_ref()
1069 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1070 .blocking_send(GLProcessorMessage::DrawDecodedMasks(
1071 SendablePtr {
1072 ptr: NonNull::from(dst),
1073 len: 1,
1074 },
1075 SendablePtr {
1076 ptr: NonNull::new(detect.as_ptr() as *mut DetectBox).unwrap(),
1077 len: detect.len(),
1078 },
1079 SendablePtr {
1080 ptr: NonNull::new(segmentation.as_ptr() as *mut Segmentation).unwrap(),
1081 len: segmentation.len(),
1082 },
1083 overlay.opacity,
1084 overlay.background.map(|bg| SendablePtr {
1085 ptr: NonNull::from(bg).cast::<TensorDyn>(),
1086 len: 1,
1087 }),
1088 overlay.letterbox,
1089 overlay.color_mode,
1090 err_send,
1091 ))
1092 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1093 err_recv.blocking_recv().map_err(|_| {
1094 Error::Internal("GL converter error messaging closed without update".to_string())
1095 })?
1096 }
1097
1098 fn draw_proto_masks(
1099 &mut self,
1100 dst: &mut TensorDyn,
1101 detect: &[DetectBox],
1102 proto_data: &ProtoData,
1103 overlay: crate::MaskOverlay<'_>,
1104 ) -> crate::Result<()> {
1105 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1106 self.sender
1107 .as_ref()
1108 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1109 .blocking_send(GLProcessorMessage::DrawProtoMasks(
1110 SendablePtr {
1111 ptr: NonNull::from(dst),
1112 len: 1,
1113 },
1114 SendablePtr {
1115 ptr: NonNull::new(detect.as_ptr() as *mut DetectBox).unwrap(),
1116 len: detect.len(),
1117 },
1118 SendablePtr {
1119 ptr: NonNull::from(proto_data).cast::<ProtoData>(),
1120 len: 1,
1121 },
1122 overlay.opacity,
1123 overlay.background.map(|bg| SendablePtr {
1124 ptr: NonNull::from(bg).cast::<TensorDyn>(),
1125 len: 1,
1126 }),
1127 overlay.letterbox,
1128 overlay.color_mode,
1129 err_send,
1130 ))
1131 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1132 err_recv.blocking_recv().map_err(|_| {
1133 Error::Internal("GL converter error messaging closed without update".to_string())
1134 })?
1135 }
1136
1137 fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<(), crate::Error> {
1138 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1139 self.sender
1140 .as_ref()
1141 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1142 .blocking_send(GLProcessorMessage::SetColors(colors.to_vec(), err_send))
1143 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1144 err_recv.blocking_recv().map_err(|_| {
1145 Error::Internal("GL converter error messaging closed without update".to_string())
1146 })?
1147 }
1148}
1149
1150impl GLProcessorThreaded {
1151 pub fn set_colorimetry_mode(&mut self, mode: crate::ColorimetryMode) -> Result<(), Error> {
1156 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1157 self.sender
1158 .as_ref()
1159 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1160 .blocking_send(GLProcessorMessage::SetColorimetryMode(mode, err_send))
1161 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1162 err_recv.blocking_recv().map_err(|_| {
1163 Error::Internal("GL converter error messaging closed without update".to_string())
1164 })?
1165 }
1166
1167 pub fn set_int8_interpolation_mode(
1169 &mut self,
1170 mode: Int8InterpolationMode,
1171 ) -> Result<(), crate::Error> {
1172 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1173 self.sender
1174 .as_ref()
1175 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1176 .blocking_send(GLProcessorMessage::SetInt8Interpolation(mode, err_send))
1177 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1178 err_recv.blocking_recv().map_err(|_| {
1179 Error::Internal("GL converter error messaging closed without update".to_string())
1180 })?
1181 }
1182
1183 pub fn egl_cache_stats(&self) -> Result<super::cache::GlCacheStats, Error> {
1190 let (send, recv) = tokio::sync::oneshot::channel();
1191 self.sender
1192 .as_ref()
1193 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1194 .blocking_send(GLProcessorMessage::EglCacheStats(send))
1195 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1196 recv.blocking_recv().map_err(|_| {
1197 Error::Internal("GL converter error messaging closed without update".to_string())
1198 })?
1199 }
1200
1201 pub fn convert_with_fence(
1212 &mut self,
1213 src: &TensorDyn,
1214 dst: &mut TensorDyn,
1215 rotation: crate::Rotation,
1216 flip: Flip,
1217 crop: Crop,
1218 ) -> Result<Option<super::CompletionFence>, Error> {
1219 use crate::ImageProcessorTrait as _;
1220 if !self.caps.native_fence_sync {
1221 self.convert(src, dst, rotation, flip, crop)?;
1224 return Ok(None);
1225 }
1226 let (send, recv) = tokio::sync::oneshot::channel();
1227 self.sender
1228 .as_ref()
1229 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1230 .blocking_send(GLProcessorMessage::ImageConvertFenced(
1231 SendablePtr {
1232 ptr: NonNull::from(src),
1233 len: 1,
1234 },
1235 SendablePtr {
1236 ptr: NonNull::from(dst),
1237 len: 1,
1238 },
1239 rotation,
1240 flip,
1241 crop,
1242 send,
1243 ))
1244 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1245 recv.blocking_recv().map_err(|_| {
1246 Error::Internal("GL converter error messaging closed without update".to_string())
1247 })?
1248 }
1249
1250 pub fn convert_stats(&self) -> Result<super::cache::ConvertStats, Error> {
1255 let (send, recv) = tokio::sync::oneshot::channel();
1256 self.sender
1257 .as_ref()
1258 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1259 .blocking_send(GLProcessorMessage::ConvertStats(send))
1260 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1261 recv.blocking_recv().map_err(|_| {
1262 Error::Internal("GL converter error messaging closed without update".to_string())
1263 })?
1264 }
1265
1266 pub fn create_pbo_image(
1268 &self,
1269 width: usize,
1270 height: usize,
1271 format: edgefirst_tensor::PixelFormat,
1272 ) -> Result<edgefirst_tensor::Tensor<u8>, Error> {
1273 let sender = self
1274 .sender
1275 .as_ref()
1276 .ok_or(Error::OpenGl("GL processor is shutting down".to_string()))?;
1277
1278 let size = pbo_elem_count(width, height, format)
1279 .filter(|&s| s != 0)
1280 .ok_or_else(|| Error::OpenGl("Invalid image dimensions".to_string()))?;
1281
1282 let (tx, rx) = tokio::sync::oneshot::channel();
1284 sender
1285 .blocking_send(GLProcessorMessage::PboCreate(size, tx))
1286 .map_err(|_| Error::OpenGl("GL thread channel closed".to_string()))?;
1287 let buffer_id = rx
1288 .blocking_recv()
1289 .map_err(|_| Error::OpenGl("GL thread did not respond".to_string()))??;
1290
1291 let ops: std::sync::Arc<dyn edgefirst_tensor::PboOps> = std::sync::Arc::new(GlPboOps {
1292 sender: sender.downgrade(),
1293 });
1294
1295 let shape = pbo_shape(width, height, format);
1296
1297 let pbo_tensor =
1298 edgefirst_tensor::PboTensor::<u8>::from_pbo(buffer_id, size, &shape, None, ops)
1299 .map_err(|e| Error::OpenGl(format!("PBO tensor creation failed: {e:?}")))?;
1300 let mut tensor = edgefirst_tensor::Tensor::from_pbo(pbo_tensor)
1301 .map_err(|e| Error::OpenGl(format!("PBO tensor wrap failed: {e:?}")))?;
1302 tensor
1303 .set_format(format)
1304 .map_err(|e| Error::OpenGl(format!("Failed to set format on PBO tensor: {e:?}")))?;
1305 register_pbo_cuda(&mut tensor, buffer_id, size, sender);
1312 Ok(tensor)
1313 }
1314
1315 pub(crate) fn create_pbo_image_dtype(
1321 &self,
1322 width: usize,
1323 height: usize,
1324 format: edgefirst_tensor::PixelFormat,
1325 dtype: edgefirst_tensor::DType,
1326 ) -> Result<TensorDyn, Error> {
1327 let sender = self
1328 .sender
1329 .as_ref()
1330 .ok_or(Error::OpenGl("GL processor is shutting down".to_string()))?;
1331
1332 let elems = pbo_elem_count(width, height, format)
1333 .filter(|&e| e != 0)
1334 .ok_or_else(|| Error::OpenGl("Invalid image dimensions".to_string()))?;
1335
1336 let size = elems
1337 .checked_mul(dtype.size())
1338 .ok_or_else(|| Error::OpenGl("PBO size overflow".to_string()))?;
1339
1340 let (tx, rx) = tokio::sync::oneshot::channel();
1342 sender
1343 .blocking_send(GLProcessorMessage::PboCreate(size, tx))
1344 .map_err(|_| Error::OpenGl("GL thread channel closed".to_string()))?;
1345 let buffer_id = rx
1346 .blocking_recv()
1347 .map_err(|_| Error::OpenGl("GL thread did not respond".to_string()))??;
1348
1349 let ops: std::sync::Arc<dyn edgefirst_tensor::PboOps> = std::sync::Arc::new(GlPboOps {
1350 sender: sender.downgrade(),
1351 });
1352
1353 let shape = pbo_shape(width, height, format);
1354
1355 let map_err = |e: edgefirst_tensor::Error| {
1356 Error::OpenGl(format!("PBO tensor creation failed: {e:?}"))
1357 };
1358 let set_err = |e: edgefirst_tensor::Error| {
1359 Error::OpenGl(format!("Failed to set format on PBO tensor: {e:?}"))
1360 };
1361
1362 match dtype {
1363 edgefirst_tensor::DType::U8 => {
1364 let pbo =
1365 edgefirst_tensor::PboTensor::<u8>::from_pbo(buffer_id, size, &shape, None, ops)
1366 .map_err(map_err)?;
1367 let mut t = edgefirst_tensor::Tensor::from_pbo(pbo).map_err(map_err)?;
1368 t.set_format(format).map_err(set_err)?;
1369 register_pbo_cuda(&mut t, buffer_id, size, sender);
1370 Ok(TensorDyn::from(t))
1371 }
1372 edgefirst_tensor::DType::F16 => {
1373 let pbo = edgefirst_tensor::PboTensor::<edgefirst_tensor::f16>::from_pbo(
1374 buffer_id, size, &shape, None, ops,
1375 )
1376 .map_err(map_err)?;
1377 let mut t = edgefirst_tensor::Tensor::from_pbo(pbo).map_err(map_err)?;
1378 t.set_format(format).map_err(set_err)?;
1379 register_pbo_cuda(&mut t, buffer_id, size, sender);
1380 Ok(TensorDyn::from(t))
1381 }
1382 edgefirst_tensor::DType::F32 => {
1383 let pbo = edgefirst_tensor::PboTensor::<f32>::from_pbo(
1384 buffer_id, size, &shape, None, ops,
1385 )
1386 .map_err(map_err)?;
1387 let mut t = edgefirst_tensor::Tensor::from_pbo(pbo).map_err(map_err)?;
1388 t.set_format(format).map_err(set_err)?;
1389 register_pbo_cuda(&mut t, buffer_id, size, sender);
1390 Ok(TensorDyn::from(t))
1391 }
1392 other => Err(Error::OpenGl(format!("unsupported PBO dtype {other:?}"))),
1393 }
1394 }
1395
1396 pub(crate) fn transfer_backend(&self) -> TransferBackend {
1398 self.caps.transfer_backend
1399 }
1400
1401 pub(crate) fn supported_render_dtypes(&self) -> crate::RenderDtypeSupport {
1408 self.caps.render_dtypes
1409 }
1410}
1411
1412impl Drop for GLProcessorThreaded {
1413 fn drop(&mut self) {
1414 drop(self.sender.take());
1420 let _ = self.handle.take().and_then(|h| h.join().ok());
1421 }
1422}
1423
1424#[cfg(test)]
1428#[cfg_attr(coverage_nightly, coverage(off))]
1429mod tests {
1430 use super::{pbo_elem_count, pbo_shape, GLProcessorThreaded, GlPboOps};
1431 use edgefirst_tensor::{PixelFormat, TensorTrait};
1432
1433 #[test]
1434 fn elem_count_per_format() {
1435 assert_eq!(pbo_elem_count(8, 4, PixelFormat::Rgba), Some(8 * 4 * 4));
1437 assert_eq!(pbo_elem_count(8, 4, PixelFormat::Rgb), Some(8 * 4 * 3));
1439 assert_eq!(pbo_elem_count(8, 4, PixelFormat::Nv12), Some(8 * 4 * 3 / 2));
1441 assert_eq!(pbo_elem_count(8, 4, PixelFormat::Nv16), Some(8 * 4 * 2));
1443 }
1444
1445 #[test]
1446 fn elem_count_overflow_is_none() {
1447 assert_eq!(pbo_elem_count(usize::MAX, 2, PixelFormat::Rgba), None);
1449 assert_eq!(pbo_elem_count(usize::MAX, 1, PixelFormat::Rgb), None);
1451 }
1452
1453 #[test]
1454 fn shape_per_format() {
1455 assert_eq!(pbo_shape(8, 4, PixelFormat::PlanarRgb), vec![3, 4, 8]);
1457 assert_eq!(pbo_shape(8, 4, PixelFormat::Nv12), vec![4 * 3 / 2, 8]);
1459 assert_eq!(pbo_shape(8, 4, PixelFormat::Nv16), vec![4 * 2, 8]);
1461 assert_eq!(pbo_shape(8, 4, PixelFormat::Rgba), vec![4, 8, 4]);
1463 }
1464
1465 #[test]
1483 #[cfg(any(target_os = "linux", target_os = "windows"))] fn concurrent_maps_from_two_independent_handles_on_one_buffer_do_not_corrupt_gl_state() {
1485 let gl = match GLProcessorThreaded::new(None) {
1486 Ok(gl) => gl,
1487 Err(e) => {
1488 eprintln!(
1489 "SKIPPED: concurrent_maps_from_two_independent_handles... - \
1490 OpenGL not available: {e:?}"
1491 );
1492 return;
1493 }
1494 };
1495 let tensor_a = match gl.create_pbo_image(64, 64, PixelFormat::Rgba) {
1496 Ok(t) => t,
1497 Err(e) => {
1498 eprintln!(
1499 "SKIPPED: concurrent_maps_from_two_independent_handles... - \
1500 PBO not supported: {e:?}"
1501 );
1502 return;
1503 }
1504 };
1505 let pbo_a = tensor_a.as_pbo().expect("just created a PBO tensor");
1506 let buffer_id = pbo_a.buffer_id();
1507 let size = tensor_a.capacity_bytes();
1508 let shape = tensor_a.shape().to_vec();
1509
1510 let ops_b: std::sync::Arc<dyn edgefirst_tensor::PboOps> = std::sync::Arc::new(GlPboOps {
1514 sender: gl.sender.as_ref().unwrap().downgrade(),
1515 });
1516 let tensor_b =
1517 edgefirst_tensor::PboTensor::<u8>::from_pbo(buffer_id, size, &shape, None, ops_b)
1518 .expect("from_pbo against a real, already-created buffer_id must succeed");
1519
1520 let map_a = tensor_a.map().expect("first map must succeed");
1522
1523 let map_b_result = tensor_b.map();
1527 let Err(map_b_err) = map_b_result else {
1535 panic!(
1536 "a second, independent handle mapping the SAME real buffer while the \
1537 first mapping is still open must be refused, not silently succeed"
1538 );
1539 };
1540 let msg = format!("{map_b_err:?}");
1541 assert!(
1542 msg.contains("already mapped"),
1543 "must be refused specifically by the GL-thread-level reservation check \
1544 (mapped_pbo_buffers), not some other error path: {msg}"
1545 );
1546
1547 drop(map_a);
1548
1549 let map_b_after = tensor_b.map();
1553 assert!(
1554 map_b_after.is_ok(),
1555 "after the first handle's mapping releases, the second handle must be able to \
1556 map the same buffer: {map_b_after:?}"
1557 );
1558 }
1559}