use super::*;
use drmkit_planes::LayerId;
#[derive(Debug, Default)]
struct FakeSource {
format: SourceFormat,
next: Option<Result<AcquiredBuffer, SourceError>>,
released: Vec<u32>,
released_with_fence: usize,
fresh: bool,
}
impl LayerBufferSource for FakeSource {
fn acquire(&mut self) -> Result<AcquiredBuffer, SourceError> {
self.next.take().unwrap_or(Err(SourceError::WouldBlock))
}
fn release(&mut self, acquired: AcquiredBuffer) {
self.released.push(acquired.fb_id);
}
fn release_with_fence(
&mut self,
acquired: AcquiredBuffer,
_release_fence: Option<drmkit_sync::SyncFence>,
) {
self.released_with_fence += 1;
self.release(acquired);
}
fn has_fresh_content(&self) -> bool {
self.fresh
}
fn format(&self) -> SourceFormat {
self.format
}
}
#[derive(Debug, Default)]
struct MinimalSource;
impl LayerBufferSource for MinimalSource {
fn acquire(&mut self) -> Result<AcquiredBuffer, SourceError> {
Ok(AcquiredBuffer {
fb_id: 7,
..AcquiredBuffer::default()
})
}
fn release(&mut self, _acquired: AcquiredBuffer) {}
fn format(&self) -> SourceFormat {
SourceFormat::default()
}
}
#[test]
fn would_block_is_distinct_from_failure() {
let mut source = FakeSource::default();
assert!(matches!(source.acquire(), Err(SourceError::WouldBlock)));
source.next = Some(Err(SourceError::Failed(rustix::io::Errno::INVAL)));
assert!(matches!(source.acquire(), Err(SourceError::Failed(_))));
source.next = Some(Err(SourceError::Unsupported));
assert!(matches!(source.acquire(), Err(SourceError::Unsupported)));
}
#[test]
fn a_real_failure_is_not_mistaken_for_flow_control() {
let failure = SourceError::Failed(rustix::io::Errno::AGAIN);
assert!(
!matches!(failure, SourceError::WouldBlock),
"an errno that happens to be EAGAIN is still a failure if the source \
reported it as one -- the variant carries the intent, not the number"
);
}
#[test]
fn fresh_content_defaults_to_the_safe_answer() {
assert!(
MinimalSource.has_fresh_content(),
"a source that cannot introspect its buffer must report changed, or a \
real update is silently skipped"
);
let idle = FakeSource::default();
assert!(!idle.has_fresh_content());
}
#[test]
fn binding_model_defaults_to_scene_submits_fb_id() {
assert_eq!(
MinimalSource.binding_model(),
BindingModel::SceneSubmitsFbId
);
assert_eq!(BindingModel::default(), BindingModel::SceneSubmitsFbId);
}
#[test]
fn release_fence_is_opt_in() {
assert!(
!MinimalSource.wants_release_fence(),
"the scene only requests an OUT_FENCE when a source asks, so this must \
cost nothing by default"
);
}
#[test]
fn release_with_fence_forwards_by_default() {
let mut source = MinimalSource;
source.release_with_fence(
AcquiredBuffer {
fb_id: 3,
..AcquiredBuffer::default()
},
None,
);
}
#[test]
fn release_with_fence_is_overridable() {
let mut source = FakeSource::default();
source.release_with_fence(
AcquiredBuffer {
fb_id: 5,
..AcquiredBuffer::default()
},
None,
);
assert_eq!(source.released_with_fence, 1);
assert_eq!(source.released, vec![5]);
}
#[test]
fn composition_escapes_default_to_unsupported() {
let mut source = MinimalSource;
assert!(matches!(
source.map(drmkit_dumb::MapAccess::Read),
Err(SourceError::Unsupported)
));
assert!(matches!(
source.export_dma_buf(),
Err(SourceError::Unsupported)
));
}
#[test]
fn driver_binding_hooks_are_inert_by_default() {
let mut source = MinimalSource;
assert!(source.bind_to_plane(31).is_ok());
source.unbind_from_plane(31);
source.on_session_paused();
}
#[test]
fn moving_an_acquired_buffer_transfers_it_intact() {
let buffer = AcquiredBuffer {
fb_id: 42,
token: 3,
..AcquiredBuffer::default()
};
let moved = buffer;
assert_eq!(moved.fb_id, 42);
assert_eq!(moved.token, 3);
}
#[test]
fn an_acquired_buffer_defaults_to_full_frame_damage() {
let buffer = AcquiredBuffer::default();
assert!(
buffer.damage.is_empty(),
"empty means full-frame: correct, just not power-optimal"
);
assert!(buffer.acquire_fence.is_none());
}
#[test]
fn commit_report_accounting_balances() {
let report = CommitReport {
layers_total: 5,
layers_assigned: 2,
layers_composited: 1,
layers_unassigned: 1,
layers_skipped_no_frame: 1,
..CommitReport::default()
};
assert!(report.accounting_balances());
let unbalanced = CommitReport {
layers_total: 5,
layers_assigned: 2,
..CommitReport::default()
};
assert!(
!unbalanced.accounting_balances(),
"a layer counted nowhere must show up as an imbalance"
);
}
#[test]
fn unhonored_pins_do_not_affect_the_accounting() {
let report = CommitReport {
layers_total: 1,
layers_assigned: 1,
pin_requests_unhonored: 1,
..CommitReport::default()
};
assert!(report.accounting_balances());
}
#[test]
fn fast_path_implies_no_test_commits() {
let good = CommitReport {
fb_delta_fast_path: true,
test_commits_issued: 0,
..CommitReport::default()
};
assert!(good.fast_path_consistent());
let contradictory = CommitReport {
fb_delta_fast_path: true,
test_commits_issued: 1,
..CommitReport::default()
};
assert!(!contradictory.fast_path_consistent());
let warm = CommitReport {
fb_delta_fast_path: false,
test_commits_issued: 1,
..CommitReport::default()
};
assert!(warm.fast_path_consistent());
}
#[test]
fn an_empty_report_is_balanced_and_consistent() {
let report = CommitReport::default();
assert!(report.accounting_balances());
assert!(report.fast_path_consistent());
assert!(report.placements.is_empty());
assert!(!report.fb_delta_fast_path);
}
#[test]
fn placements_describe_each_layer_outcome() {
let report = CommitReport {
layers_total: 2,
layers_assigned: 1,
layers_unassigned: 1,
placements: vec![
LayerPlacement {
layer: LayerId(1),
placement: Placement::AssignedToPlane,
plane_id: Some(31),
plane_rotation_bits: 0b0001,
},
LayerPlacement {
layer: LayerId(2),
placement: Placement::Unassigned,
plane_id: None,
plane_rotation_bits: 0,
},
],
..CommitReport::default()
};
assert!(report.accounting_balances());
assert_eq!(report.placements.len(), 2);
assert_eq!(
report.placements[1].plane_id, None,
"unassigned has no plane"
);
assert_eq!(
report.placements[1].plane_rotation_bits, 0,
"and no rotation mask to report"
);
}
fn acquisition(layer: u64, fb_id: u32) -> Acquisition {
Acquisition::new(
LayerId(layer),
AcquiredBuffer {
fb_id,
..AcquiredBuffer::default()
},
)
}
fn released_fbs(released: &Released) -> Vec<u32> {
released
.acquisitions
.iter()
.map(|a| a.buffer.fb_id)
.collect()
}
#[test]
fn release_is_deferred_two_commits_deep() {
let mut queue = ReleaseQueue::new();
let first = queue.commit_succeeded(vec![acquisition(1, 100)], None, |_| false);
assert!(
first.is_empty(),
"frame 1's buffer is being scanned out; releasing it here is the tear"
);
let second = queue.commit_succeeded(vec![acquisition(1, 200)], None, |_| false);
assert!(
second.is_empty(),
"frame 1's buffer only leaves the screen when frame 2's flip completes, \
which the *next* commit observes"
);
let third = queue.commit_succeeded(vec![acquisition(1, 300)], None, |_| false);
assert_eq!(
released_fbs(&third),
vec![100],
"two commits deep: frame 1 retires now"
);
assert_eq!(third.reason, ReleaseReason::Retired);
let fourth = queue.commit_succeeded(vec![acquisition(1, 400)], None, |_| false);
assert_eq!(released_fbs(&fourth), vec![200], "then frame 2, in order");
}
#[test]
fn the_ring_holds_exactly_two_generations() {
let mut queue = ReleaseQueue::new();
assert!(queue.is_empty());
for frame in 0..20u32 {
queue.commit_succeeded(vec![acquisition(1, frame)], None, |_| false);
assert!(
queue.in_flight() <= 2,
"frame {frame}: {} buffers in flight, expected at most 2",
queue.in_flight()
);
}
assert_eq!(queue.in_flight(), 2, "steady state holds N-1 and N-2");
}
#[test]
fn a_failed_commit_releases_its_own_acquisitions_at_once() {
let mut queue = ReleaseQueue::new();
queue.commit_succeeded(vec![acquisition(1, 100)], None, |_| false);
let failed = queue.commit_failed(vec![acquisition(1, 200)]);
assert_eq!(
released_fbs(&failed),
vec![200],
"this frame's buffer comes straight back"
);
assert_eq!(failed.reason, ReleaseReason::CommitFailed);
}
#[test]
fn a_failed_commit_leaves_the_in_flight_generations_alone() {
let mut queue = ReleaseQueue::new();
queue.commit_succeeded(vec![acquisition(1, 100)], None, |_| false);
queue.commit_succeeded(vec![acquisition(1, 200)], None, |_| false);
let before = queue.in_flight();
queue.commit_failed(vec![acquisition(1, 300)]);
assert_eq!(queue.in_flight(), before, "nothing on screen changed");
let next = queue.commit_succeeded(vec![acquisition(1, 400)], None, |_| false);
assert_eq!(
released_fbs(&next),
vec![100],
"a rejected commit must not skip a generation"
);
}
#[test]
fn a_test_commit_releases_immediately_and_is_distinguishable() {
let mut queue = ReleaseQueue::new();
let released = queue.test_committed(vec![acquisition(1, 100)]);
assert_eq!(released_fbs(&released), vec![100]);
assert_eq!(released.reason, ReleaseReason::TestOnly);
assert!(queue.is_empty(), "a test commit holds nothing in flight");
}
#[test]
fn draining_returns_every_held_buffer_oldest_first() {
let mut queue = ReleaseQueue::new();
queue.commit_succeeded(vec![acquisition(1, 100)], None, |_| false);
queue.commit_succeeded(vec![acquisition(1, 200)], None, |_| false);
assert_eq!(queue.in_flight(), 2);
let drained = queue.drain();
assert_eq!(
released_fbs(&drained),
vec![100, 200],
"oldest first, matching the order a running scene would have used"
);
assert_eq!(drained.reason, ReleaseReason::Drained);
assert!(queue.is_empty(), "draining leaves nothing behind");
}
#[test]
fn draining_an_empty_queue_is_harmless() {
let mut queue = ReleaseQueue::new();
let drained = queue.drain();
assert!(drained.is_empty());
assert_eq!(drained.len(), 0);
}
#[test]
fn every_layers_buffer_retires_together() {
let mut queue = ReleaseQueue::new();
queue.commit_succeeded(vec![acquisition(1, 100), acquisition(2, 101)], None, |_| {
false
});
queue.commit_succeeded(vec![acquisition(1, 200), acquisition(2, 201)], None, |_| {
false
});
let third =
queue.commit_succeeded(vec![acquisition(1, 300), acquisition(2, 301)], None, |_| {
false
});
let mut fbs = released_fbs(&third);
fbs.sort_unstable();
assert_eq!(
fbs,
vec![100, 101],
"both layers' frame-1 buffers retire together"
);
}
#[test]
fn a_skipped_layer_does_not_break_the_rotation() {
let mut queue = ReleaseQueue::new();
queue.commit_succeeded(vec![acquisition(1, 100), acquisition(2, 101)], None, |_| {
false
});
queue.commit_succeeded(vec![acquisition(1, 200)], None, |_| false);
let third = queue.commit_succeeded(vec![acquisition(1, 300)], None, |_| false);
let mut fbs = released_fbs(&third);
fbs.sort_unstable();
assert_eq!(fbs, vec![100, 101]);
let fourth = queue.commit_succeeded(vec![acquisition(1, 400)], None, |_| false);
assert_eq!(
released_fbs(&fourth),
vec![200],
"only layer 1 contributed to frame 2"
);
}
#[test]
fn the_release_fence_is_stamped_only_where_wanted() {
use std::os::fd::AsFd as _;
let mut queue = ReleaseQueue::new();
queue.commit_succeeded(vec![acquisition(1, 100), acquisition(2, 101)], None, |_| {
false
});
let raw = rustix::event::eventfd(0, rustix::event::EventfdFlags::CLOEXEC).expect("eventfd");
let fence = drmkit_sync::SyncFence::import(raw.as_fd()).expect("import");
queue.commit_succeeded(vec![acquisition(1, 200)], Some(&fence), |layer| {
layer == LayerId(1)
});
let retired = queue.commit_succeeded(vec![acquisition(1, 300)], None, |_| false);
let stamped: Vec<(u32, bool)> = retired
.acquisitions
.iter()
.map(|a| (a.buffer.fb_id, a.release_fence.is_some()))
.collect();
assert_eq!(stamped.len(), 2);
assert!(
stamped.contains(&(100, true)),
"layer 1 opted in and must get the fence: {stamped:?}"
);
assert!(
stamped.contains(&(101, false)),
"layer 2 did not opt in and must not: {stamped:?}"
);
}
#[test]
fn no_out_fence_means_nothing_is_stamped() {
let mut queue = ReleaseQueue::new();
queue.commit_succeeded(vec![acquisition(1, 100)], None, |_| true);
queue.commit_succeeded(vec![acquisition(1, 200)], None, |_| true);
let retired = queue.commit_succeeded(vec![acquisition(1, 300)], None, |_| true);
assert!(
retired
.acquisitions
.iter()
.all(|a| a.release_fence.is_none()),
"a CRTC without OUT_FENCE_PTR stamps nothing"
);
}
#[test]
fn pending_flip_tracks_whether_teardown_is_safe() {
let mut pending = ScenePendingFlip::new();
assert!(!pending.is_armed(), "nothing committed yet");
pending.arm();
assert!(
pending.is_armed(),
"a commit that armed PAGE_FLIP_EVENT still references its framebuffer"
);
pending.landed();
assert!(!pending.is_armed(), "once dispatched, teardown is safe");
}
use drmkit_planes::{Layer as PlaneLayer, PropTag};
fn lowering_input() -> LoweringInput {
LoweringInput {
display: DisplayParams {
dst_rect: Rect {
x: 10,
y: 20,
w: 640,
h: 480,
},
..DisplayParams::default()
},
format: SourceFormat {
fourcc: 0x3234_5258, modifier: 0,
width: 1920,
height: 1080,
},
binding: BindingModel::SceneSubmitsFbId,
fb_id: 42,
crtc_id: 7,
default_zpos: None,
}
}
#[test]
fn lowering_writes_the_destination_rectangle_in_whole_pixels() {
let mut plane_layer = PlaneLayer::new();
lower_layer(&lowering_input(), &mut plane_layer);
assert_eq!(plane_layer.property(PropTag::CrtcX), Some(10));
assert_eq!(plane_layer.property(PropTag::CrtcY), Some(20));
assert_eq!(plane_layer.property(PropTag::CrtcW), Some(640));
assert_eq!(plane_layer.property(PropTag::CrtcH), Some(480));
}
#[test]
fn an_unset_source_rectangle_resolves_to_the_buffer_extent() {
let mut plane_layer = PlaneLayer::new();
lower_layer(&lowering_input(), &mut plane_layer);
assert_eq!(
plane_layer.property(PropTag::SrcW),
Some(to_16_16(1920)),
"an unset source width must become the buffer width, never 0"
);
assert_eq!(plane_layer.property(PropTag::SrcH), Some(to_16_16(1080)));
assert_ne!(plane_layer.property(PropTag::SrcW), Some(0));
}
#[test]
fn an_explicit_source_rectangle_is_kept_and_converted_to_fixed_point() {
let mut input = lowering_input();
input.display.src_rect = Rect {
x: 8,
y: 16,
w: 320,
h: 240,
};
let mut plane_layer = PlaneLayer::new();
lower_layer(&input, &mut plane_layer);
assert_eq!(plane_layer.property(PropTag::SrcX), Some(to_16_16(8)));
assert_eq!(plane_layer.property(PropTag::SrcY), Some(to_16_16(16)));
assert_eq!(plane_layer.property(PropTag::SrcW), Some(320 << 16));
assert_eq!(plane_layer.property(PropTag::SrcH), Some(240 << 16));
}
#[test]
fn crtc_id_is_always_written() {
let mut plane_layer = PlaneLayer::new();
lower_layer(&lowering_input(), &mut plane_layer);
assert_eq!(plane_layer.property(PropTag::CrtcId), Some(7));
let mut input = lowering_input();
input.binding = BindingModel::DriverOwnsBinding;
let mut driver_owned = PlaneLayer::new();
lower_layer(&input, &mut driver_owned);
assert_eq!(driver_owned.property(PropTag::CrtcId), Some(7));
}
#[test]
fn a_driver_owned_binding_skips_fb_id_and_is_marked_external() {
let mut input = lowering_input();
input.binding = BindingModel::DriverOwnsBinding;
let mut plane_layer = PlaneLayer::new();
lower_layer(&input, &mut plane_layer);
assert_eq!(
plane_layer.property(PropTag::FbId),
None,
"the scene must not write FB_ID for a driver-owned binding"
);
assert!(plane_layer.is_externally_bound());
}
#[test]
fn a_scene_owned_binding_writes_fb_id_and_is_not_external() {
let mut plane_layer = PlaneLayer::new();
lower_layer(&lowering_input(), &mut plane_layer);
assert_eq!(plane_layer.property(PropTag::FbId), Some(42));
assert!(!plane_layer.is_externally_bound());
}
#[test]
fn format_and_modifier_reach_the_property_bag() {
let mut input = lowering_input();
input.format.modifier = 0x0800_0000_0000_0001;
let mut plane_layer = PlaneLayer::new();
lower_layer(&input, &mut plane_layer);
assert_eq!(plane_layer.format(), Some(0x3234_5258));
assert_eq!(plane_layer.modifier(), 0x0800_0000_0000_0001);
}
#[test]
fn zpos_is_left_unwritten_unless_requested() {
let mut plane_layer = PlaneLayer::new();
lower_layer(&lowering_input(), &mut plane_layer);
assert_eq!(
plane_layer.property(PropTag::Zpos),
None,
"an unrequested zpos must not be written as 0"
);
let mut explicit = lowering_input();
explicit.display.zpos = Some(3);
let mut with_zpos = PlaneLayer::new();
lower_layer(&explicit, &mut with_zpos);
assert_eq!(with_zpos.property(PropTag::Zpos), Some(3));
}
#[test]
fn the_default_zpos_hint_only_applies_when_unset() {
let mut hinted = lowering_input();
hinted.default_zpos = Some(1);
let mut plane_layer = PlaneLayer::new();
lower_layer(&hinted, &mut plane_layer);
assert_eq!(plane_layer.property(PropTag::Zpos), Some(1));
hinted.display.zpos = Some(4);
let mut explicit = PlaneLayer::new();
lower_layer(&hinted, &mut explicit);
assert_eq!(explicit.property(PropTag::Zpos), Some(4));
}
#[test]
fn rotation_is_written_only_when_non_zero() {
let mut plane_layer = PlaneLayer::new();
lower_layer(&lowering_input(), &mut plane_layer);
assert_eq!(plane_layer.property(PropTag::Rotation), None);
let mut rotated = lowering_input();
rotated.display.rotation = 0b0010;
let mut with_rotation = PlaneLayer::new();
lower_layer(&rotated, &mut with_rotation);
assert_eq!(with_rotation.property(PropTag::Rotation), Some(0b0010));
}
#[test]
fn alpha_is_written_only_when_requested() {
let mut plane_layer = PlaneLayer::new();
lower_layer(&lowering_input(), &mut plane_layer);
assert_eq!(plane_layer.property(PropTag::Alpha), None);
let mut translucent = lowering_input();
translucent.display.alpha = Some(0x8000);
let mut with_alpha = PlaneLayer::new();
lower_layer(&translucent, &mut with_alpha);
assert_eq!(with_alpha.property(PropTag::Alpha), Some(0x8000));
}
#[test]
fn a_negative_destination_position_round_trips() {
let mut input = lowering_input();
input.display.dst_rect.x = -64;
input.display.dst_rect.y = -32;
let mut plane_layer = PlaneLayer::new();
lower_layer(&input, &mut plane_layer);
let rect = plane_layer.crtc_rect();
assert_eq!(rect.x, -64, "a plane may extend off the left edge");
assert_eq!(rect.y, -32);
}
#[test]
fn a_one_to_one_layer_does_not_read_as_scaled() {
let mut input = lowering_input();
input.display.dst_rect = Rect {
x: 0,
y: 0,
w: 1920,
h: 1080,
};
let mut plane_layer = PlaneLayer::new();
lower_layer(&input, &mut plane_layer);
assert!(
!plane_layer.requires_scaling(),
"a full-extent source into a matching destination is 1:1"
);
input.display.dst_rect.w = 1280;
let mut scaled = PlaneLayer::new();
lower_layer(&input, &mut scaled);
assert!(scaled.requires_scaling());
}
#[test]
fn a_fixed_source_rectangle_scales_on_its_exact_size() {
let scaled = |fixed: Option<FixedRect>| {
let mut input = lowering_input();
input.display.src_rect = Rect {
x: 0,
y: 0,
w: 10,
h: 10,
}; input.display.dst_rect = Rect {
x: 0,
y: 0,
w: 800,
h: 600,
};
input.display.src_rect_fixed = fixed;
let mut plane_layer = PlaneLayer::new();
lower_layer(&input, &mut plane_layer);
plane_layer.requires_scaling()
};
let rect = |x, w| {
Some(FixedRect {
x,
y: 0,
w,
h: 600 << 16,
})
};
assert!(
!scaled(rect(0x8000, 800 << 16)),
"a half-pixel offset alone is no scale"
);
assert!(scaled(rect(0, (800 << 16) - 0x8000)), "799.5 px onto 800");
assert!(scaled(None), "back to the whole-pixel rect, 10 onto 800");
assert!(scaled(rect(0, (800 << 16) + 0x4000)), "800.25 px onto 800");
}
#[test]
fn a_fixed_source_rectangle_is_written_as_given() {
let mut input = lowering_input();
input.display.src_rect_fixed = Some(FixedRect {
x: 0x8000,
y: 0x0040,
w: (320 << 16) + 0x100,
h: 0,
});
let mut plane_layer = PlaneLayer::new();
lower_layer(&input, &mut plane_layer);
assert_eq!(plane_layer.property(PropTag::SrcX), Some(0x8000));
assert_eq!(plane_layer.property(PropTag::SrcY), Some(0x0040));
assert_eq!(
plane_layer.property(PropTag::SrcW),
Some((320 << 16) + 0x100)
);
assert_eq!(plane_layer.property(PropTag::SrcH), Some(to_16_16(1080)));
}
#[test]
fn the_canvas_rounds_a_fixed_rectangle_to_whole_pixels() {
use crate::scene::round_16_16;
assert_eq!(round_16_16(0), 0);
assert_eq!(round_16_16(0x7fff), 0);
assert_eq!(round_16_16(0x8000), 1);
assert_eq!(round_16_16((799 << 16) + 0x8000), 800);
assert_eq!(round_16_16(u32::MAX), 0x1_0000);
}
fn real_commit() -> CommitKind {
CommitKind::Real { arms_flip: true }
}
#[test]
fn a_successful_commit_rotates_and_arms_the_flip() {
let mut lifecycle = FrameLifecycle::new();
let first = lifecycle.finalize(
real_commit(),
KernelResult::Ok,
vec![acquisition(1, 100)],
CommitReport::default(),
None,
|_| false,
);
assert!(first.released.is_empty(), "nothing has left the screen yet");
assert!(lifecycle.has_pending_flip(), "a real commit armed a flip");
assert!(!first.suspended);
lifecycle.finalize(
real_commit(),
KernelResult::Ok,
vec![acquisition(1, 200)],
CommitReport::default(),
None,
|_| false,
);
let third = lifecycle.finalize(
real_commit(),
KernelResult::Ok,
vec![acquisition(1, 300)],
CommitReport::default(),
None,
|_| false,
);
assert_eq!(released_fbs(&third.released), vec![100]);
}
#[test]
fn a_commit_without_a_flip_event_leaves_no_pending_flip() {
let mut lifecycle = FrameLifecycle::new();
lifecycle.finalize(
CommitKind::Real { arms_flip: false },
KernelResult::Ok,
vec![acquisition(1, 100)],
CommitReport::default(),
None,
|_| false,
);
assert!(!lifecycle.has_pending_flip());
}
#[test]
fn an_ordinary_rejection_releases_but_does_not_suspend() {
let mut lifecycle = FrameLifecycle::new();
lifecycle.finalize(
real_commit(),
KernelResult::Ok,
vec![acquisition(1, 100)],
CommitReport::default(),
None,
|_| false,
);
let rejected = lifecycle.finalize(
real_commit(),
KernelResult::Rejected,
vec![acquisition(1, 200)],
CommitReport::default(),
None,
|_| false,
);
assert_eq!(released_fbs(&rejected.released), vec![200]);
assert_eq!(rejected.released.reason, ReleaseReason::CommitFailed);
assert!(
!rejected.suspended,
"a non-EACCES failure must not suspend the scene -- the frame heals"
);
assert!(!lifecycle.is_suspended());
}
#[test]
fn lost_master_suspends_the_scene() {
let mut lifecycle = FrameLifecycle::new();
let outcome = lifecycle.finalize(
real_commit(),
KernelResult::NotMaster,
vec![acquisition(1, 100)],
CommitReport::default(),
None,
|_| false,
);
assert!(outcome.suspended);
assert!(lifecycle.is_suspended());
assert_eq!(
released_fbs(&outcome.released),
vec![100],
"the buffers still come straight back"
);
lifecycle.resume();
assert!(!lifecycle.is_suspended(), "a session resume lifts it");
}
#[test]
fn a_rejected_fast_path_frame_invalidates_the_cached_allocation() {
let mut lifecycle = FrameLifecycle::new();
let report = CommitReport {
fb_delta_fast_path: true,
test_commits_issued: 0,
..CommitReport::default()
};
let outcome = lifecycle.finalize(
real_commit(),
KernelResult::Rejected,
vec![acquisition(1, 100)],
report,
None,
|_| false,
);
assert!(
outcome.invalidate_allocation,
"a fast-path frame the kernel refused must drop the cache"
);
assert!(
!outcome.report.fb_delta_fast_path,
"and the report must stop claiming a fast path that did not hold"
);
assert!(lifecycle.has_warned_fast_path_reject());
assert!(!outcome.suspended, "it is still a recoverable rejection");
}
#[test]
fn an_ordinary_rejection_does_not_invalidate_the_cache() {
let mut lifecycle = FrameLifecycle::new();
let report = CommitReport {
fb_delta_fast_path: false,
test_commits_issued: 1,
..CommitReport::default()
};
let outcome = lifecycle.finalize(
real_commit(),
KernelResult::Rejected,
vec![acquisition(1, 100)],
report,
None,
|_| false,
);
assert!(!outcome.invalidate_allocation);
assert!(!lifecycle.has_warned_fast_path_reject());
}
#[test]
fn a_test_commit_leaves_scene_state_untouched() {
let mut lifecycle = FrameLifecycle::new();
lifecycle.finalize(
real_commit(),
KernelResult::Ok,
vec![acquisition(1, 100)],
CommitReport::default(),
None,
|_| false,
);
let in_flight = lifecycle.buffers_in_flight();
lifecycle.flip_landed();
let outcome = lifecycle.finalize(
CommitKind::Test,
KernelResult::Ok,
vec![acquisition(1, 200)],
CommitReport::default(),
None,
|_| false,
);
assert_eq!(released_fbs(&outcome.released), vec![200]);
assert_eq!(outcome.released.reason, ReleaseReason::TestOnly);
assert_eq!(
lifecycle.buffers_in_flight(),
in_flight,
"a test applied nothing, so nothing rotated"
);
assert!(!lifecycle.has_pending_flip(), "and armed no flip");
}
#[test]
fn a_failed_test_commit_is_still_just_a_test() {
let mut lifecycle = FrameLifecycle::new();
let outcome = lifecycle.finalize(
CommitKind::Test,
KernelResult::NotMaster,
vec![acquisition(1, 100)],
CommitReport::default(),
None,
|_| false,
);
assert_eq!(outcome.released.reason, ReleaseReason::TestOnly);
assert!(
!outcome.suspended,
"a probe that could not run is not a reason to stop the scene"
);
}
#[test]
fn draining_does_not_land_the_flip() {
let mut lifecycle = FrameLifecycle::new();
lifecycle.finalize(
real_commit(),
KernelResult::Ok,
vec![acquisition(1, 100)],
CommitReport::default(),
None,
|_| false,
);
assert!(lifecycle.has_pending_flip());
let drained = lifecycle.drain();
assert_eq!(drained.reason, ReleaseReason::Drained);
assert_eq!(lifecycle.buffers_in_flight(), 0);
assert!(
lifecycle.has_pending_flip(),
"draining returns buffers; it does not wait on the kernel, so the flip \
is still outstanding and teardown is still unsafe"
);
lifecycle.flip_landed();
assert!(!lifecycle.has_pending_flip());
}
#[test]
fn the_acquire_tally_separates_skips_from_acquisitions() {
let mut tally = AcquireTally::default();
assert!(tally.record(true));
assert!(!tally.record(false));
assert!(tally.record(true));
assert_eq!(tally.acquired, 2);
assert_eq!(tally.skipped_no_frame, 1);
assert_eq!(
tally.considered(),
3,
"every layer is accounted for exactly once"
);
}
#[test]
fn skipped_layers_keep_the_report_balanced() {
let mut tally = AcquireTally::default();
tally.record(true);
tally.record(false);
tally.record(false);
let report = CommitReport {
layers_total: tally.considered(),
layers_assigned: tally.acquired,
layers_skipped_no_frame: tally.skipped_no_frame,
..CommitReport::default()
};
assert!(
report.accounting_balances(),
"a skipped layer must be counted somewhere, or the invariant breaks"
);
}
#[test]
fn a_plane_that_takes_a_fence_is_armed_with_it() {
assert_eq!(
crate::fence_action(Some(77)),
crate::FenceAction::Arm { property_id: 77 },
);
}
#[test]
fn a_plane_without_the_property_falls_back_to_a_cpu_wait() {
assert_eq!(crate::fence_action(None), crate::FenceAction::CpuWait);
}
#[test]
fn a_canvas_zpos_above_the_plane_range_is_clamped_not_sent() {
let mut map = PlanePropertyMap::new();
map.zpos_range.insert(7, (1, 17));
assert_eq!(
map.clamp_zpos(7, 19),
17,
"above the range clamps to the top"
);
assert_eq!(
map.clamp_zpos(7, 0),
1,
"below the range clamps to the bottom"
);
assert_eq!(map.clamp_zpos(7, 9), 9, "inside the range is left alone");
assert_eq!(
map.clamp_zpos(7, 17),
17,
"the top of the range is inside it"
);
assert_eq!(
map.clamp_zpos(8, 19),
19,
"no known range means no clamping"
);
}
#[test]
fn a_degenerate_zpos_clamps_to_the_only_value_it_will_take() {
let mut map = PlanePropertyMap::new();
map.zpos_range.insert(42, (0, 0));
assert_eq!(
map.clamp_zpos(42, 3),
0,
"the only legal value, not the one that would be refused"
);
assert_eq!(map.clamp_zpos(42, 0), 0, "and asking for it is unchanged");
}
fn reservation_plane(
id: u32,
plane_type: drmkit_planes::PlaneType,
zpos: Option<(u64, u64)>,
) -> drmkit_planes::PlaneCapabilities {
drmkit_planes::PlaneCapabilities {
id,
possible_crtcs: 0b1,
plane_type,
formats: vec![drmkit_fmt::fourcc::ARGB8888, drmkit_fmt::fourcc::XRGB8888],
zpos_min: zpos.map(|(min, _)| min),
zpos_max: zpos.map(|(_, max)| max),
..drmkit_planes::PlaneCapabilities::default()
}
}
fn reservation_layer(zpos: Option<u64>) -> drmkit_planes::Layer {
let mut layer = drmkit_planes::Layer::default();
if let Some(zpos) = zpos {
layer.set_property(drmkit_planes::PropTag::Zpos, zpos);
}
layer
}
#[test]
fn a_pinned_primary_with_no_eligible_layer_is_reserved() {
let registry = drmkit_planes::PlaneRegistry::from_capabilities(vec![
reservation_plane(31, drmkit_planes::PlaneType::Primary, Some((2, 2))),
reservation_plane(32, drmkit_planes::PlaneType::Overlay, Some((3, 9))),
reservation_plane(33, drmkit_planes::PlaneType::Overlay, Some((3, 9))),
]);
let layers = [reservation_layer(Some(3)), reservation_layer(Some(3))];
let refs: Vec<drmkit_planes::LayerRef<'_>> = layers
.iter()
.enumerate()
.map(|(i, layer)| drmkit_planes::LayerRef {
id: drmkit_planes::LayerId(i as u64 + 1),
layer,
})
.collect();
assert_eq!(
crate::scene::canvas_reservation(true, &refs, ®istry, 0, None),
vec![31],
"the primary is held out of the disable pass"
);
}
#[test]
fn a_layer_matching_the_pin_makes_the_anchor_unnecessary() {
let registry = drmkit_planes::PlaneRegistry::from_capabilities(vec![
reservation_plane(31, drmkit_planes::PlaneType::Primary, Some((2, 2))),
reservation_plane(32, drmkit_planes::PlaneType::Overlay, Some((3, 9))),
]);
let layers = [reservation_layer(Some(2)), reservation_layer(Some(3))];
let refs: Vec<drmkit_planes::LayerRef<'_>> = layers
.iter()
.enumerate()
.map(|(i, layer)| drmkit_planes::LayerRef {
id: drmkit_planes::LayerId(i as u64 + 1),
layer,
})
.collect();
assert!(crate::scene::canvas_reservation(true, &refs, ®istry, 0, None).is_empty());
}
#[test]
fn a_layer_with_no_zpos_makes_the_anchor_unnecessary() {
let registry = drmkit_planes::PlaneRegistry::from_capabilities(vec![
reservation_plane(31, drmkit_planes::PlaneType::Primary, Some((2, 2))),
reservation_plane(32, drmkit_planes::PlaneType::Overlay, Some((3, 9))),
]);
let layers = [reservation_layer(None)];
let refs: Vec<drmkit_planes::LayerRef<'_>> = layers
.iter()
.enumerate()
.map(|(i, layer)| drmkit_planes::LayerRef {
id: drmkit_planes::LayerId(i as u64 + 1),
layer,
})
.collect();
assert!(crate::scene::canvas_reservation(true, &refs, ®istry, 0, None).is_empty());
}
#[test]
fn an_unpinned_primary_is_not_anchored() {
let registry = drmkit_planes::PlaneRegistry::from_capabilities(vec![
reservation_plane(31, drmkit_planes::PlaneType::Primary, None),
reservation_plane(32, drmkit_planes::PlaneType::Overlay, None),
]);
let layers = [reservation_layer(Some(3))];
let refs: Vec<drmkit_planes::LayerRef<'_>> = layers
.iter()
.enumerate()
.map(|(i, layer)| drmkit_planes::LayerRef {
id: drmkit_planes::LayerId(i as u64 + 1),
layer,
})
.collect();
assert!(crate::scene::canvas_reservation(true, &refs, ®istry, 0, None).is_empty());
}
#[test]
fn overflow_reserves_an_overlay_for_the_canvas() {
let registry = drmkit_planes::PlaneRegistry::from_capabilities(vec![
reservation_plane(31, drmkit_planes::PlaneType::Primary, Some((0, 0))),
reservation_plane(32, drmkit_planes::PlaneType::Overlay, Some((1, 4))),
]);
let layers: Vec<drmkit_planes::Layer> = (0..3).map(|_| reservation_layer(None)).collect();
let refs: Vec<drmkit_planes::LayerRef<'_>> = layers
.iter()
.enumerate()
.map(|(i, layer)| drmkit_planes::LayerRef {
id: drmkit_planes::LayerId(i as u64 + 1),
layer,
})
.collect();
assert_eq!(
crate::scene::canvas_reservation(true, &refs, ®istry, 0, None),
vec![32]
);
}
fn refs_of(layers: &[drmkit_planes::Layer]) -> Vec<drmkit_planes::LayerRef<'_>> {
layers
.iter()
.enumerate()
.map(|(i, layer)| drmkit_planes::LayerRef {
id: drmkit_planes::LayerId(i as u64 + 1),
layer,
})
.collect()
}
fn two_primaries() -> drmkit_planes::PlaneRegistry {
drmkit_planes::PlaneRegistry::from_capabilities(vec![
reservation_plane(31, drmkit_planes::PlaneType::Primary, Some((0, 0))),
reservation_plane(32, drmkit_planes::PlaneType::Primary, Some((0, 0))),
reservation_plane(33, drmkit_planes::PlaneType::Overlay, Some((1, 8))),
reservation_plane(34, drmkit_planes::PlaneType::Overlay, Some((1, 8))),
])
}
#[test]
fn with_two_primaries_overflow_reserves_the_first_overlay() {
let layers: Vec<drmkit_planes::Layer> = (0..5).map(|_| reservation_layer(None)).collect();
assert_eq!(
crate::scene::canvas_reservation(true, &refs_of(&layers), &two_primaries(), 0, None),
vec![33]
);
}
#[test]
fn overflow_reserves_last_frames_canvas_plane() {
let layers: Vec<drmkit_planes::Layer> = (0..5).map(|_| reservation_layer(None)).collect();
assert_eq!(
crate::scene::canvas_reservation(true, &refs_of(&layers), &two_primaries(), 0, Some(34)),
vec![34]
);
}
#[test]
fn overflow_never_reserves_a_virtual_plane() {
let registry = drmkit_planes::PlaneRegistry::from_capabilities(vec![
reservation_plane(31, drmkit_planes::PlaneType::Primary, Some((0, 0))),
drmkit_planes::PlaneCapabilities {
multirect_parent: Some(34),
..reservation_plane(33, drmkit_planes::PlaneType::Overlay, Some((1, 8)))
},
reservation_plane(34, drmkit_planes::PlaneType::Overlay, Some((1, 8))),
]);
let layers: Vec<drmkit_planes::Layer> = (0..5).map(|_| reservation_layer(None)).collect();
let reserve = |previous| {
crate::scene::canvas_reservation(true, &refs_of(&layers), ®istry, 0, previous)
};
assert_eq!(reserve(None), vec![34]);
assert_eq!(reserve(Some(33)), vec![34]);
}
#[test]
fn the_canvas_prefers_its_last_plane_then_overlays_then_primaries() {
let registry = two_primaries();
let order = |hosts: &[u32], previous| {
crate::scene::canvas_plane_order(hosts, ®istry, 0, previous).collect::<Vec<_>>()
};
assert_eq!(order(&[31, 32, 33, 34], None), [33, 34, 31, 32]);
assert_eq!(order(&[31, 32, 33, 34], Some(32)), [32, 33, 34, 31]);
assert_eq!(order(&[31, 33], Some(34)), [33, 31]);
}
#[test]
fn overflow_reserves_nothing_where_planes_stack_by_id() {
let registry = drmkit_planes::PlaneRegistry::from_capabilities(vec![
reservation_plane(31, drmkit_planes::PlaneType::Primary, None),
reservation_plane(32, drmkit_planes::PlaneType::Overlay, None),
]);
let layers: Vec<drmkit_planes::Layer> = (0..3).map(|_| reservation_layer(None)).collect();
let refs: Vec<drmkit_planes::LayerRef<'_>> = layers
.iter()
.enumerate()
.map(|(i, layer)| drmkit_planes::LayerRef {
id: drmkit_planes::LayerId(i as u64 + 1),
layer,
})
.collect();
assert!(crate::scene::canvas_reservation(true, &refs, ®istry, 0, None).is_empty());
}
mod acquire_fence {
use std::os::fd::{AsFd as _, OwnedFd};
use drmkit_sync::SyncFence;
use crate::AcquiredBuffer;
fn signaled() -> OwnedFd {
rustix::event::eventfd(1, rustix::event::EventfdFlags::CLOEXEC).expect("eventfd")
}
fn open_fd_count() -> usize {
std::fs::read_dir("/proc/self/fd")
.expect("/proc/self/fd")
.count()
}
#[test]
fn a_buffer_with_no_fence_says_so() {
let acquired = AcquiredBuffer::default();
assert!(acquired.acquire_fence.is_none());
}
#[test]
fn churning_fenced_buffers_does_not_leak_descriptors() {
let source = signaled();
let churn = |times: usize| {
for _ in 0..times {
let fence = SyncFence::import(source.as_fd()).expect("import");
let acquired = AcquiredBuffer {
fb_id: 1,
acquire_fence: Some(fence),
..AcquiredBuffer::default()
};
let moved = acquired;
assert!(moved.acquire_fence.is_some());
drop(moved);
}
};
churn(16);
let after_few = open_fd_count();
churn(240);
let growth = open_fd_count().saturating_sub(after_few);
assert!(
growth < 16,
"240 further acquire/drop cycles grew the fd table by {growth}; \
a per-acquire leak would grow it by 240"
);
}
#[test]
fn a_fence_carried_through_a_move_is_still_waitable() {
let source = signaled();
let acquired = AcquiredBuffer {
fb_id: 1,
acquire_fence: Some(SyncFence::import(source.as_fd()).expect("import")),
..AcquiredBuffer::default()
};
let moved = acquired;
let fence = moved.acquire_fence.expect("the move kept it");
fence
.wait(std::time::Duration::from_millis(200))
.expect("an already-signaled fence waits successfully");
}
}
mod scene_surfaces {
use std::collections::HashMap;
use drmkit_planes::{ContentType, Rect};
use crate::{DisplayParams, LayerHandle, LayerScene, Placement};
#[test]
fn a_default_handle_is_the_invalid_sentinel() {
assert!(!LayerHandle::default().is_valid());
}
#[test]
fn a_handle_distinguishes_slot_from_generation() {
let mut scene = LayerScene::new(1);
let first = scene.add_layer(Box::new(super::MinimalSource));
scene.remove_layer(first);
let second = scene.add_layer(Box::new(super::MinimalSource));
assert_ne!(first, second, "the recycled slot is a different handle");
assert_ne!(
first.layer_id(),
second.layer_id(),
"and a different identity"
);
let mut by_handle: HashMap<LayerHandle, &str> = HashMap::new();
by_handle.insert(first, "gone");
by_handle.insert(second, "live");
assert_eq!(by_handle.len(), 2);
assert_eq!(by_handle.get(&second), Some(&"live"));
}
#[test]
fn a_stale_handle_resolves_to_nothing() {
let mut scene = LayerScene::new(1);
let first = scene.add_layer(Box::new(super::MinimalSource));
scene.remove_layer(first);
let second = scene.add_layer(Box::new(super::MinimalSource));
assert!(scene.layer(first).is_none(), "the removed layer is gone");
assert!(scene.layer(second).is_some());
}
#[test]
fn geometry_does_not_flag_a_layer_for_reallocation() {
let mut scene = LayerScene::new(1);
let handle = scene.add_layer(Box::new(super::MinimalSource));
let layer = scene.layer_mut(handle).expect("live");
assert!(!layer.hints_dirty());
layer.set_display(DisplayParams {
dst_rect: Rect {
x: 10,
y: 20,
w: 64,
h: 64,
},
..DisplayParams::default()
});
assert!(
!layer.hints_dirty(),
"geometry must not drop the warm start"
);
}
#[test]
fn a_hint_that_changes_scoring_does_flag_it() {
let mut scene = LayerScene::new(1);
let handle = scene.add_layer(Box::new(super::MinimalSource));
for set in [
&(|l: &mut crate::SceneLayer| l.set_content_type(ContentType::Video))
as &dyn Fn(&mut crate::SceneLayer),
&|l: &mut crate::SceneLayer| l.set_update_hint(24),
&|l: &mut crate::SceneLayer| l.set_app_priority(3),
] {
let fresh = scene.add_layer(Box::new(super::MinimalSource));
let layer = scene.layer_mut(fresh).expect("live");
assert!(!layer.hints_dirty(), "starts clean");
set(layer);
assert!(layer.hints_dirty());
}
let _ = handle;
}
#[test]
fn the_hint_accessors_read_back_what_was_set() {
let mut scene = LayerScene::new(1);
let handle = scene.add_layer(Box::new(super::MinimalSource));
let layer = scene.layer_mut(handle).expect("live");
assert_eq!(layer.app_priority(), 0, "defaults to none");
assert_eq!(layer.update_hint_hz(), 0);
assert_eq!(layer.content_type(), ContentType::default());
layer.set_app_priority(7);
layer.set_update_hint(48);
layer.set_content_type(ContentType::Video);
assert_eq!(layer.app_priority(), 7);
assert_eq!(layer.update_hint_hz(), 48);
assert_eq!(layer.content_type(), ContentType::Video);
}
#[test]
fn a_placement_defaults_to_unassigned_with_no_plane() {
let placement = crate::LayerPlacement {
layer: drmkit_planes::LayerId(1),
..crate::LayerPlacement::default()
};
assert_eq!(placement.placement, Placement::Unassigned);
assert_eq!(placement.plane_id, None);
assert_eq!(placement.plane_rotation_bits, 0);
}
#[test]
fn a_fresh_report_counts_nothing() {
let report = crate::CommitReport::default();
assert_eq!(report.layers_total, 0);
assert_eq!(report.layers_assigned, 0);
assert_eq!(report.layers_composited, 0);
assert_eq!(report.test_commits_issued, 0);
assert!(report.placements.is_empty());
assert!(
report.accounting_balances(),
"zero of everything still has to add up"
);
}
#[test]
fn the_scene_remembers_which_crtc_it_drives() {
assert_eq!(LayerScene::new(42).crtc_id(), 42);
}
}
#[test]
fn damage_becomes_corner_pairs_not_widths() {
let rects = crate::commit::damage_rects(&[DamageRect {
x: 10,
y: 20,
w: 30,
h: 40,
}]);
assert_eq!(
rects,
vec![[10, 20, 40, 60]],
"x2 is x + w and y2 is y + h; [10, 20, 30, 40] would be the width \
written where the far corner belongs"
);
}
#[test]
fn every_damage_rectangle_is_converted_in_order() {
let rects = crate::commit::damage_rects(&[
DamageRect {
x: 0,
y: 0,
w: 8,
h: 8,
},
DamageRect {
x: 100,
y: 200,
w: 1,
h: 1,
},
]);
assert_eq!(rects, vec![[0, 0, 8, 8], [100, 200, 101, 201]]);
}
#[test]
fn a_negative_origin_is_carried_through_rather_than_clamped() {
let rects = crate::commit::damage_rects(&[DamageRect {
x: -4,
y: -8,
w: 16,
h: 16,
}]);
assert_eq!(rects, vec![[-4, -8, 12, 8]]);
}
#[test]
fn a_far_corner_past_i32_saturates_rather_than_wrapping() {
let rects = crate::commit::damage_rects(&[DamageRect {
x: i32::MAX - 1,
y: 0,
w: 64,
h: 8,
}]);
assert_eq!(rects[0][2], i32::MAX, "clamped, not wrapped to a negative");
assert!(
rects[0][2] >= rects[0][0],
"the far corner must never end up behind the near one"
);
}
#[test]
fn no_damage_converts_to_no_rectangles() {
assert!(crate::commit::damage_rects(&[]).is_empty());
}
fn rect(x: i32, y: i32, w: u32, h: u32) -> DamageRect {
DamageRect { x, y, w, h }
}
#[test]
fn damage_inside_the_buffer_is_unchanged() {
let damage = [rect(0, 0, 8, 8), rect(100, 50, 20, 10)];
assert_eq!(crate::commit::clip_damage(&damage, 640, 480), damage);
}
#[test]
fn damage_past_the_buffer_is_clipped_to_it() {
let clipped =
crate::commit::clip_damage(&[rect(-4, -8, 16, 16), rect(630, 470, 20, 20)], 640, 480);
assert_eq!(clipped, [rect(0, 0, 12, 8), rect(630, 470, 10, 10)]);
}
#[test]
fn damage_with_nothing_inside_the_buffer_is_dropped() {
let clipped = crate::commit::clip_damage(
&[
rect(700, 0, 8, 8),
rect(-20, 0, 10, 8),
rect(10, 10, 0, 8),
rect(4, 4, 2, 2),
],
640,
480,
);
assert_eq!(clipped, [rect(4, 4, 2, 2)]);
}
#[test]
fn damage_entirely_off_the_buffer_becomes_whole_frame() {
assert!(crate::commit::clip_damage(&[rect(640, 0, 8, 8)], 640, 480).is_empty());
}
#[test]
fn more_than_sixteen_damage_rectangles_is_whole_frame() {
let sixteen: Vec<DamageRect> = (0..16).map(|i| rect(i * 10, 0, 4, 4)).collect();
assert_eq!(crate::commit::clip_damage(&sixteen, 640, 480).len(), 16);
let seventeen: Vec<DamageRect> = (0..17).map(|i| rect(i * 10, 0, 4, 4)).collect();
assert!(
crate::commit::clip_damage(&seventeen, 640, 480).is_empty(),
"over upstream's bound the frame repaints whole"
);
}
fn eight_bit_alpha_plane() -> PlanePropertyMap {
let mut map = PlanePropertyMap::new();
map.planes.insert(
7,
[(PropTag::FbId, 101), (PropTag::Alpha, 102)]
.into_iter()
.collect(),
);
map.alpha_max.insert(7, 255);
map
}
fn alpha_layer(alpha: u64) -> drmkit_planes::Layer {
let mut layer = drmkit_planes::Layer::new();
layer
.set_property(PropTag::FbId, 5)
.set_property(PropTag::Alpha, alpha);
layer
}
#[test]
fn alpha_is_written_on_the_planes_scale() {
let map = eight_bit_alpha_plane();
for (asked, sent) in [(0xFFFF, 255), (0x8000, 128), (0, 0)] {
let mut request = drmkit_core::AtomicRequest::with_capacity(4);
emit_layer(&mut request, &map, 7, &alpha_layer(asked), None).expect("emit");
let alpha = request
.writes()
.iter()
.find(|write| write.property_id == 102)
.map(|write| write.value);
assert_eq!(alpha, Some(sent), "alpha {asked:#x} on an 8-bit plane");
}
}
#[test]
fn a_steady_frame_does_not_rewrite_rescaled_alpha() {
let map = eight_bit_alpha_plane();
let layer = alpha_layer(0xFFFF);
let baseline = layer.snapshot();
let mut request = drmkit_core::AtomicRequest::with_capacity(4);
let written = emit_layer(&mut request, &map, 7, &layer, Some(&baseline)).expect("emit");
assert_eq!(
written.properties, 1,
"only FB_ID, which is written every frame by contract"
);
}
#[test]
fn alpha_on_a_plane_without_a_range_is_left_alone() {
let mut map = eight_bit_alpha_plane();
map.alpha_max.clear();
assert_eq!(map.rescale_alpha(7, 0x8000), 0x8000);
}
#[test]
fn the_lowest_layer_above_a_fixed_slot_makes_the_anchor_unnecessary() {
let registry = drmkit_planes::PlaneRegistry::from_capabilities(vec![
reservation_plane(31, drmkit_planes::PlaneType::Primary, Some((2, 2))),
reservation_plane(32, drmkit_planes::PlaneType::Overlay, Some((3, 9))),
reservation_plane(33, drmkit_planes::PlaneType::Overlay, Some((3, 9))),
]);
let layers = [reservation_layer(Some(3)), reservation_layer(Some(4))];
let refs: Vec<drmkit_planes::LayerRef<'_>> = layers
.iter()
.enumerate()
.map(|(i, layer)| drmkit_planes::LayerRef {
id: drmkit_planes::LayerId(i as u64 + 1),
layer,
})
.collect();
assert!(crate::scene::canvas_reservation(true, &refs, ®istry, 0, None).is_empty());
}
#[test]
fn the_bottom_slot_layer_is_the_unique_lowest_above_the_pin() {
use crate::scene::bottom_slot_layer;
assert_eq!(bottom_slot_layer(&[Some(1)], 0), Some(0));
assert_eq!(bottom_slot_layer(&[Some(5), Some(3), Some(4)], 2), Some(1));
assert_eq!(bottom_slot_layer(&[Some(3), Some(3)], 2), None);
assert_eq!(bottom_slot_layer(&[Some(1), Some(2)], 2), None);
assert_eq!(bottom_slot_layer(&[Some(3), None], 2), None);
assert_eq!(bottom_slot_layer(&[], 2), None);
}
mod ycbcr_fallback {
use crate::commit::{encoding_value, range_value};
use drmkit_planes::{ColorEncoding, ColorEncodingValues, ColorRange, ColorRangeValues};
const ALL: ColorEncodingValues = ColorEncodingValues {
bt601: Some(10),
bt709: Some(11),
bt2020: Some(12),
};
#[test]
fn an_encoding_the_plane_lists_is_written_as_asked() {
assert_eq!(encoding_value(&ALL, ColorEncoding::Bt601), Some(10));
assert_eq!(encoding_value(&ALL, ColorEncoding::Bt709), Some(11));
assert_eq!(encoding_value(&ALL, ColorEncoding::Bt2020), Some(12));
}
#[test]
fn a_missing_encoding_falls_back_to_bt709_then_bt601() {
let no_2020 = ColorEncodingValues {
bt2020: None,
..ALL
};
assert_eq!(encoding_value(&no_2020, ColorEncoding::Bt2020), Some(11));
let only_601 = ColorEncodingValues {
bt601: Some(10),
..ColorEncodingValues::default()
};
assert_eq!(encoding_value(&only_601, ColorEncoding::Bt709), Some(10));
assert_eq!(
encoding_value(&ColorEncodingValues::default(), ColorEncoding::Bt709),
None,
"a plane listing nothing known gets nothing written"
);
}
#[test]
fn a_missing_range_falls_back_to_limited_then_full() {
let both = ColorRangeValues {
limited: Some(20),
full: Some(21),
};
assert_eq!(range_value(&both, ColorRange::Full), Some(21));
assert_eq!(range_value(&both, ColorRange::Limited), Some(20));
let only_limited = ColorRangeValues { full: None, ..both };
assert_eq!(range_value(&only_limited, ColorRange::Full), Some(20));
let only_full = ColorRangeValues {
limited: None,
..both
};
assert_eq!(range_value(&only_full, ColorRange::Limited), Some(21));
}
}