1use crate::{
9 ColorRange, Error, ErrorKind, PixelFormat, Plane, Result, TransferMode, VideoDimensions,
10 VideoFrame,
11};
12
13#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
15pub enum GraphicsApi {
16 NativeOpenGl,
17 WebGl,
18}
19
20#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
22pub struct ContextIdentity(pub u64);
23
24#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
26pub struct ExecutionOwner(pub u64);
27
28#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
30pub enum ResourceKind {
31 Texture2d,
32 Framebuffer,
33}
34
35#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
37pub enum ResourceOwnership {
38 Caller,
39 Library,
40}
41
42#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
44pub struct GraphicsResource {
45 api: GraphicsApi,
46 context: ContextIdentity,
47 owner: ExecutionOwner,
48 kind: ResourceKind,
49 handle: u64,
50 dimensions: VideoDimensions,
51 pixel_format: PixelFormat,
52 color_range: ColorRange,
53 orientation: Orientation,
54 ownership: ResourceOwnership,
55}
56
57impl GraphicsResource {
58 #[allow(clippy::too_many_arguments)]
59 pub const fn new(
60 api: GraphicsApi,
61 context: ContextIdentity,
62 owner: ExecutionOwner,
63 kind: ResourceKind,
64 handle: u64,
65 dimensions: VideoDimensions,
66 pixel_format: PixelFormat,
67 color_range: ColorRange,
68 orientation: Orientation,
69 ownership: ResourceOwnership,
70 ) -> Self {
71 Self {
72 api,
73 context,
74 owner,
75 kind,
76 handle,
77 dimensions,
78 pixel_format,
79 color_range,
80 orientation,
81 ownership,
82 }
83 }
84
85 pub const fn api(self) -> GraphicsApi {
86 self.api
87 }
88 pub const fn context(self) -> ContextIdentity {
89 self.context
90 }
91 pub const fn owner(self) -> ExecutionOwner {
92 self.owner
93 }
94 pub const fn kind(self) -> ResourceKind {
95 self.kind
96 }
97 pub const fn handle(self) -> u64 {
98 self.handle
99 }
100 pub const fn dimensions(self) -> VideoDimensions {
101 self.dimensions
102 }
103 pub const fn pixel_format(self) -> PixelFormat {
104 self.pixel_format
105 }
106 pub const fn color_range(self) -> ColorRange {
107 self.color_range
108 }
109 pub const fn orientation(self) -> Orientation {
110 self.orientation
111 }
112 pub const fn ownership(self) -> ResourceOwnership {
113 self.ownership
114 }
115
116 pub fn release(self, adapter: &mut dyn GraphicsAdapter) -> Result<()> {
118 validate_resource(adapter, self)?;
119 if self.ownership == ResourceOwnership::Library {
120 adapter.delete(self)?;
121 }
122 Ok(())
123 }
124}
125
126#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
128pub enum Orientation {
129 TopLeft,
130 BottomLeft,
131}
132
133#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
135pub enum ScaleFilter {
136 Nearest,
137 Linear,
138}
139
140#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
142pub enum ColorConversion {
143 None,
144 FullToLimited,
145 LimitedToFull,
146}
147
148#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
150pub enum TransferStage {
151 RepackStride,
152 Scale { filter: ScaleFilter },
153 FlipVertical,
154 ConvertPixelFormat { from: PixelFormat, to: PixelFormat },
155 ConvertColor { conversion: ColorConversion },
156}
157
158#[derive(Clone, Debug, Eq, PartialEq)]
160pub struct TransferCapability {
161 pub mode: TransferMode,
162 pub stages: Vec<TransferStage>,
163 pub reason: Option<String>,
164}
165
166impl TransferCapability {
167 fn supported(mode: TransferMode, stages: Vec<TransferStage>) -> Self {
168 Self {
169 mode,
170 stages,
171 reason: None,
172 }
173 }
174
175 fn unsupported(reason: impl Into<String>) -> Self {
176 Self {
177 mode: TransferMode::Unsupported,
178 stages: Vec::new(),
179 reason: Some(reason.into()),
180 }
181 }
182
183 pub fn is_supported(&self) -> bool {
184 self.mode != TransferMode::Unsupported
185 }
186}
187
188#[derive(Clone, Copy, Debug, Eq, PartialEq)]
190pub struct TransferPolicy {
191 required: Option<TransferMode>,
192 scale_filter: ScaleFilter,
193}
194
195impl TransferPolicy {
196 pub const fn any() -> Self {
197 Self {
198 required: None,
199 scale_filter: ScaleFilter::Nearest,
200 }
201 }
202 pub const fn require(mode: TransferMode) -> Self {
203 Self {
204 required: Some(mode),
205 scale_filter: ScaleFilter::Nearest,
206 }
207 }
208 pub const fn with_scale_filter(mut self, filter: ScaleFilter) -> Self {
209 self.scale_filter = filter;
210 self
211 }
212 pub const fn required(self) -> Option<TransferMode> {
213 self.required
214 }
215}
216
217impl Default for TransferPolicy {
218 fn default() -> Self {
219 Self::any()
220 }
221}
222
223#[derive(Clone, Copy, Debug)]
225pub struct CpuFrameSource<'a> {
226 pub frame: &'a VideoFrame,
227 pub orientation: Orientation,
228}
229
230#[derive(Debug)]
232pub struct CpuPlaneDestination<'a> {
233 pub data: &'a mut [u8],
234 pub stride: usize,
235}
236
237#[derive(Debug)]
239pub struct CpuFrameDestination<'a> {
240 pub dimensions: VideoDimensions,
241 pub pixel_format: PixelFormat,
242 pub color_range: ColorRange,
243 pub orientation: Orientation,
244 pub planes: Vec<CpuPlaneDestination<'a>>,
245}
246
247#[derive(Clone, Copy, Debug)]
248pub enum FrameSource<'a> {
249 Cpu(CpuFrameSource<'a>),
250 Graphics(GraphicsResource),
251}
252
253#[derive(Debug)]
254pub enum FrameDestination<'a> {
255 Cpu(CpuFrameDestination<'a>),
256 Graphics(GraphicsResource),
257}
258
259pub trait GraphicsAdapter {
265 fn api(&self) -> GraphicsApi;
266 fn context_identity(&self) -> ContextIdentity;
267 fn execution_owner(&self) -> ExecutionOwner;
268 fn is_current(&self) -> bool;
269 fn is_context_lost(&self) -> bool;
270 fn capability(
271 &self,
272 source: FrameSource<'_>,
273 destination: &FrameDestination<'_>,
274 ) -> TransferMode;
275 fn upload(
276 &mut self,
277 source: CpuFrameSource<'_>,
278 destination: GraphicsResource,
279 stages: &[TransferStage],
280 ) -> Result<()>;
281 fn readback(
282 &mut self,
283 source: GraphicsResource,
284 destination: CpuFrameDestination<'_>,
285 stages: &[TransferStage],
286 ) -> Result<()>;
287 fn copy(
288 &mut self,
289 source: GraphicsResource,
290 destination: GraphicsResource,
291 stages: &[TransferStage],
292 ) -> Result<()>;
293 fn delete(&mut self, resource: GraphicsResource) -> Result<()>;
294}
295
296pub fn inspect_transfer(
298 adapter: Option<&dyn GraphicsAdapter>,
299 source: FrameSource<'_>,
300 destination: &FrameDestination<'_>,
301 policy: TransferPolicy,
302) -> TransferCapability {
303 let mut stages = stages_for(source, destination, policy.scale_filter);
304 let mode = match (source, destination) {
305 (FrameSource::Cpu(_), FrameDestination::Cpu(_)) => TransferMode::CpuCopy,
306 _ => {
307 let Some(adapter) = adapter else {
308 return TransferCapability::unsupported(
309 "a graphics transfer requires a context adapter",
310 );
311 };
312 if let Err(error) = validate_endpoints(adapter, source, destination) {
313 return TransferCapability::unsupported(error.message());
314 }
315 adapter.capability(source, destination)
316 }
317 };
318 if mode == TransferMode::Unsupported {
319 return TransferCapability::unsupported("the backend does not support this transfer");
320 }
321 if let Some(required) = policy.required {
322 if required == TransferMode::Unsupported || required != mode {
323 return TransferCapability::unsupported(format!(
324 "strict policy requires {required:?}, but the backend reports {mode:?}"
325 ));
326 }
327 }
328 if mode == TransferMode::Shared && !stages.is_empty() {
329 return TransferCapability::unsupported(
330 "the backend reported a shared transfer that requires conversion stages",
331 );
332 }
333 if mode == TransferMode::Shared {
334 stages.clear();
335 }
336 TransferCapability::supported(mode, stages)
337}
338
339pub fn execute_transfer(
341 mut adapter: Option<&mut dyn GraphicsAdapter>,
342 source: FrameSource<'_>,
343 destination: FrameDestination<'_>,
344 policy: TransferPolicy,
345) -> Result<TransferCapability> {
346 let capability = inspect_transfer(adapter.as_deref(), source, &destination, policy);
347 if !capability.is_supported() {
348 return Err(Error::new(
349 ErrorKind::Unsupported,
350 capability
351 .reason
352 .clone()
353 .unwrap_or_else(|| "unsupported frame transfer".into()),
354 ));
355 }
356 match (source, destination) {
357 (FrameSource::Cpu(source), FrameDestination::Cpu(destination)) => {
358 transfer_cpu(source, destination, policy.scale_filter)?
359 }
360 (FrameSource::Cpu(source), FrameDestination::Graphics(destination)) => adapter
361 .as_mut()
362 .expect("validated adapter")
363 .upload(source, destination, &capability.stages)?,
364 (FrameSource::Graphics(source), FrameDestination::Cpu(destination)) => adapter
365 .as_mut()
366 .expect("validated adapter")
367 .readback(source, destination, &capability.stages)?,
368 (FrameSource::Graphics(source), FrameDestination::Graphics(destination)) => adapter
369 .as_mut()
370 .expect("validated adapter")
371 .copy(source, destination, &capability.stages)?,
372 }
373 Ok(capability)
374}
375
376fn validate_endpoints(
377 adapter: &dyn GraphicsAdapter,
378 source: FrameSource<'_>,
379 destination: &FrameDestination<'_>,
380) -> Result<()> {
381 if adapter.is_context_lost() {
382 return Err(graphics("the graphics context is lost"));
383 }
384 if !adapter.is_current() {
385 return Err(graphics("the graphics context is not current"));
386 }
387 if let FrameSource::Graphics(resource) = source {
388 validate_resource(adapter, resource)?;
389 }
390 if let FrameDestination::Graphics(resource) = destination {
391 validate_resource(adapter, *resource)?;
392 }
393 Ok(())
394}
395
396fn validate_resource(adapter: &dyn GraphicsAdapter, resource: GraphicsResource) -> Result<()> {
397 if adapter.is_context_lost() {
398 return Err(graphics("the graphics context is lost"));
399 }
400 if !adapter.is_current() {
401 return Err(graphics("the graphics context is not current"));
402 }
403 if resource.api != adapter.api() {
404 return Err(graphics("graphics resource API does not match the adapter"));
405 }
406 if resource.context != adapter.context_identity() {
407 return Err(graphics("graphics resource belongs to a different context"));
408 }
409 if resource.owner != adapter.execution_owner() {
410 return Err(graphics(
411 "graphics resource belongs to a different execution owner",
412 ));
413 }
414 Ok(())
415}
416
417fn stages_for(
418 source: FrameSource<'_>,
419 destination: &FrameDestination<'_>,
420 filter: ScaleFilter,
421) -> Vec<TransferStage> {
422 let (source_dimensions, source_format, source_range, source_orientation, source_stride) =
423 source_metadata(source);
424 let (
425 destination_dimensions,
426 destination_format,
427 destination_range,
428 destination_orientation,
429 destination_stride,
430 ) = destination_metadata(destination);
431 let mut stages = Vec::new();
432 if source_stride != destination_stride {
433 stages.push(TransferStage::RepackStride);
434 }
435 if source_dimensions != destination_dimensions {
436 stages.push(TransferStage::Scale { filter });
437 }
438 if source_orientation != destination_orientation {
439 stages.push(TransferStage::FlipVertical);
440 }
441 if source_format != destination_format {
442 stages.push(TransferStage::ConvertPixelFormat {
443 from: source_format,
444 to: destination_format,
445 });
446 }
447 if source_range != destination_range {
448 let conversion = match (source_range, destination_range) {
449 (ColorRange::Full, ColorRange::Limited) => ColorConversion::FullToLimited,
450 (ColorRange::Limited, ColorRange::Full) => ColorConversion::LimitedToFull,
451 _ => ColorConversion::None,
452 };
453 stages.push(TransferStage::ConvertColor { conversion });
454 }
455 stages
456}
457
458fn source_metadata(
459 source: FrameSource<'_>,
460) -> (
461 VideoDimensions,
462 PixelFormat,
463 ColorRange,
464 Orientation,
465 Option<usize>,
466) {
467 match source {
468 FrameSource::Cpu(cpu) => (
469 cpu.frame.dimensions,
470 cpu.frame.pixel_format,
471 cpu.frame.color_range,
472 cpu.orientation,
473 cpu.frame.planes.first().map(|plane| plane.stride),
474 ),
475 FrameSource::Graphics(resource) => (
476 resource.dimensions,
477 resource.pixel_format,
478 resource.color_range,
479 resource.orientation,
480 None,
481 ),
482 }
483}
484
485fn destination_metadata(
486 destination: &FrameDestination<'_>,
487) -> (
488 VideoDimensions,
489 PixelFormat,
490 ColorRange,
491 Orientation,
492 Option<usize>,
493) {
494 match destination {
495 FrameDestination::Cpu(cpu) => (
496 cpu.dimensions,
497 cpu.pixel_format,
498 cpu.color_range,
499 cpu.orientation,
500 cpu.planes.first().map(|plane| plane.stride),
501 ),
502 FrameDestination::Graphics(resource) => (
503 resource.dimensions,
504 resource.pixel_format,
505 resource.color_range,
506 resource.orientation,
507 None,
508 ),
509 }
510}
511
512fn transfer_cpu(
513 source: CpuFrameSource<'_>,
514 mut destination: CpuFrameDestination<'_>,
515 filter: ScaleFilter,
516) -> Result<()> {
517 if filter == ScaleFilter::Linear && source.frame.dimensions != destination.dimensions {
518 return Err(Error::new(
519 ErrorKind::Unsupported,
520 "linear CPU scaling is not implemented",
521 ));
522 }
523 validate_cpu_destination(&destination)?;
524 if !is_packed(source.frame.pixel_format) || !is_packed(destination.pixel_format) {
525 return Err(Error::new(
526 ErrorKind::Unsupported,
527 "CPU conversion currently supports packed RGBA, BGRA, RGB, and gray frames",
528 ));
529 }
530 let source_width = usize::try_from(source.frame.dimensions.width)
531 .map_err(|_| limit("source width cannot be represented"))?;
532 let source_height = usize::try_from(source.frame.dimensions.height)
533 .map_err(|_| limit("source height cannot be represented"))?;
534 let destination_width = usize::try_from(destination.dimensions.width)
535 .map_err(|_| limit("destination width cannot be represented"))?;
536 let destination_height = usize::try_from(destination.dimensions.height)
537 .map_err(|_| limit("destination height cannot be represented"))?;
538 let source_plane = &source.frame.planes[0];
539 let destination_plane = &mut destination.planes[0];
540 for destination_y in 0..destination_height {
541 let logical_source_y = destination_y
542 .checked_mul(source_height)
543 .ok_or_else(|| limit("vertical scale overflow"))?
544 / destination_height;
545 let source_y = orient_row(logical_source_y, source_height, source.orientation);
546 let output_y = orient_row(destination_y, destination_height, destination.orientation);
547 for destination_x in 0..destination_width {
548 let source_x = destination_x
549 .checked_mul(source_width)
550 .ok_or_else(|| limit("horizontal scale overflow"))?
551 / destination_width;
552 let mut rgba = read_pixel(source_plane, source.frame.pixel_format, source_x, source_y)?;
553 convert_range(&mut rgba, source.frame.color_range, destination.color_range);
554 write_pixel(
555 destination_plane,
556 destination.pixel_format,
557 destination_x,
558 output_y,
559 rgba,
560 )?;
561 }
562 }
563 Ok(())
564}
565
566fn validate_cpu_destination(destination: &CpuFrameDestination<'_>) -> Result<()> {
567 if destination.planes.len() != 1 || !is_packed(destination.pixel_format) {
568 return Err(Error::new(
569 ErrorKind::Unsupported,
570 "CPU destination must contain one supported packed plane",
571 ));
572 }
573 let width = usize::try_from(destination.dimensions.width)
574 .map_err(|_| limit("destination width cannot be represented"))?;
575 let height = usize::try_from(destination.dimensions.height)
576 .map_err(|_| limit("destination height cannot be represented"))?;
577 let row_bytes = width
578 .checked_mul(bytes_per_pixel(destination.pixel_format)?)
579 .ok_or_else(|| limit("destination row size overflow"))?;
580 let plane = &destination.planes[0];
581 if plane.stride < row_bytes {
582 return Err(invalid("destination stride is too small"));
583 }
584 let length = plane
585 .stride
586 .checked_mul(height)
587 .ok_or_else(|| limit("destination plane size overflow"))?;
588 if plane.data.len() < length {
589 return Err(invalid(
590 "destination plane is shorter than its stride and height",
591 ));
592 }
593 Ok(())
594}
595
596fn is_packed(format: PixelFormat) -> bool {
597 matches!(
598 format,
599 PixelFormat::Rgba8 | PixelFormat::Bgra8 | PixelFormat::Rgb8 | PixelFormat::Gray8
600 )
601}
602
603fn bytes_per_pixel(format: PixelFormat) -> Result<usize> {
604 match format {
605 PixelFormat::Rgba8 | PixelFormat::Bgra8 => Ok(4),
606 PixelFormat::Rgb8 => Ok(3),
607 PixelFormat::Gray8 => Ok(1),
608 _ => Err(Error::new(
609 ErrorKind::Unsupported,
610 "pixel format is not a packed CPU format",
611 )),
612 }
613}
614
615fn orient_row(row: usize, height: usize, orientation: Orientation) -> usize {
616 match orientation {
617 Orientation::TopLeft => row,
618 Orientation::BottomLeft => height - 1 - row,
619 }
620}
621
622fn read_pixel(plane: &Plane, format: PixelFormat, x: usize, y: usize) -> Result<[u8; 4]> {
623 let offset = y
624 .checked_mul(plane.stride)
625 .and_then(|row| row.checked_add(x.checked_mul(bytes_per_pixel(format).ok()?)?))
626 .ok_or_else(|| limit("source pixel offset overflow"))?;
627 let data = &plane.data;
628 match format {
629 PixelFormat::Rgba8 => Ok([
630 data[offset],
631 data[offset + 1],
632 data[offset + 2],
633 data[offset + 3],
634 ]),
635 PixelFormat::Bgra8 => Ok([
636 data[offset + 2],
637 data[offset + 1],
638 data[offset],
639 data[offset + 3],
640 ]),
641 PixelFormat::Rgb8 => Ok([data[offset], data[offset + 1], data[offset + 2], 255]),
642 PixelFormat::Gray8 => Ok([data[offset], data[offset], data[offset], 255]),
643 _ => Err(Error::new(
644 ErrorKind::Unsupported,
645 "pixel format is not a packed CPU format",
646 )),
647 }
648}
649
650fn write_pixel(
651 plane: &mut CpuPlaneDestination<'_>,
652 format: PixelFormat,
653 x: usize,
654 y: usize,
655 rgba: [u8; 4],
656) -> Result<()> {
657 let offset = y
658 .checked_mul(plane.stride)
659 .and_then(|row| row.checked_add(x.checked_mul(bytes_per_pixel(format).ok()?)?))
660 .ok_or_else(|| limit("destination pixel offset overflow"))?;
661 match format {
662 PixelFormat::Rgba8 => plane.data[offset..offset + 4].copy_from_slice(&rgba),
663 PixelFormat::Bgra8 => {
664 plane.data[offset..offset + 4].copy_from_slice(&[rgba[2], rgba[1], rgba[0], rgba[3]])
665 }
666 PixelFormat::Rgb8 => plane.data[offset..offset + 3].copy_from_slice(&rgba[..3]),
667 PixelFormat::Gray8 => {
668 plane.data[offset] = ((u16::from(rgba[0]) * 77
669 + u16::from(rgba[1]) * 150
670 + u16::from(rgba[2]) * 29
671 + 128)
672 >> 8) as u8
673 }
674 _ => {
675 return Err(Error::new(
676 ErrorKind::Unsupported,
677 "pixel format is not a packed CPU format",
678 ));
679 }
680 }
681 Ok(())
682}
683
684fn convert_range(rgba: &mut [u8; 4], source: ColorRange, destination: ColorRange) {
685 if source == destination {
686 return;
687 }
688 for component in &mut rgba[..3] {
689 *component = match (source, destination) {
690 (ColorRange::Full, ColorRange::Limited) => {
691 (16 + (u32::from(*component) * 219 + 127) / 255) as u8
692 }
693 (ColorRange::Limited, ColorRange::Full) => {
694 ((u32::from(component.saturating_sub(16)) * 255 + 109) / 219).min(255) as u8
695 }
696 _ => *component,
697 };
698 }
699}
700
701fn graphics(message: &str) -> Error {
702 Error::new(ErrorKind::Graphics, message)
703}
704fn invalid(message: &str) -> Error {
705 Error::new(ErrorKind::InvalidInput, message)
706}
707fn limit(message: &str) -> Error {
708 Error::new(ErrorKind::ResourceLimit, message)
709}
710
711#[cfg(test)]
712mod tests {
713 use super::*;
714 use crate::Limits;
715
716 fn dimensions(width: u32, height: u32) -> VideoDimensions {
717 VideoDimensions::new(width, height, &Limits::default()).unwrap()
718 }
719
720 fn rgba_frame(width: u32, height: u32, stride: usize, data: Vec<u8>) -> VideoFrame {
721 VideoFrame::new(
722 dimensions(width, height),
723 PixelFormat::Rgba8,
724 ColorRange::Full,
725 vec![Plane { data, stride }],
726 &Limits::default(),
727 )
728 .unwrap()
729 }
730
731 fn resource(api: GraphicsApi, ownership: ResourceOwnership) -> GraphicsResource {
732 GraphicsResource::new(
733 api,
734 ContextIdentity(7),
735 ExecutionOwner(9),
736 ResourceKind::Texture2d,
737 42,
738 dimensions(2, 2),
739 PixelFormat::Rgba8,
740 ColorRange::Full,
741 Orientation::BottomLeft,
742 ownership,
743 )
744 }
745
746 #[derive(Debug)]
747 struct FakeAdapter {
748 api: GraphicsApi,
749 context: ContextIdentity,
750 owner: ExecutionOwner,
751 current: bool,
752 lost: bool,
753 mode: TransferMode,
754 calls: Vec<&'static str>,
755 }
756
757 impl FakeAdapter {
758 fn new(api: GraphicsApi, mode: TransferMode) -> Self {
759 Self {
760 api,
761 context: ContextIdentity(7),
762 owner: ExecutionOwner(9),
763 current: true,
764 lost: false,
765 mode,
766 calls: Vec::new(),
767 }
768 }
769 }
770
771 impl GraphicsAdapter for FakeAdapter {
772 fn api(&self) -> GraphicsApi {
773 self.api
774 }
775 fn context_identity(&self) -> ContextIdentity {
776 self.context
777 }
778 fn execution_owner(&self) -> ExecutionOwner {
779 self.owner
780 }
781 fn is_current(&self) -> bool {
782 self.current
783 }
784 fn is_context_lost(&self) -> bool {
785 self.lost
786 }
787 fn capability(
788 &self,
789 _source: FrameSource<'_>,
790 _destination: &FrameDestination<'_>,
791 ) -> TransferMode {
792 self.mode
793 }
794 fn upload(
795 &mut self,
796 _source: CpuFrameSource<'_>,
797 _destination: GraphicsResource,
798 _stages: &[TransferStage],
799 ) -> Result<()> {
800 self.calls.push("upload");
801 Ok(())
802 }
803 fn readback(
804 &mut self,
805 _source: GraphicsResource,
806 mut destination: CpuFrameDestination<'_>,
807 _stages: &[TransferStage],
808 ) -> Result<()> {
809 destination.planes[0].data.fill(23);
810 self.calls.push("readback");
811 Ok(())
812 }
813 fn copy(
814 &mut self,
815 _source: GraphicsResource,
816 _destination: GraphicsResource,
817 _stages: &[TransferStage],
818 ) -> Result<()> {
819 self.calls.push("copy");
820 Ok(())
821 }
822 fn delete(&mut self, _resource: GraphicsResource) -> Result<()> {
823 self.calls.push("delete");
824 Ok(())
825 }
826 }
827
828 #[test]
829 fn cpu_transfer_handles_stride_scaling_orientation_and_format() {
830 let source = rgba_frame(
831 2,
832 2,
833 12,
834 vec![
835 255, 0, 0, 255, 0, 255, 0, 255, 9, 9, 9, 9, 0, 0, 255, 255, 255, 255, 255, 255, 9,
836 9, 9, 9,
837 ],
838 );
839 let mut output = vec![0; 4];
840 let destination = CpuFrameDestination {
841 dimensions: dimensions(1, 1),
842 pixel_format: PixelFormat::Bgra8,
843 color_range: ColorRange::Full,
844 orientation: Orientation::BottomLeft,
845 planes: vec![CpuPlaneDestination {
846 data: &mut output,
847 stride: 4,
848 }],
849 };
850 let capability = execute_transfer(
851 None,
852 FrameSource::Cpu(CpuFrameSource {
853 frame: &source,
854 orientation: Orientation::TopLeft,
855 }),
856 FrameDestination::Cpu(destination),
857 TransferPolicy::any(),
858 )
859 .unwrap();
860 assert_eq!(output, [0, 0, 255, 255]);
861 assert_eq!(capability.mode, TransferMode::CpuCopy);
862 assert!(capability.stages.contains(&TransferStage::RepackStride));
863 assert!(capability.stages.contains(&TransferStage::Scale {
864 filter: ScaleFilter::Nearest
865 }));
866 assert!(capability.stages.contains(&TransferStage::FlipVertical));
867 }
868
869 #[test]
870 fn cpu_transfer_converts_color_range() {
871 let source = rgba_frame(1, 1, 4, vec![0, 255, 128, 255]);
872 let mut output = vec![0; 4];
873 let destination = CpuFrameDestination {
874 dimensions: dimensions(1, 1),
875 pixel_format: PixelFormat::Rgba8,
876 color_range: ColorRange::Limited,
877 orientation: Orientation::TopLeft,
878 planes: vec![CpuPlaneDestination {
879 data: &mut output,
880 stride: 4,
881 }],
882 };
883 execute_transfer(
884 None,
885 FrameSource::Cpu(CpuFrameSource {
886 frame: &source,
887 orientation: Orientation::TopLeft,
888 }),
889 FrameDestination::Cpu(destination),
890 TransferPolicy::any(),
891 )
892 .unwrap();
893 assert_eq!(output, [16, 235, 126, 255]);
894 }
895
896 #[test]
897 fn native_gl_upload_and_webgl_readback_use_the_adapter() {
898 let frame = rgba_frame(2, 2, 8, vec![0; 16]);
899 let mut native = FakeAdapter::new(GraphicsApi::NativeOpenGl, TransferMode::GpuCopy);
900 execute_transfer(
901 Some(&mut native),
902 FrameSource::Cpu(CpuFrameSource {
903 frame: &frame,
904 orientation: Orientation::TopLeft,
905 }),
906 FrameDestination::Graphics(resource(
907 GraphicsApi::NativeOpenGl,
908 ResourceOwnership::Caller,
909 )),
910 TransferPolicy::require(TransferMode::GpuCopy),
911 )
912 .unwrap();
913 assert_eq!(native.calls, ["upload"]);
914
915 let mut output = vec![0; 16];
916 let destination = CpuFrameDestination {
917 dimensions: dimensions(2, 2),
918 pixel_format: PixelFormat::Rgba8,
919 color_range: ColorRange::Full,
920 orientation: Orientation::TopLeft,
921 planes: vec![CpuPlaneDestination {
922 data: &mut output,
923 stride: 8,
924 }],
925 };
926 let mut web = FakeAdapter::new(GraphicsApi::WebGl, TransferMode::CpuCopy);
927 execute_transfer(
928 Some(&mut web),
929 FrameSource::Graphics(resource(GraphicsApi::WebGl, ResourceOwnership::Caller)),
930 FrameDestination::Cpu(destination),
931 TransferPolicy::require(TransferMode::CpuCopy),
932 )
933 .unwrap();
934 assert_eq!(output, vec![23; 16]);
935 assert_eq!(web.calls, ["readback"]);
936 }
937
938 #[test]
939 fn strict_policy_rejects_fallback_without_calling_backend() {
940 let frame = rgba_frame(2, 2, 8, vec![0; 16]);
941 let mut adapter = FakeAdapter::new(GraphicsApi::NativeOpenGl, TransferMode::CpuCopy);
942 let error = execute_transfer(
943 Some(&mut adapter),
944 FrameSource::Cpu(CpuFrameSource {
945 frame: &frame,
946 orientation: Orientation::TopLeft,
947 }),
948 FrameDestination::Graphics(resource(
949 GraphicsApi::NativeOpenGl,
950 ResourceOwnership::Caller,
951 )),
952 TransferPolicy::require(TransferMode::Shared),
953 )
954 .unwrap_err();
955 assert_eq!(error.kind(), ErrorKind::Unsupported);
956 assert!(adapter.calls.is_empty());
957 }
958
959 #[test]
960 fn shared_graphics_transfer_is_reported_when_no_conversion_is_needed() {
961 let mut adapter = FakeAdapter::new(GraphicsApi::NativeOpenGl, TransferMode::Shared);
962 let graphics = resource(GraphicsApi::NativeOpenGl, ResourceOwnership::Caller);
963 let capability = execute_transfer(
964 Some(&mut adapter),
965 FrameSource::Graphics(graphics),
966 FrameDestination::Graphics(graphics),
967 TransferPolicy::require(TransferMode::Shared),
968 )
969 .unwrap();
970 assert_eq!(capability.mode, TransferMode::Shared);
971 assert!(capability.stages.is_empty());
972 assert_eq!(adapter.calls, ["copy"]);
973 }
974
975 #[test]
976 fn context_loss_identity_owner_and_current_state_are_validated() {
977 let destination = FrameDestination::Graphics(resource(
978 GraphicsApi::NativeOpenGl,
979 ResourceOwnership::Caller,
980 ));
981 let frame = rgba_frame(2, 2, 8, vec![0; 16]);
982 let source = FrameSource::Cpu(CpuFrameSource {
983 frame: &frame,
984 orientation: Orientation::TopLeft,
985 });
986 let mut adapter = FakeAdapter::new(GraphicsApi::NativeOpenGl, TransferMode::GpuCopy);
987 adapter.lost = true;
988 assert_eq!(
989 inspect_transfer(Some(&adapter), source, &destination, TransferPolicy::any()).mode,
990 TransferMode::Unsupported
991 );
992 adapter.lost = false;
993 adapter.current = false;
994 assert_eq!(
995 inspect_transfer(Some(&adapter), source, &destination, TransferPolicy::any()).mode,
996 TransferMode::Unsupported
997 );
998 adapter.current = true;
999 adapter.context = ContextIdentity(99);
1000 assert_eq!(
1001 inspect_transfer(Some(&adapter), source, &destination, TransferPolicy::any()).mode,
1002 TransferMode::Unsupported
1003 );
1004 adapter.context = ContextIdentity(7);
1005 adapter.owner = ExecutionOwner(99);
1006 assert_eq!(
1007 inspect_transfer(Some(&adapter), source, &destination, TransferPolicy::any()).mode,
1008 TransferMode::Unsupported
1009 );
1010 }
1011
1012 #[test]
1013 fn cleanup_deletes_only_library_owned_resources() {
1014 let mut adapter = FakeAdapter::new(GraphicsApi::WebGl, TransferMode::GpuCopy);
1015 resource(GraphicsApi::WebGl, ResourceOwnership::Caller)
1016 .release(&mut adapter)
1017 .unwrap();
1018 assert!(adapter.calls.is_empty());
1019 resource(GraphicsApi::WebGl, ResourceOwnership::Library)
1020 .release(&mut adapter)
1021 .unwrap();
1022 assert_eq!(adapter.calls, ["delete"]);
1023 }
1024}