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") => false,
452 _ => caps.serialize_gl,
453 };
454 log::debug!(
455 "GL serialization policy: {}",
456 if serialize_per_msg {
457 "Full (per-message global lock)"
458 } else {
459 "LifecycleOnly (parallel across processors)"
460 }
461 );
462 while let Some(msg) = recv.blocking_recv() {
463 #[cfg(target_os = "linux")]
472 let _guard = serialize_per_msg.then(|| {
473 super::context::GL_MUTEX
474 .lock()
475 .unwrap_or_else(|e| e.into_inner())
476 });
477 #[cfg(any(
478 target_os = "macos",
479 target_os = "ios",
480 target_os = "android",
481 target_os = "windows"
482 ))]
483 let _guard =
484 serialize_per_msg.then(|| NON_LINUX_GL_MUTEX.lock().unwrap_or_else(|e| e.into_inner()));
485
486 if poisoned {
491 reject_poisoned_message(msg);
492 continue;
493 }
494
495 gl_converter.begin_gpu_pass();
499 handle_gl_message(
500 msg,
501 &mut gl_converter,
502 &mut mapped_pbo_buffers,
503 &mut poisoned,
504 );
505 gl_converter.end_gpu_pass_if_synced();
509 }
510 #[cfg(target_os = "linux")]
514 let _guard = super::context::GL_MUTEX
515 .lock()
516 .unwrap_or_else(|e| e.into_inner());
517 #[cfg(target_os = "windows")]
518 let _guard = NON_LINUX_GL_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
519 drop(gl_converter);
520}
521
522#[cfg(not(target_os = "linux"))]
526static NON_LINUX_GL_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
527
528fn handle_gl_message(
529 msg: GLProcessorMessage,
530 gl_converter: &mut GLProcessorST,
531 mapped_pbo_buffers: &mut std::collections::HashSet<u32>,
532 poisoned: &mut bool,
533) {
534 match msg {
535 GLProcessorMessage::ImageConvert(src, dst, rotation, flip, crop, defer, resp) => {
536 handle_image_convert(
537 src,
538 dst,
539 (rotation, flip, crop, defer),
540 resp,
541 gl_converter,
542 poisoned,
543 );
544 }
545 GLProcessorMessage::ImageConvertFenced(src, dst, rotation, flip, crop, resp) => {
546 handle_image_convert_fenced(
547 src,
548 dst,
549 (rotation, flip, crop),
550 resp,
551 gl_converter,
552 poisoned,
553 );
554 }
555 GLProcessorMessage::Flush(resp) => {
556 let result = std::panic::catch_unwind(AssertUnwindSafe(|| gl_converter.flush()));
557 reply_caught(result, resp, poisoned, "Flush");
558 }
559 GLProcessorMessage::DrawDecodedMasks(
560 dst,
561 det,
562 seg,
563 opacity,
564 bg,
565 letterbox,
566 color_mode,
567 resp,
568 ) => {
569 handle_draw_decoded_masks(
570 dst,
571 det,
572 seg,
573 (opacity, bg, letterbox, color_mode),
574 resp,
575 gl_converter,
576 poisoned,
577 );
578 }
579 GLProcessorMessage::DrawProtoMasks(
580 dst,
581 det,
582 proto_data,
583 opacity,
584 bg,
585 letterbox,
586 color_mode,
587 resp,
588 ) => {
589 handle_draw_proto_masks(
590 dst,
591 det,
592 proto_data,
593 (opacity, bg, letterbox, color_mode),
594 resp,
595 gl_converter,
596 poisoned,
597 );
598 }
599 GLProcessorMessage::SetColors(colors, resp) => {
600 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
601 gl_converter.set_class_colors(&colors)
602 }));
603 reply_caught(result, resp, poisoned, "SetColors");
604 }
605 GLProcessorMessage::SetColorimetryMode(mode, resp) => {
606 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
607 gl_converter.set_colorimetry_mode(mode);
608 Ok(())
609 }));
610 reply_caught(result, resp, poisoned, "SetColorimetryMode");
611 }
612 GLProcessorMessage::SetInt8Interpolation(mode, resp) => {
613 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
614 gl_converter.set_int8_interpolation_mode(mode);
615 Ok(())
616 }));
617 reply_caught(result, resp, poisoned, "SetInt8Interpolation");
618 }
619 GLProcessorMessage::EglCacheStats(resp) => {
620 let result =
621 std::panic::catch_unwind(AssertUnwindSafe(|| Ok(gl_converter.egl_cache_stats())));
622 reply_caught(result, resp, poisoned, "EglCacheStats");
623 }
624 GLProcessorMessage::ConvertStats(resp) => {
625 let result =
626 std::panic::catch_unwind(AssertUnwindSafe(|| Ok(gl_converter.convert_stats())));
627 reply_caught(result, resp, poisoned, "ConvertStats");
628 }
629 GLProcessorMessage::PboCreate(size, resp) => {
630 handle_pbo_create(size, resp, poisoned);
631 }
632 GLProcessorMessage::PboMap(buffer_id, size, resp) => {
633 handle_pbo_map(buffer_id, size, resp, mapped_pbo_buffers, poisoned);
634 }
635 GLProcessorMessage::PboUnmap(buffer_id, resp) => {
636 handle_pbo_unmap(buffer_id, resp, mapped_pbo_buffers, poisoned);
637 }
638 GLProcessorMessage::PboDelete(buffer_id) => {
639 handle_pbo_delete(buffer_id, mapped_pbo_buffers, poisoned);
640 }
641 GLProcessorMessage::CudaRegisterBuffer(buffer_id, resp) => {
642 let _ = resp.send(edgefirst_tensor::gl_register_buffer(buffer_id));
644 }
645 GLProcessorMessage::CudaMap(resource, resp) => {
646 gl_converter.flush_pending();
653 let _ = resp.send(edgefirst_tensor::gl_map_resource(resource));
654 }
655 GLProcessorMessage::CudaUnmap(resource) => {
656 edgefirst_tensor::gl_unmap_resource(resource);
657 }
658 GLProcessorMessage::CudaUnregister(resource) => {
659 edgefirst_tensor::gl_unregister_resource(resource);
660 }
661 }
662}
663
664fn handle_image_convert(
665 src: SendablePtr<TensorDyn>,
666 mut dst: SendablePtr<TensorDyn>,
667 geom: (Rotation, Flip, Crop, bool),
668 resp: tokio::sync::oneshot::Sender<Result<(), Error>>,
669 gl_converter: &mut GLProcessorST,
670 poisoned: &mut bool,
671) {
672 let (rotation, flip, crop, defer) = geom;
673 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
676 let src = unsafe { src.ptr.as_ref() };
677 let dst = unsafe { dst.ptr.as_mut() };
678 if defer {
682 gl_converter.convert_deferred(src, dst, rotation, flip, crop)
683 } else {
684 gl_converter.convert(src, dst, rotation, flip, crop)
685 }
686 }));
687 reply_caught(result, resp, poisoned, "ImageConvert");
688}
689
690fn handle_image_convert_fenced(
691 src: SendablePtr<TensorDyn>,
692 mut dst: SendablePtr<TensorDyn>,
693 geom: (Rotation, Flip, Crop),
694 resp: tokio::sync::oneshot::Sender<Result<Option<super::CompletionFence>, Error>>,
695 gl_converter: &mut GLProcessorST,
696 poisoned: &mut bool,
697) {
698 let (rotation, flip, crop) = geom;
699 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
702 let src = unsafe { src.ptr.as_ref() };
703 let dst = unsafe { dst.ptr.as_mut() };
704 gl_converter.convert_fenced(src, dst, rotation, flip, crop)
705 }));
706 reply_caught(result, resp, poisoned, "ImageConvertFenced");
707}
708
709fn handle_draw_decoded_masks(
710 mut dst: SendablePtr<TensorDyn>,
711 det: SendablePtr<DetectBox>,
712 seg: SendablePtr<Segmentation>,
713 overlay: (
714 f32,
715 Option<SendablePtr<TensorDyn>>,
716 Option<[f32; 4]>,
717 crate::ColorMode,
718 ),
719 resp: tokio::sync::oneshot::Sender<Result<(), Error>>,
720 gl_converter: &mut GLProcessorST,
721 poisoned: &mut bool,
722) {
723 let (opacity, bg, letterbox, color_mode) = overlay;
724 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
728 let dst = unsafe { dst.ptr.as_mut() };
729 let det = unsafe { std::slice::from_raw_parts(det.ptr.as_ptr(), det.len) };
730 let seg = unsafe { std::slice::from_raw_parts(seg.ptr.as_ptr(), seg.len) };
731 let bg_ref = bg.map(|p| unsafe { &*p.ptr.as_ptr() });
732 gl_converter.draw_decoded_masks(
733 dst,
734 det,
735 seg,
736 crate::MaskOverlay {
737 background: bg_ref,
738 opacity,
739 letterbox,
740 color_mode,
741 },
742 )
743 }));
744 reply_caught(result, resp, poisoned, "DrawDecodedMasks");
745}
746
747fn handle_draw_proto_masks(
748 mut dst: SendablePtr<TensorDyn>,
749 det: SendablePtr<DetectBox>,
750 proto_data: SendablePtr<ProtoData>,
751 overlay: (
752 f32,
753 Option<SendablePtr<TensorDyn>>,
754 Option<[f32; 4]>,
755 crate::ColorMode,
756 ),
757 resp: tokio::sync::oneshot::Sender<Result<(), Error>>,
758 gl_converter: &mut GLProcessorST,
759 poisoned: &mut bool,
760) {
761 let (opacity, bg, letterbox, color_mode) = overlay;
762 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
765 let dst = unsafe { dst.ptr.as_mut() };
766 let det = unsafe { std::slice::from_raw_parts(det.ptr.as_ptr(), det.len) };
767 let bg_ref = bg.map(|p| unsafe { &*p.ptr.as_ptr() });
768 let proto_data = unsafe { proto_data.ptr.as_ref() };
769 gl_converter.draw_proto_masks(
770 dst,
771 det,
772 proto_data,
773 crate::MaskOverlay {
774 background: bg_ref,
775 opacity,
776 letterbox,
777 color_mode,
778 },
779 )
780 }));
781 reply_caught(result, resp, poisoned, "DrawProtoMasks");
782}
783
784fn handle_pbo_create(
785 size: usize,
786 resp: tokio::sync::oneshot::Sender<Result<u32, Error>>,
787 poisoned: &mut bool,
788) {
789 let result = std::panic::catch_unwind(AssertUnwindSafe(|| unsafe {
790 let mut id: u32 = 0;
791 edgefirst_gl::gl::GenBuffers(1, &mut id);
792 edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, id);
793 edgefirst_gl::gl::BufferData(
794 edgefirst_gl::gl::PIXEL_PACK_BUFFER,
795 size as isize,
796 std::ptr::null(),
797 edgefirst_gl::gl::STREAM_COPY,
798 );
799 edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, 0);
800 match check_gl_error("PboCreate", 0) {
801 Ok(()) => Ok(id),
802 Err(e) => {
803 edgefirst_gl::gl::DeleteBuffers(1, &id);
804 Err(e)
805 }
806 }
807 }));
808 reply_caught(result, resp, poisoned, "PboCreate");
809}
810
811fn handle_pbo_map(
812 buffer_id: u32,
813 size: usize,
814 resp: tokio::sync::oneshot::Sender<Result<edgefirst_tensor::PboMapping, Error>>,
815 mapped_pbo_buffers: &mut std::collections::HashSet<u32>,
816 poisoned: &mut bool,
817) {
818 if !mapped_pbo_buffers.insert(buffer_id) {
819 let _ = resp.send(Err(crate::Error::OpenGl(format!(
825 "PBO buffer {buffer_id} is already mapped in this GL context \
826 (a producer and a reconstructed cross-package consumer may \
827 both hold a live tensor over it)"
828 ))));
829 return;
830 }
831 let result = std::panic::catch_unwind(AssertUnwindSafe(|| unsafe {
832 edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, buffer_id);
833 let ptr = edgefirst_gl::gl::MapBufferRange(
834 edgefirst_gl::gl::PIXEL_PACK_BUFFER,
835 0,
836 size as isize,
837 edgefirst_gl::gl::MAP_READ_BIT | edgefirst_gl::gl::MAP_WRITE_BIT,
838 );
839 edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, 0);
840 if ptr.is_null() {
841 Err(crate::Error::OpenGl(
842 "glMapBufferRange returned null".to_string(),
843 ))
844 } else {
845 Ok(edgefirst_tensor::PboMapping {
846 ptr: ptr as *mut u8,
847 size,
848 })
849 }
850 }));
851 let mapped_ok = matches!(result, Ok(Ok(_)));
852 if !mapped_ok {
853 mapped_pbo_buffers.remove(&buffer_id);
858 }
859 reply_caught(result, resp, poisoned, "PboMap");
860}
861
862fn handle_pbo_unmap(
863 buffer_id: u32,
864 resp: tokio::sync::oneshot::Sender<Result<(), Error>>,
865 mapped_pbo_buffers: &mut std::collections::HashSet<u32>,
866 poisoned: &mut bool,
867) {
868 mapped_pbo_buffers.remove(&buffer_id);
875 let result = std::panic::catch_unwind(AssertUnwindSafe(|| unsafe {
876 edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, buffer_id);
877 let ok = edgefirst_gl::gl::UnmapBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER);
878 edgefirst_gl::gl::BindBuffer(edgefirst_gl::gl::PIXEL_PACK_BUFFER, 0);
879 if ok == edgefirst_gl::gl::FALSE {
880 Err(Error::OpenGl(
881 "PBO data was corrupted during mapping".into(),
882 ))
883 } else {
884 check_gl_error("PboUnmap", 0)
885 }
886 }));
887 reply_caught(result, resp, poisoned, "PboUnmap");
888}
889
890fn handle_pbo_delete(
891 buffer_id: u32,
892 mapped_pbo_buffers: &mut std::collections::HashSet<u32>,
893 poisoned: &mut bool,
894) {
895 mapped_pbo_buffers.remove(&buffer_id);
902 if let Err(e) = std::panic::catch_unwind(AssertUnwindSafe(|| unsafe {
903 edgefirst_gl::gl::DeleteBuffers(1, &buffer_id);
904 })) {
905 *poisoned = true;
906 log::error!(
907 "GL thread panicked during PboDelete: {}",
908 panic_message(e.as_ref()),
909 );
910 }
911}
912
913impl GLProcessorThreaded {
914 pub fn new(kind: Option<EglDisplayKind>) -> Result<Self, Error> {
917 Self::with_cache_capacity(kind, None)
918 }
919
920 pub fn with_cache_capacity(
926 kind: Option<EglDisplayKind>,
927 capacity: Option<usize>,
928 ) -> Result<Self, Error> {
929 let (send, recv) = tokio::sync::mpsc::channel::<GLProcessorMessage>(1);
930
931 let (create_ctx_send, create_ctx_recv) = tokio::sync::oneshot::channel();
932
933 let handle = std::thread::spawn(move || {
934 run_gl_worker(kind, capacity, recv, create_ctx_send);
935 });
936
937 let caps = match create_ctx_recv.blocking_recv() {
938 Ok(Err(e)) => return Err(e),
939 Err(_) => {
940 return Err(Error::Internal(
941 "GL converter error messaging closed without update".to_string(),
942 ));
943 }
944 Ok(Ok(caps)) => caps,
945 };
946
947 Ok(Self {
948 handle: Some(handle),
949 sender: Some(send),
950 caps,
951 })
952 }
953}
954
955impl ImageProcessorTrait for GLProcessorThreaded {
956 fn convert(
957 &mut self,
958 src: &TensorDyn,
959 dst: &mut TensorDyn,
960 rotation: crate::Rotation,
961 flip: Flip,
962 crop: Crop,
963 ) -> crate::Result<()> {
964 let (err_send, err_recv) = tokio::sync::oneshot::channel();
965 self.sender
966 .as_ref()
967 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
968 .blocking_send(GLProcessorMessage::ImageConvert(
969 SendablePtr {
970 ptr: NonNull::from(src),
971 len: 1,
972 },
973 SendablePtr {
974 ptr: NonNull::from(dst),
975 len: 1,
976 },
977 rotation,
978 flip,
979 crop,
980 false, err_send,
982 ))
983 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
984 err_recv.blocking_recv().map_err(|_| {
985 Error::Internal("GL converter error messaging closed without update".to_string())
986 })?
987 }
988
989 fn convert_deferred(
990 &mut self,
991 src: &TensorDyn,
992 dst: &mut TensorDyn,
993 rotation: crate::Rotation,
994 flip: Flip,
995 crop: Crop,
996 ) -> crate::Result<()> {
997 let (err_send, err_recv) = tokio::sync::oneshot::channel();
998 self.sender
999 .as_ref()
1000 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1001 .blocking_send(GLProcessorMessage::ImageConvert(
1002 SendablePtr {
1003 ptr: NonNull::from(src),
1004 len: 1,
1005 },
1006 SendablePtr {
1007 ptr: NonNull::from(dst),
1008 len: 1,
1009 },
1010 rotation,
1011 flip,
1012 crop,
1013 true, err_send,
1015 ))
1016 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1017 err_recv.blocking_recv().map_err(|_| {
1018 Error::Internal("GL converter error messaging closed without update".to_string())
1019 })?
1020 }
1021
1022 fn flush(&mut self) -> crate::Result<()> {
1023 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1024 self.sender
1025 .as_ref()
1026 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1027 .blocking_send(GLProcessorMessage::Flush(err_send))
1028 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1029 err_recv.blocking_recv().map_err(|_| {
1030 Error::Internal("GL converter error messaging closed without update".to_string())
1031 })?
1032 }
1033
1034 fn draw_decoded_masks(
1035 &mut self,
1036 dst: &mut TensorDyn,
1037 detect: &[crate::DetectBox],
1038 segmentation: &[crate::Segmentation],
1039 overlay: crate::MaskOverlay<'_>,
1040 ) -> crate::Result<()> {
1041 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1042 self.sender
1043 .as_ref()
1044 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1045 .blocking_send(GLProcessorMessage::DrawDecodedMasks(
1046 SendablePtr {
1047 ptr: NonNull::from(dst),
1048 len: 1,
1049 },
1050 SendablePtr {
1051 ptr: NonNull::new(detect.as_ptr() as *mut DetectBox).unwrap(),
1052 len: detect.len(),
1053 },
1054 SendablePtr {
1055 ptr: NonNull::new(segmentation.as_ptr() as *mut Segmentation).unwrap(),
1056 len: segmentation.len(),
1057 },
1058 overlay.opacity,
1059 overlay.background.map(|bg| SendablePtr {
1060 ptr: NonNull::from(bg).cast::<TensorDyn>(),
1061 len: 1,
1062 }),
1063 overlay.letterbox,
1064 overlay.color_mode,
1065 err_send,
1066 ))
1067 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1068 err_recv.blocking_recv().map_err(|_| {
1069 Error::Internal("GL converter error messaging closed without update".to_string())
1070 })?
1071 }
1072
1073 fn draw_proto_masks(
1074 &mut self,
1075 dst: &mut TensorDyn,
1076 detect: &[DetectBox],
1077 proto_data: &ProtoData,
1078 overlay: crate::MaskOverlay<'_>,
1079 ) -> crate::Result<()> {
1080 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1081 self.sender
1082 .as_ref()
1083 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1084 .blocking_send(GLProcessorMessage::DrawProtoMasks(
1085 SendablePtr {
1086 ptr: NonNull::from(dst),
1087 len: 1,
1088 },
1089 SendablePtr {
1090 ptr: NonNull::new(detect.as_ptr() as *mut DetectBox).unwrap(),
1091 len: detect.len(),
1092 },
1093 SendablePtr {
1094 ptr: NonNull::from(proto_data).cast::<ProtoData>(),
1095 len: 1,
1096 },
1097 overlay.opacity,
1098 overlay.background.map(|bg| SendablePtr {
1099 ptr: NonNull::from(bg).cast::<TensorDyn>(),
1100 len: 1,
1101 }),
1102 overlay.letterbox,
1103 overlay.color_mode,
1104 err_send,
1105 ))
1106 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1107 err_recv.blocking_recv().map_err(|_| {
1108 Error::Internal("GL converter error messaging closed without update".to_string())
1109 })?
1110 }
1111
1112 fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<(), crate::Error> {
1113 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1114 self.sender
1115 .as_ref()
1116 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1117 .blocking_send(GLProcessorMessage::SetColors(colors.to_vec(), err_send))
1118 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1119 err_recv.blocking_recv().map_err(|_| {
1120 Error::Internal("GL converter error messaging closed without update".to_string())
1121 })?
1122 }
1123}
1124
1125impl GLProcessorThreaded {
1126 pub fn set_colorimetry_mode(&mut self, mode: crate::ColorimetryMode) -> Result<(), Error> {
1131 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1132 self.sender
1133 .as_ref()
1134 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1135 .blocking_send(GLProcessorMessage::SetColorimetryMode(mode, err_send))
1136 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1137 err_recv.blocking_recv().map_err(|_| {
1138 Error::Internal("GL converter error messaging closed without update".to_string())
1139 })?
1140 }
1141
1142 pub fn set_int8_interpolation_mode(
1144 &mut self,
1145 mode: Int8InterpolationMode,
1146 ) -> Result<(), crate::Error> {
1147 let (err_send, err_recv) = tokio::sync::oneshot::channel();
1148 self.sender
1149 .as_ref()
1150 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1151 .blocking_send(GLProcessorMessage::SetInt8Interpolation(mode, err_send))
1152 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1153 err_recv.blocking_recv().map_err(|_| {
1154 Error::Internal("GL converter error messaging closed without update".to_string())
1155 })?
1156 }
1157
1158 pub fn egl_cache_stats(&self) -> Result<super::cache::GlCacheStats, Error> {
1165 let (send, recv) = tokio::sync::oneshot::channel();
1166 self.sender
1167 .as_ref()
1168 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1169 .blocking_send(GLProcessorMessage::EglCacheStats(send))
1170 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1171 recv.blocking_recv().map_err(|_| {
1172 Error::Internal("GL converter error messaging closed without update".to_string())
1173 })?
1174 }
1175
1176 pub fn convert_with_fence(
1187 &mut self,
1188 src: &TensorDyn,
1189 dst: &mut TensorDyn,
1190 rotation: crate::Rotation,
1191 flip: Flip,
1192 crop: Crop,
1193 ) -> Result<Option<super::CompletionFence>, Error> {
1194 use crate::ImageProcessorTrait as _;
1195 if !self.caps.native_fence_sync {
1196 self.convert(src, dst, rotation, flip, crop)?;
1199 return Ok(None);
1200 }
1201 let (send, recv) = tokio::sync::oneshot::channel();
1202 self.sender
1203 .as_ref()
1204 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1205 .blocking_send(GLProcessorMessage::ImageConvertFenced(
1206 SendablePtr {
1207 ptr: NonNull::from(src),
1208 len: 1,
1209 },
1210 SendablePtr {
1211 ptr: NonNull::from(dst),
1212 len: 1,
1213 },
1214 rotation,
1215 flip,
1216 crop,
1217 send,
1218 ))
1219 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1220 recv.blocking_recv().map_err(|_| {
1221 Error::Internal("GL converter error messaging closed without update".to_string())
1222 })?
1223 }
1224
1225 pub fn convert_stats(&self) -> Result<super::cache::ConvertStats, Error> {
1230 let (send, recv) = tokio::sync::oneshot::channel();
1231 self.sender
1232 .as_ref()
1233 .ok_or_else(|| Error::Internal("GL processor is shutting down".to_string()))?
1234 .blocking_send(GLProcessorMessage::ConvertStats(send))
1235 .map_err(|_| Error::Internal("GL converter thread exited".to_string()))?;
1236 recv.blocking_recv().map_err(|_| {
1237 Error::Internal("GL converter error messaging closed without update".to_string())
1238 })?
1239 }
1240
1241 pub fn create_pbo_image(
1243 &self,
1244 width: usize,
1245 height: usize,
1246 format: edgefirst_tensor::PixelFormat,
1247 ) -> Result<edgefirst_tensor::Tensor<u8>, Error> {
1248 let sender = self
1249 .sender
1250 .as_ref()
1251 .ok_or(Error::OpenGl("GL processor is shutting down".to_string()))?;
1252
1253 let size = pbo_elem_count(width, height, format)
1254 .filter(|&s| s != 0)
1255 .ok_or_else(|| Error::OpenGl("Invalid image dimensions".to_string()))?;
1256
1257 let (tx, rx) = tokio::sync::oneshot::channel();
1259 sender
1260 .blocking_send(GLProcessorMessage::PboCreate(size, tx))
1261 .map_err(|_| Error::OpenGl("GL thread channel closed".to_string()))?;
1262 let buffer_id = rx
1263 .blocking_recv()
1264 .map_err(|_| Error::OpenGl("GL thread did not respond".to_string()))??;
1265
1266 let ops: std::sync::Arc<dyn edgefirst_tensor::PboOps> = std::sync::Arc::new(GlPboOps {
1267 sender: sender.downgrade(),
1268 });
1269
1270 let shape = pbo_shape(width, height, format);
1271
1272 let pbo_tensor =
1273 edgefirst_tensor::PboTensor::<u8>::from_pbo(buffer_id, size, &shape, None, ops)
1274 .map_err(|e| Error::OpenGl(format!("PBO tensor creation failed: {e:?}")))?;
1275 let mut tensor = edgefirst_tensor::Tensor::from_pbo(pbo_tensor)
1276 .map_err(|e| Error::OpenGl(format!("PBO tensor wrap failed: {e:?}")))?;
1277 tensor
1278 .set_format(format)
1279 .map_err(|e| Error::OpenGl(format!("Failed to set format on PBO tensor: {e:?}")))?;
1280 register_pbo_cuda(&mut tensor, buffer_id, size, sender);
1287 Ok(tensor)
1288 }
1289
1290 pub(crate) fn create_pbo_image_dtype(
1296 &self,
1297 width: usize,
1298 height: usize,
1299 format: edgefirst_tensor::PixelFormat,
1300 dtype: edgefirst_tensor::DType,
1301 ) -> Result<TensorDyn, Error> {
1302 let sender = self
1303 .sender
1304 .as_ref()
1305 .ok_or(Error::OpenGl("GL processor is shutting down".to_string()))?;
1306
1307 let elems = pbo_elem_count(width, height, format)
1308 .filter(|&e| e != 0)
1309 .ok_or_else(|| Error::OpenGl("Invalid image dimensions".to_string()))?;
1310
1311 let size = elems
1312 .checked_mul(dtype.size())
1313 .ok_or_else(|| Error::OpenGl("PBO size overflow".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 map_err = |e: edgefirst_tensor::Error| {
1331 Error::OpenGl(format!("PBO tensor creation failed: {e:?}"))
1332 };
1333 let set_err = |e: edgefirst_tensor::Error| {
1334 Error::OpenGl(format!("Failed to set format on PBO tensor: {e:?}"))
1335 };
1336
1337 match dtype {
1338 edgefirst_tensor::DType::U8 => {
1339 let pbo =
1340 edgefirst_tensor::PboTensor::<u8>::from_pbo(buffer_id, size, &shape, None, ops)
1341 .map_err(map_err)?;
1342 let mut t = edgefirst_tensor::Tensor::from_pbo(pbo).map_err(map_err)?;
1343 t.set_format(format).map_err(set_err)?;
1344 register_pbo_cuda(&mut t, buffer_id, size, sender);
1345 Ok(TensorDyn::from(t))
1346 }
1347 edgefirst_tensor::DType::F16 => {
1348 let pbo = edgefirst_tensor::PboTensor::<edgefirst_tensor::f16>::from_pbo(
1349 buffer_id, size, &shape, None, ops,
1350 )
1351 .map_err(map_err)?;
1352 let mut t = edgefirst_tensor::Tensor::from_pbo(pbo).map_err(map_err)?;
1353 t.set_format(format).map_err(set_err)?;
1354 register_pbo_cuda(&mut t, buffer_id, size, sender);
1355 Ok(TensorDyn::from(t))
1356 }
1357 edgefirst_tensor::DType::F32 => {
1358 let pbo = edgefirst_tensor::PboTensor::<f32>::from_pbo(
1359 buffer_id, size, &shape, None, ops,
1360 )
1361 .map_err(map_err)?;
1362 let mut t = edgefirst_tensor::Tensor::from_pbo(pbo).map_err(map_err)?;
1363 t.set_format(format).map_err(set_err)?;
1364 register_pbo_cuda(&mut t, buffer_id, size, sender);
1365 Ok(TensorDyn::from(t))
1366 }
1367 other => Err(Error::OpenGl(format!("unsupported PBO dtype {other:?}"))),
1368 }
1369 }
1370
1371 pub(crate) fn transfer_backend(&self) -> TransferBackend {
1373 self.caps.transfer_backend
1374 }
1375
1376 pub(crate) fn supported_render_dtypes(&self) -> crate::RenderDtypeSupport {
1383 self.caps.render_dtypes
1384 }
1385}
1386
1387impl Drop for GLProcessorThreaded {
1388 fn drop(&mut self) {
1389 drop(self.sender.take());
1390 let _ = self.handle.take().and_then(|h| h.join().ok());
1391 }
1392}
1393
1394#[cfg(test)]
1398#[cfg_attr(coverage_nightly, coverage(off))]
1399mod tests {
1400 use super::{pbo_elem_count, pbo_shape, GLProcessorThreaded, GlPboOps};
1401 use edgefirst_tensor::{PixelFormat, TensorTrait};
1402
1403 #[test]
1404 fn elem_count_per_format() {
1405 assert_eq!(pbo_elem_count(8, 4, PixelFormat::Rgba), Some(8 * 4 * 4));
1407 assert_eq!(pbo_elem_count(8, 4, PixelFormat::Rgb), Some(8 * 4 * 3));
1409 assert_eq!(pbo_elem_count(8, 4, PixelFormat::Nv12), Some(8 * 4 * 3 / 2));
1411 assert_eq!(pbo_elem_count(8, 4, PixelFormat::Nv16), Some(8 * 4 * 2));
1413 }
1414
1415 #[test]
1416 fn elem_count_overflow_is_none() {
1417 assert_eq!(pbo_elem_count(usize::MAX, 2, PixelFormat::Rgba), None);
1419 assert_eq!(pbo_elem_count(usize::MAX, 1, PixelFormat::Rgb), None);
1421 }
1422
1423 #[test]
1424 fn shape_per_format() {
1425 assert_eq!(pbo_shape(8, 4, PixelFormat::PlanarRgb), vec![3, 4, 8]);
1427 assert_eq!(pbo_shape(8, 4, PixelFormat::Nv12), vec![4 * 3 / 2, 8]);
1429 assert_eq!(pbo_shape(8, 4, PixelFormat::Nv16), vec![4 * 2, 8]);
1431 assert_eq!(pbo_shape(8, 4, PixelFormat::Rgba), vec![4, 8, 4]);
1433 }
1434
1435 #[test]
1453 #[cfg(any(target_os = "linux", target_os = "windows"))] fn concurrent_maps_from_two_independent_handles_on_one_buffer_do_not_corrupt_gl_state() {
1455 let gl = match GLProcessorThreaded::new(None) {
1456 Ok(gl) => gl,
1457 Err(e) => {
1458 eprintln!(
1459 "SKIPPED: concurrent_maps_from_two_independent_handles... - \
1460 OpenGL not available: {e:?}"
1461 );
1462 return;
1463 }
1464 };
1465 let tensor_a = match gl.create_pbo_image(64, 64, PixelFormat::Rgba) {
1466 Ok(t) => t,
1467 Err(e) => {
1468 eprintln!(
1469 "SKIPPED: concurrent_maps_from_two_independent_handles... - \
1470 PBO not supported: {e:?}"
1471 );
1472 return;
1473 }
1474 };
1475 let pbo_a = tensor_a.as_pbo().expect("just created a PBO tensor");
1476 let buffer_id = pbo_a.buffer_id();
1477 let size = tensor_a.capacity_bytes();
1478 let shape = tensor_a.shape().to_vec();
1479
1480 let ops_b: std::sync::Arc<dyn edgefirst_tensor::PboOps> = std::sync::Arc::new(GlPboOps {
1484 sender: gl.sender.as_ref().unwrap().downgrade(),
1485 });
1486 let tensor_b =
1487 edgefirst_tensor::PboTensor::<u8>::from_pbo(buffer_id, size, &shape, None, ops_b)
1488 .expect("from_pbo against a real, already-created buffer_id must succeed");
1489
1490 let map_a = tensor_a.map().expect("first map must succeed");
1492
1493 let map_b_result = tensor_b.map();
1497 let Err(map_b_err) = map_b_result else {
1505 panic!(
1506 "a second, independent handle mapping the SAME real buffer while the \
1507 first mapping is still open must be refused, not silently succeed"
1508 );
1509 };
1510 let msg = format!("{map_b_err:?}");
1511 assert!(
1512 msg.contains("already mapped"),
1513 "must be refused specifically by the GL-thread-level reservation check \
1514 (mapped_pbo_buffers), not some other error path: {msg}"
1515 );
1516
1517 drop(map_a);
1518
1519 let map_b_after = tensor_b.map();
1523 assert!(
1524 map_b_after.is_ok(),
1525 "after the first handle's mapping releases, the second handle must be able to \
1526 map the same buffer: {map_b_after:?}"
1527 );
1528 }
1529}