mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-27 19:47:35 -06:00
refactor(lightcraft-ui-egui): move region.rs tests to crates/ui-egui/src/region/tests.rs
This commit is contained in:
@@ -445,641 +445,4 @@ pub fn window_for(full_w: usize, full_h: usize, visible: (f32, f32, f32, f32), m
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::state::AppSettings;
|
||||
|
||||
const BIG: usize = 16384;
|
||||
|
||||
fn sizes(drawn: f32, canvas: f32, native: usize) -> ViewSizes {
|
||||
ViewSizes { drawn_long: drawn, canvas_long: canvas, native_long: native, texture_side: BIG, draft: None, windows: true }
|
||||
}
|
||||
|
||||
fn auto() -> AppSettings {
|
||||
AppSettings::default()
|
||||
}
|
||||
|
||||
// Given a fit view on a Retina Mac (about 2800 px), the preview is enough: no original decoded
|
||||
#[test]
|
||||
fn a_retina_fit_view_needs_no_window() {
|
||||
let p = plan(&auto(), sizes(2800.0, 2800.0, 6000));
|
||||
assert_eq!(p.main_edge, 2560, "the preview source level");
|
||||
assert_eq!(p.window_edge, None);
|
||||
}
|
||||
|
||||
// Given a 4K display, the fit view is 1.5x the preview: the window makes it sharp
|
||||
#[test]
|
||||
fn a_large_fit_view_gets_a_window() {
|
||||
let p = plan(&auto(), sizes(3840.0, 3840.0, 6000));
|
||||
assert_eq!((p.main_edge, p.window_edge), (2560, Some(3840)));
|
||||
}
|
||||
|
||||
// Given 1:1 on a 24 MP photo in a 2800 px canvas, the whole frame stays canvas-sized and the
|
||||
// window is cut from the photo's own pixels
|
||||
#[test]
|
||||
fn one_to_one_renders_the_canvas_and_a_native_window() {
|
||||
let p = plan(&auto(), sizes(6000.0, 2800.0, 6000));
|
||||
assert_eq!((p.main_edge, p.window_edge), (2560, Some(6000)));
|
||||
}
|
||||
|
||||
// Given 800 %, the window is still at 100 % (the GPU magnifies it): the cost does not grow
|
||||
#[test]
|
||||
fn beyond_one_to_one_the_window_stays_at_native_resolution() {
|
||||
let p = plan(&auto(), sizes(48000.0, 2800.0, 6000));
|
||||
assert_eq!((p.main_edge, p.window_edge), (2560, Some(6000)));
|
||||
let q = plan(&auto(), sizes(480.0 * 100.0, 2800.0, 6000));
|
||||
assert_eq!(q.window_edge, Some(6000));
|
||||
}
|
||||
|
||||
// Given a small photo zoomed in, the whole-frame render already holds all its pixels
|
||||
#[test]
|
||||
fn a_small_photo_needs_no_window() {
|
||||
let p = plan(&auto(), sizes(8000.0, 1400.0, 1000));
|
||||
assert_eq!((p.main_edge, p.window_edge), (1000, None));
|
||||
}
|
||||
|
||||
// Given a slider drag at 400 % (drafts at 0.6 scale), the window plan does not change: it is
|
||||
// the whole-frame draft that gets cheaper
|
||||
#[test]
|
||||
fn a_drag_shrinks_the_whole_frame_draft_not_the_window() {
|
||||
let still = plan(&auto(), sizes(24000.0, 2800.0, 6000));
|
||||
let drag = plan(&auto(), ViewSizes { draft: Some(0.6), ..sizes(24000.0, 2800.0, 6000) });
|
||||
assert!(drag.main_edge < still.main_edge, "{drag:?} vs {still:?}");
|
||||
assert_eq!(drag.window_edge, still.window_edge);
|
||||
}
|
||||
|
||||
// Given no window (a whole-frame diagnostic), the view at rest renders the
|
||||
// whole frame at the size it is drawn, but a slider drag drafts at the canvas size like the
|
||||
// windowed path
|
||||
#[test]
|
||||
fn without_a_window_a_drag_drafts_at_the_canvas_size() {
|
||||
let no_window = |draft: Option<f32>| ViewSizes { windows: false, draft, ..sizes(24000.0, 2800.0, 6000) };
|
||||
let still = plan(&auto(), no_window(None));
|
||||
let drag = plan(&auto(), no_window(Some(0.6)));
|
||||
assert_eq!(still.window_edge, None);
|
||||
assert!(still.main_edge > 2800, "at rest the zoomed frame: {still:?}");
|
||||
assert!(drag.main_edge <= 2800, "a drag drafts at most the canvas: {drag:?}");
|
||||
}
|
||||
|
||||
// Given a slider drag, the GPU renders it at full size and the CPU a smaller draft (#330)
|
||||
#[test]
|
||||
fn a_drag_shrinks_the_draft_only_on_the_cpu() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let mut gpu = DragScale::default();
|
||||
assert_eq!(gpu.scale(None, true, None, 0.0), None, "no drag, no draft");
|
||||
assert_eq!(gpu.scale(Some((PhotoId(1), "Exposure")), true, None, 0.0), Some(1.0));
|
||||
let mut cpu = DragScale::default();
|
||||
assert_eq!(cpu.scale(Some((PhotoId(1), "Exposure")), false, None, 0.0), Some(CPU_DRAFT_SCALE));
|
||||
}
|
||||
|
||||
// Given a drag that started before the GPU was ready, it keeps its size until it ends: the
|
||||
// resolution never jumps in the middle of a drag
|
||||
#[test]
|
||||
fn a_drag_keeps_its_draft_size_until_it_ends() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let mut d = DragScale::default();
|
||||
let drag = Some((PhotoId(1), "Exposure"));
|
||||
assert_eq!(d.scale(drag, false, None, 0.0), Some(CPU_DRAFT_SCALE));
|
||||
assert_eq!(d.scale(drag, true, None, 0.0), Some(CPU_DRAFT_SCALE), "the GPU came up mid-drag");
|
||||
assert_eq!(d.scale(None, true, None, 0.0), None);
|
||||
assert_eq!(d.scale(drag, true, None, 0.0), Some(1.0), "the next drag gets full size");
|
||||
// and the other way: the GPU lost mid-drag keeps the drag at full size until it ends
|
||||
assert_eq!(d.scale(drag, false, None, 0.0), Some(1.0));
|
||||
}
|
||||
|
||||
/// One whole drag of `drag` at full size whose drafts all take `ms` to show, ending at `now`.
|
||||
fn timed_drag(d: &mut DragScale, drag: Option<(lightcraft_catalog::PhotoId, &str)>, ms: f64, now: f64) -> Option<f32> {
|
||||
let scale = d.scale(drag, true, None, now);
|
||||
for key in 1..=6 {
|
||||
d.observe(key, ms, None);
|
||||
}
|
||||
d.scale(None, true, None, now);
|
||||
scale
|
||||
}
|
||||
|
||||
// Given full-size drafts over the slider budget twice in a row (a LUT profile or refined mask
|
||||
// the CPU renders, a photo past a GPU limit, a slow GPU), the next drag of that control on
|
||||
// that photo drafts small; once could be a busy moment. Other controls and photos keep full size
|
||||
#[test]
|
||||
fn a_drag_over_budget_twice_drafts_small_the_next_time() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let mut d = DragScale::default();
|
||||
let exposure = Some((PhotoId(1), "Exposure"));
|
||||
assert_eq!(d.scale(exposure, true, Some(7), 0.0), Some(1.0));
|
||||
d.observe(7, 400.0, None); // the render from before the drag: not a draft of it
|
||||
for (key, ms) in [(8, 300.0), (9, 30.0), (10, 25.0), (11, 9.0)] {
|
||||
d.observe(key, ms, None);
|
||||
d.observe(key, ms, None); // a render is timed once, however many frames show it
|
||||
}
|
||||
assert_eq!(d.scale(exposure, true, None, 0.0), Some(1.0), "over budget, but not far enough to give up mid-drag");
|
||||
assert_eq!(d.scale(None, true, None, 0.0), None);
|
||||
assert_eq!(timed_drag(&mut d, exposure, 25.0, 1.0), Some(1.0), "one slow drag: maybe the machine was busy");
|
||||
assert_eq!(d.scale(exposure, true, None, 2.0), Some(CPU_DRAFT_SCALE));
|
||||
d.scale(None, true, None, 2.0);
|
||||
assert_eq!(d.scale(Some((PhotoId(1), "Contrast")), true, None, 2.0), Some(1.0));
|
||||
assert_eq!(d.scale(Some((PhotoId(2), "Exposure")), true, None, 2.0), Some(1.0));
|
||||
}
|
||||
|
||||
// Given a pair found slow, it drafts small only for a while: then a drag tries full size
|
||||
// again; a slow retry doubles the wait (up to its cap), a fast one forgets the verdict
|
||||
#[test]
|
||||
fn a_slow_verdict_expires_and_is_retried() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let mut d = DragScale::default();
|
||||
let drag = Some((PhotoId(1), "Exposure"));
|
||||
timed_drag(&mut d, drag, 25.0, 0.0);
|
||||
timed_drag(&mut d, drag, 25.0, 0.0);
|
||||
assert_eq!(timed_drag(&mut d, drag, 25.0, RETRY_AFTER_S - 1.0), Some(CPU_DRAFT_SCALE), "slow until the wait is over");
|
||||
assert_eq!(timed_drag(&mut d, drag, 25.0, RETRY_AFTER_S), Some(1.0), "then a drag tries full size: still slow");
|
||||
let mut at = RETRY_AFTER_S;
|
||||
let mut wait = RETRY_AFTER_S * 2.0;
|
||||
for _ in 0..6 {
|
||||
assert_eq!(timed_drag(&mut d, drag, 25.0, at + wait - 1.0), Some(CPU_DRAFT_SCALE), "the wait doubled to {wait} s");
|
||||
assert_eq!(timed_drag(&mut d, drag, 25.0, at + wait), Some(1.0), "a retry after {wait} s, slow again");
|
||||
at += wait;
|
||||
wait = (wait * 2.0).min(RETRY_MAX_S);
|
||||
}
|
||||
assert_eq!(wait, RETRY_MAX_S, "the wait stops growing");
|
||||
assert_eq!(timed_drag(&mut d, drag, 8.0, at + wait), Some(1.0), "a retry, fast this time");
|
||||
assert_eq!(timed_drag(&mut d, drag, 25.0, at + wait + 1.0), Some(1.0), "forgotten: one slow drag is a suspicion again");
|
||||
assert_eq!(timed_drag(&mut d, drag, 25.0, at + wait + 2.0), Some(1.0), "suspect: still full size");
|
||||
assert_eq!(d.scale(drag, true, None, at + wait + 3.0), Some(CPU_DRAFT_SCALE), "slow from the start again: wait 30 s");
|
||||
}
|
||||
|
||||
// Given a drag only just over budget, it doesn't count against the pair (it would flip between
|
||||
// the two sizes); one clearly over does
|
||||
#[test]
|
||||
fn only_a_drag_clearly_over_budget_counts() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let mut d = DragScale::default();
|
||||
let drag = Some((PhotoId(1), "Exposure"));
|
||||
for t in 0..4 {
|
||||
timed_drag(&mut d, drag, 18.0, f64::from(t));
|
||||
}
|
||||
assert_eq!(d.scale(drag, true, None, 4.0), Some(1.0), "18 ms drafts: just over the 16 ms budget");
|
||||
d.scale(None, true, None, 4.0);
|
||||
timed_drag(&mut d, drag, VERDICT_MS + 5.0, 5.0);
|
||||
timed_drag(&mut d, drag, VERDICT_MS + 5.0, 6.0);
|
||||
assert_eq!(d.scale(drag, true, None, 7.0), Some(CPU_DRAFT_SCALE));
|
||||
}
|
||||
|
||||
// Given one slow drag while the machine was busy, then fast ones, the slider keeps full size
|
||||
#[test]
|
||||
fn a_busy_moment_doesnt_make_a_slider_draft_small() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let mut d = DragScale::default();
|
||||
let drag = Some((PhotoId(1), "Exposure"));
|
||||
timed_drag(&mut d, drag, 40.0, 0.0);
|
||||
for t in 1..=3 {
|
||||
assert_eq!(timed_drag(&mut d, drag, 8.0, f64::from(t)), Some(1.0));
|
||||
}
|
||||
timed_drag(&mut d, drag, 40.0, 4.0);
|
||||
assert_eq!(d.scale(drag, true, None, 5.0), Some(1.0), "the fast drags forgot the first slow one");
|
||||
}
|
||||
|
||||
// Given a hostile host clock (NaN, infinite, going backwards), a drag never panics; a clock it
|
||||
// can't read tries full size
|
||||
#[test]
|
||||
fn a_hostile_clock_never_panics() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let mut d = DragScale::default();
|
||||
let drag = Some((PhotoId(1), "Exposure"));
|
||||
timed_drag(&mut d, drag, 25.0, 100.0);
|
||||
timed_drag(&mut d, drag, 25.0, 100.0);
|
||||
assert_eq!(d.scale(drag, true, None, 50.0), Some(CPU_DRAFT_SCALE), "the clock went backwards: still slow");
|
||||
d.scale(None, true, None, 50.0);
|
||||
assert_eq!(d.scale(drag, true, None, f64::NAN), Some(1.0));
|
||||
d.scale(None, true, None, f64::NAN);
|
||||
for now in [f64::INFINITY, f64::NEG_INFINITY, f64::NAN, -1.0] {
|
||||
timed_drag(&mut d, drag, 25.0, now);
|
||||
}
|
||||
}
|
||||
|
||||
// Given full-size drafts within budget (the first one, rebuilding the stage caches, may be
|
||||
// slow), the next drag stays at full size
|
||||
#[test]
|
||||
fn a_drag_within_budget_stays_full_size() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let mut d = DragScale::default();
|
||||
let drag = Some((PhotoId(1), "Exposure"));
|
||||
d.scale(drag, true, None, 0.0);
|
||||
for (key, ms) in [(1, 120.0), (2, 4.0), (3, 6.0), (4, 30.0), (5, 5.0)] {
|
||||
d.observe(key, ms, None);
|
||||
}
|
||||
d.scale(None, true, None, 0.0);
|
||||
assert_eq!(d.scale(drag, true, None, 0.0), Some(1.0));
|
||||
// hostile times are ignored, never a panic
|
||||
for (key, ms) in [(6, f64::NAN), (7, f64::INFINITY), (8, -1.0)] {
|
||||
d.observe(key, ms, None);
|
||||
}
|
||||
d.scale(None, true, None, 0.0);
|
||||
assert_eq!(d.scale(drag, true, None, 0.0), Some(1.0));
|
||||
}
|
||||
|
||||
// Given renders within budget whose frames (the texture upload) take it over, the drag counts
|
||||
// as over budget: a render time alone said 231 of 234 drafts fit while none showed in 16 ms
|
||||
#[test]
|
||||
fn a_drafts_time_includes_the_frame_that_shows_it() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let mut d = DragScale::default();
|
||||
let drag = Some((PhotoId(1), "Exposure"));
|
||||
for _ in 0..2 {
|
||||
d.scale(drag, true, None, 0.0);
|
||||
for key in 1..=5 {
|
||||
d.observe(key, 8.0, Some(14.0)); // the previous frame, which uploaded the last draft
|
||||
}
|
||||
d.observe(5, 8.0, Some(14.0));
|
||||
d.scale(None, true, None, 0.0);
|
||||
}
|
||||
assert_eq!(d.scale(drag, true, None, 0.0), Some(CPU_DRAFT_SCALE), "8 ms renders + 14 ms frames: over budget");
|
||||
// the same renders without the host's frame times (headless) stay within budget
|
||||
let mut headless = DragScale::default();
|
||||
headless.scale(drag, true, None, 0.0);
|
||||
for _ in 0..2 {
|
||||
timed_drag(&mut headless, drag, 8.0, 0.0);
|
||||
}
|
||||
assert_eq!(headless.scale(drag, true, None, 0.0), Some(1.0));
|
||||
// a hostile frame time counts as nothing, never a panic
|
||||
let mut hostile = DragScale::default();
|
||||
hostile.scale(drag, true, None, 0.0);
|
||||
for (key, frame) in [(1, f64::NAN), (2, f64::INFINITY), (3, -1.0), (4, f64::NAN)] {
|
||||
hostile.observe(key, 8.0, Some(frame));
|
||||
}
|
||||
hostile.scale(None, true, None, 0.0);
|
||||
assert_eq!(hostile.scale(drag, true, None, 0.0), Some(1.0));
|
||||
}
|
||||
|
||||
// Given more slow drags than are remembered, only the one remembered longest ago is forgotten:
|
||||
// clearing them all would bring back a slow full-size drag for every pair
|
||||
#[test]
|
||||
fn a_full_slow_set_forgets_only_the_oldest_drag() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let mut d = DragScale::default();
|
||||
for photo in 0..=VERDICTS_KEPT as u64 {
|
||||
d.remember_slow(PhotoId(photo), "Exposure");
|
||||
}
|
||||
assert_eq!(d.scale(Some((PhotoId(0), "Exposure")), true, None, 0.0), Some(1.0), "the oldest is forgotten");
|
||||
d.scale(None, true, None, 0.0);
|
||||
for photo in [1, VERDICTS_KEPT as u64] {
|
||||
assert_eq!(d.scale(Some((PhotoId(photo), "Exposure")), true, None, 0.0), Some(CPU_DRAFT_SCALE), "photo {photo}");
|
||||
d.scale(None, true, None, 0.0);
|
||||
}
|
||||
d.remember_slow(PhotoId(1), "Exposure"); // already remembered: nothing is forgotten
|
||||
assert_eq!(d.scale(Some((PhotoId(2), "Exposure")), true, None, 0.0), Some(CPU_DRAFT_SCALE));
|
||||
}
|
||||
|
||||
// Given draft frames that took seconds (the window hidden or occluded mid-drag), they count as
|
||||
// unknown: fast renders don't mark the drag slow because the window wasn't drawing. Two such
|
||||
// drags, since one slow drag only makes the pair suspect
|
||||
#[test]
|
||||
fn a_hidden_windows_frame_times_dont_mark_a_drag_slow() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let mut d = DragScale::default();
|
||||
let drag = Some((PhotoId(1), "Exposure"));
|
||||
for t in 0..2u32 {
|
||||
assert_eq!(d.scale(drag, true, None, f64::from(t)), Some(1.0), "drag {}", t + 1);
|
||||
for key in 1..=6 {
|
||||
d.observe(u64::from(t) * 10 + key, 6.0, Some(4000.0));
|
||||
}
|
||||
d.scale(None, true, None, f64::from(t));
|
||||
}
|
||||
assert_eq!(d.scale(drag, true, None, 2.0), Some(1.0), "after two hidden-window drags: still full size");
|
||||
}
|
||||
|
||||
// Given full-size drafts far over budget (a refined mask the CPU renders at 190 ms a step),
|
||||
// the drag drops to the small draft after a few of them instead of at its end; after a second
|
||||
// such drag, the next starts small
|
||||
#[test]
|
||||
fn a_drag_far_over_budget_drafts_small_before_it_ends() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let mut d = DragScale::default();
|
||||
let drag = Some((PhotoId(1), "Exposure"));
|
||||
assert_eq!(d.scale(drag, true, None, 0.0), Some(1.0));
|
||||
let mut scales = Vec::new();
|
||||
for key in 1..=8 {
|
||||
d.observe(key, 190.0, None);
|
||||
scales.push(d.scale(drag, true, None, 0.0));
|
||||
}
|
||||
// drafts 1–4 are timed one frame late; the median of 2–4 decides on the fifth frame
|
||||
assert_eq!(scales[..4], [Some(1.0); 4], "{scales:?}");
|
||||
assert_eq!(scales[4..], [Some(CPU_DRAFT_SCALE); 4], "{scales:?}");
|
||||
d.scale(None, true, None, 0.0);
|
||||
assert_eq!(timed_drag(&mut d, drag, 190.0, 1.0), Some(1.0), "one such drag: maybe a busy moment");
|
||||
assert_eq!(d.scale(drag, true, None, 2.0), Some(CPU_DRAFT_SCALE));
|
||||
}
|
||||
|
||||
// Given a full-size drag zoomed in (a window covers the view), the hidden whole-frame draft is
|
||||
// the small one: at 1:1 on a Retina display it cost 7–9 ms a step for pixels nobody sees
|
||||
#[test]
|
||||
fn a_full_size_drag_under_a_window_drafts_the_whole_frame_small() {
|
||||
let zoomed = sizes(24000.0, 2800.0, 6000);
|
||||
let small = plan(&auto(), ViewSizes { draft: Some(CPU_DRAFT_SCALE), ..zoomed });
|
||||
let full = plan(&auto(), ViewSizes { draft: Some(1.0), ..zoomed });
|
||||
assert!(full.window_edge.is_some(), "{full:?}");
|
||||
assert_eq!(full, small);
|
||||
// at fit without a window, full size stays full size
|
||||
let fit = sizes(2800.0, 2800.0, 6000);
|
||||
assert_eq!(plan(&auto(), ViewSizes { draft: Some(1.0), ..fit }), plan(&auto(), fit));
|
||||
}
|
||||
|
||||
// Given a drag at fit at full size, the draft is as sharp as the render at rest
|
||||
#[test]
|
||||
fn a_full_size_drag_at_fit_keeps_the_resolution() {
|
||||
for drawn in [1400.0, 2800.0] {
|
||||
let still = plan(&auto(), sizes(drawn, drawn, 6000));
|
||||
let drag = plan(&auto(), ViewSizes { draft: Some(1.0), ..sizes(drawn, drawn, 6000) });
|
||||
assert_eq!(drag, still, "{drawn}");
|
||||
}
|
||||
}
|
||||
|
||||
// Given no window and a full-size drag, the draft is still at most the canvas: a drag at 1:1
|
||||
// must not render the photo's every pixel each frame
|
||||
#[test]
|
||||
fn a_full_size_drag_without_a_window_drafts_at_the_canvas_size() {
|
||||
let drag = plan(&auto(), ViewSizes { windows: false, draft: Some(1.0), ..sizes(24000.0, 2800.0, 6000) });
|
||||
assert!(drag.main_edge <= 2816, "{drag:?}");
|
||||
}
|
||||
|
||||
// Given the user capped the preview size, the whole-frame render obeys it, and a fit view
|
||||
// stays on it (no original decoded); a zoomed view still gets its sharp window
|
||||
#[test]
|
||||
fn a_preview_limit_caps_the_whole_frame_render_and_spares_the_fit_view() {
|
||||
let s = AppSettings { preview_limit: 1600, ..Default::default() };
|
||||
let fit = plan(&s, sizes(2800.0, 2800.0, 6000));
|
||||
assert_eq!((fit.main_edge, fit.window_edge), (1600, None));
|
||||
let zoomed = plan(&s, sizes(6000.0, 2800.0, 6000));
|
||||
assert_eq!((zoomed.main_edge, zoomed.window_edge), (1600, Some(6000)));
|
||||
}
|
||||
|
||||
// Given a GPU with 2048 px textures, the whole-frame render obeys it, the window frame need not
|
||||
// (a window is cut to the texture side separately)
|
||||
#[test]
|
||||
fn the_texture_side_caps_the_whole_frame_render_only() {
|
||||
let p = plan(&auto(), ViewSizes { texture_side: 2048, ..sizes(6000.0, 2800.0, 6000) });
|
||||
assert_eq!((p.main_edge, p.window_edge), (2048, Some(6000)));
|
||||
}
|
||||
|
||||
// Given a picture that is not the frame (an embedded JPEG cropped differently), no window
|
||||
#[test]
|
||||
fn a_window_needs_a_picture_with_the_frames_aspect() {
|
||||
assert!(same_aspect(1.5, 1.5) && same_aspect(1.51, 1.5));
|
||||
assert!(!same_aspect(1.5, 1.0) && !same_aspect(f32::NAN, 1.0) && !same_aspect(1.0, 0.0));
|
||||
}
|
||||
|
||||
// Given a drag at fit on a 4K or 5K display (where the fit view has a window), the window stays:
|
||||
// it is only the whole-frame draft that gets smaller
|
||||
#[test]
|
||||
fn a_drag_at_fit_on_a_large_display_keeps_the_window() {
|
||||
for drawn in [3300.0, 3840.0, 4096.0, 5120.0] {
|
||||
let still = plan(&auto(), sizes(drawn, drawn, 6000));
|
||||
let drag = plan(&auto(), ViewSizes { draft: Some(0.6), ..sizes(drawn, drawn, 6000) });
|
||||
assert!(still.window_edge.is_some(), "{drawn}: {still:?}");
|
||||
assert_eq!(drag.window_edge, still.window_edge, "{drawn}");
|
||||
}
|
||||
}
|
||||
|
||||
// Given a mode whose picture can't be a window (a whole-frame diagnostic, or a
|
||||
// spot read too far for one render), the whole-frame render is as big as the view
|
||||
#[test]
|
||||
fn without_windows_the_whole_frame_is_rendered_at_the_drawn_size() {
|
||||
let p = plan(&auto(), ViewSizes { windows: false, ..sizes(6000.0, 2800.0, 6000) });
|
||||
assert_eq!((p.main_edge, p.window_edge), (6000, None));
|
||||
let fit = plan(&auto(), ViewSizes { windows: false, ..sizes(2800.0, 2800.0, 6000) });
|
||||
assert_eq!((fit.main_edge, fit.window_edge), (2816, None));
|
||||
let deep = plan(&auto(), ViewSizes { windows: false, ..sizes(48000.0, 2800.0, 6000) });
|
||||
assert_eq!(deep.main_edge, 6000, "never above the photo's own pixels");
|
||||
let small = plan(&auto(), ViewSizes { windows: false, texture_side: 2048, ..sizes(6000.0, 2800.0, 6000) });
|
||||
assert_eq!(small.main_edge, 2048);
|
||||
}
|
||||
|
||||
// Given a user who chose a preview size above the preview source level (3840 or 5120 on a big
|
||||
// display), the whole-frame render is that big
|
||||
#[test]
|
||||
fn a_large_preview_limit_is_honoured() {
|
||||
for limit in [3840u32, 5120] {
|
||||
let s = AppSettings { preview_limit: limit, ..Default::default() };
|
||||
let p = plan(&s, sizes(5120.0, 5120.0, 6000));
|
||||
assert_eq!(p.main_edge, limit as usize, "{limit}");
|
||||
assert_eq!(p.window_edge, None, "a fit view at the limit needs no window");
|
||||
}
|
||||
let s = AppSettings { preview_limit: 2560, ..Default::default() };
|
||||
assert_eq!(plan(&s, sizes(5120.0, 5120.0, 6000)).main_edge, 2560);
|
||||
}
|
||||
|
||||
// Given a medium format photo wider than 8192 px, 1:1 is its own pixels
|
||||
#[test]
|
||||
fn a_window_frame_can_be_as_big_as_the_photo() {
|
||||
let p = plan(&auto(), sizes(40000.0, 2800.0, 11648));
|
||||
assert_eq!(p.window_edge, Some(11648));
|
||||
}
|
||||
|
||||
// Given a drag at fit on a Retina Mac, the window does not switch on (it would flicker with the drag)
|
||||
#[test]
|
||||
fn a_drag_at_fit_does_not_switch_the_window_on() {
|
||||
let p = plan(&auto(), ViewSizes { draft: Some(0.6), ..sizes(2800.0, 2800.0, 6000) });
|
||||
assert_eq!(p.window_edge, None);
|
||||
}
|
||||
|
||||
// Given a pinch from fit to 400 %, the sizes of the first frame hold until the gesture is quiet
|
||||
#[test]
|
||||
fn a_pinch_holds_the_render_sizes() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let (fit, zoomed) = (plan(&auto(), sizes(1400.0, 1400.0, 6000)), plan(&auto(), sizes(24000.0, 1400.0, 6000)));
|
||||
assert_ne!(fit, zoomed);
|
||||
let mut hold = SizeHold::default();
|
||||
let p = PhotoId(1);
|
||||
assert_eq!(hold.apply(p, 0.0, false, fit), fit);
|
||||
// the pinch runs for a second: every frame would plan something else
|
||||
for k in 1..60 {
|
||||
assert_eq!(hold.apply(p, k as f64 / 60.0, true, zoomed), fit, "frame {k}");
|
||||
}
|
||||
// it stopped at t = 59/60: still held for a moment (no flicker between two events)…
|
||||
assert!(hold.holding(1.1));
|
||||
assert_eq!(hold.apply(p, 1.1, false, zoomed), fit);
|
||||
// …then the view is planned for where it ended
|
||||
assert!(!hold.holding(59.0 / 60.0 + HOLD_SECS + 0.01));
|
||||
assert_eq!(hold.apply(p, 1.3, false, zoomed), zoomed);
|
||||
assert_eq!(hold.apply(p, 1.4, false, zoomed), zoomed);
|
||||
}
|
||||
|
||||
// Given a pinch, the display-fit size holds like the render sizes and is retaken after it
|
||||
#[test]
|
||||
fn a_pinch_holds_the_display_fit_size() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let (fit, zoomed) = (plan(&auto(), sizes(1400.0, 1400.0, 6000)), plan(&auto(), sizes(24000.0, 1400.0, 6000)));
|
||||
let mut hold = SizeHold::default();
|
||||
let p = PhotoId(1);
|
||||
hold.apply(p, 0.0, false, fit);
|
||||
assert_eq!(hold.display_fit(0.0, false, [1000, 800]), [1000, 800]);
|
||||
for k in 1..60 {
|
||||
let now = k as f64 / 60.0;
|
||||
hold.apply(p, now, true, zoomed);
|
||||
assert_eq!(hold.display_fit(now, true, [1000 + k, 800 + k]), [1000, 800], "frame {k}");
|
||||
}
|
||||
// stopped at t = 59/60: still held within the hold time…
|
||||
hold.apply(p, 1.1, false, zoomed);
|
||||
assert_eq!(hold.display_fit(1.1, false, [2560, 2048]), [1000, 800]);
|
||||
// …then the size it ended at is taken, and kept while nothing moves
|
||||
hold.apply(p, 1.3, false, zoomed);
|
||||
assert_eq!(hold.display_fit(1.3, false, [2560, 2048]), [2560, 2048]);
|
||||
assert_eq!(hold.display_fit(1.4, false, [2561, 2049]), [2561, 2049]);
|
||||
// another photo forgets the held size
|
||||
hold.apply(p, 1.5, true, zoomed);
|
||||
hold.display_fit(1.5, true, [2561, 2049]);
|
||||
hold.apply(PhotoId(2), 1.6, true, zoomed);
|
||||
assert_eq!(hold.display_fit(1.6, true, [640, 480]), [640, 480]);
|
||||
}
|
||||
|
||||
// Given another photo, the held sizes are forgotten
|
||||
#[test]
|
||||
fn another_photo_is_planned_afresh() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let (fit, zoomed) = (plan(&auto(), sizes(1400.0, 1400.0, 6000)), plan(&auto(), sizes(24000.0, 1400.0, 6000)));
|
||||
let mut hold = SizeHold::default();
|
||||
hold.apply(PhotoId(1), 0.0, false, fit);
|
||||
assert_eq!(hold.apply(PhotoId(2), 0.1, true, zoomed), zoomed);
|
||||
}
|
||||
|
||||
// Given side by side, a window is drawn in its pane only; given a wipe, on its side of the line
|
||||
#[test]
|
||||
fn a_window_is_clipped_to_its_pane_or_its_side_of_the_wipe() {
|
||||
use crate::state::BeforeAfter::*;
|
||||
let canvas = egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1000.0, 600.0));
|
||||
let (left, right) = (
|
||||
egui::Rect::from_min_max(egui::pos2(24.0, 24.0), egui::pos2(494.0, 576.0)),
|
||||
egui::Rect::from_min_max(egui::pos2(506.0, 24.0), egui::pos2(976.0, 576.0)),
|
||||
);
|
||||
assert_eq!(window_clip(SideBySide, true, left, canvas, left), left);
|
||||
assert_eq!(window_clip(SideBySide, false, right, canvas, right), right);
|
||||
let img = egui::Rect::from_min_max(egui::pos2(100.0, 50.0), egui::pos2(900.0, 550.0));
|
||||
let before = window_clip(Split, true, canvas, canvas, img);
|
||||
let after = window_clip(Split, false, canvas, canvas, img);
|
||||
assert_eq!((before.max.x, after.min.x), (500.0, 500.0), "the line is the image's middle");
|
||||
assert_eq!(window_clip(SplitTopBottom, true, canvas, canvas, img).max.y, 300.0);
|
||||
assert_eq!(window_clip(SplitTopBottom, false, canvas, canvas, img).min.y, 300.0);
|
||||
// zoomed in, the photo's middle (x = 1600) is off screen: the line is the view's middle
|
||||
let zoomed = egui::Rect::from_min_max(egui::pos2(-200.0, -900.0), egui::pos2(3400.0, 1500.0));
|
||||
assert_eq!(window_clip(Split, false, canvas, canvas, zoomed).min.x, 500.0);
|
||||
assert_eq!(window_clip(SplitTopBottom, true, canvas, canvas, zoomed).max.y, 300.0);
|
||||
assert_eq!(window_clip(Off, false, canvas, canvas, img), canvas);
|
||||
assert_eq!(window_clip(Original, true, canvas, canvas, img), canvas);
|
||||
}
|
||||
|
||||
// Given a window asked for while another is rendering and the original isn't decoded yet, it
|
||||
// waits for the decoded original instead of decoding another copy
|
||||
#[test]
|
||||
fn a_window_waits_for_the_original_another_is_decoding() {
|
||||
assert!(defer_window(true, false));
|
||||
assert!(!defer_window(true, true), "the original is in");
|
||||
assert!(!defer_window(false, false), "nothing is decoding it");
|
||||
}
|
||||
|
||||
// Hostile numbers give a plan, never a panic
|
||||
#[test]
|
||||
fn hostile_sizes_give_a_usable_plan() {
|
||||
for v in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, -3.0, 0.0] {
|
||||
let p = plan(&auto(), ViewSizes { drawn_long: v, canvas_long: v, draft: Some(v), ..sizes(1.0, 1.0, 6000) });
|
||||
assert!((8..=2560).contains(&p.main_edge), "{v}: {p:?}");
|
||||
}
|
||||
let p = plan(&auto(), sizes(1000.0, 1000.0, 0));
|
||||
assert!(p.main_edge >= 8);
|
||||
}
|
||||
|
||||
// Given a visible area in the middle of a big frame, the window covers it with a margin, on the grid
|
||||
#[test]
|
||||
fn the_window_covers_the_visible_area_with_a_margin_on_the_grid() {
|
||||
let w = window_for(48_000, 32_000, (20_000.0, 10_000.0, 21_400.0, 10_900.0), MAX_SPAN).unwrap();
|
||||
let m = margin_for(48_000);
|
||||
assert!(w.x + m <= 20_000 && w.x + w.w >= 21_400 + m, "{w:?}");
|
||||
assert!(w.y + m <= 10_000 && w.y + w.h >= 10_900 + m, "{w:?}");
|
||||
assert_eq!((w.x % SNAP, w.y % SNAP, w.w % SNAP, w.h % SNAP), (0, 0, 0, 0));
|
||||
}
|
||||
|
||||
// Given a small pan, the same window is kept (no re-render); a big pan changes it
|
||||
#[test]
|
||||
fn small_pans_keep_the_window() {
|
||||
let a = window_for(48_000, 32_000, (20_000.0, 10_000.0, 21_400.0, 10_900.0), MAX_SPAN).unwrap();
|
||||
let b = window_for(48_000, 32_000, (20_040.0, 10_030.0, 21_440.0, 10_930.0), MAX_SPAN).unwrap();
|
||||
assert_eq!(a, b);
|
||||
let c = window_for(48_000, 32_000, (26_000.0, 10_000.0, 27_400.0, 10_900.0), MAX_SPAN).unwrap();
|
||||
assert_ne!(a, c);
|
||||
}
|
||||
|
||||
// Given a frame smaller than the margin, or a view at its edge, the window is clamped into it
|
||||
#[test]
|
||||
fn the_window_stays_inside_the_frame() {
|
||||
let w = window_for(3000, 2000, (-500.0, -500.0, 900.0, 700.0), MAX_SPAN).unwrap();
|
||||
assert_eq!((w.x, w.y), (0, 0));
|
||||
let w = window_for(3000, 2000, (2500.0, 1500.0, 9000.0, 9000.0), MAX_SPAN).unwrap();
|
||||
assert!(w.x + w.w <= 3000 && w.y + w.h <= 2000, "{w:?}");
|
||||
assert_eq!((w.x + w.w, w.y + w.h), (3000, 2000));
|
||||
}
|
||||
|
||||
// Given a deeper zoom, the margin grows with the wide stages' kernels (3σ of the base, clarity
|
||||
// and dehaze planes are 0.045 of the frame), up to a cap that bounds the window
|
||||
#[test]
|
||||
fn the_margin_grows_with_the_zoom_up_to_a_cap() {
|
||||
assert_eq!(margin_for(1000), SNAP, "never less than one grid step");
|
||||
assert!(margin_for(8000) > margin_for(3000));
|
||||
assert_eq!(margin_for(48_000), 2304, "4.5 % of an 8:1 frame, under the cap");
|
||||
assert_eq!(margin_for(80_000), MAX_MARGIN);
|
||||
assert_eq!(margin_for(0), SNAP);
|
||||
assert!(margin_for(usize::MAX) <= MAX_MARGIN);
|
||||
for l in [100, 3000, 8000, 48_000, 400_000] {
|
||||
assert_eq!(margin_for(l) % SNAP, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Given a tile rendered for other settings, another photo or another zoom, it is not drawn
|
||||
#[test]
|
||||
fn a_tile_is_drawn_only_while_it_shows_what_the_loupe_shows() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let v = RegionView {
|
||||
photo: PhotoId(1),
|
||||
before: false,
|
||||
key: 7,
|
||||
full: (6000, 4000),
|
||||
window: PixelWindow { x: 0, y: 0, w: 256, h: 256 },
|
||||
settings: 11,
|
||||
};
|
||||
assert!(v.is_current(PhotoId(1), (6000, 4000), 11, false));
|
||||
assert!(!v.is_current(PhotoId(2), (6000, 4000), 11, false), "another photo");
|
||||
assert!(!v.is_current(PhotoId(1), (6000, 4001), 11, false), "another zoom");
|
||||
assert!(!v.is_current(PhotoId(1), (6000, 4000), 12, false), "another look (an edit, an undo)");
|
||||
assert!(v.is_current(PhotoId(1), (6000, 4000), 12, true), "a drag's drafts follow the value");
|
||||
assert!(!v.is_current(PhotoId(2), (6000, 4000), 12, true), "…of this photo only");
|
||||
}
|
||||
|
||||
// Given a GPU that allows 2048 px textures, no window is wider than that
|
||||
#[test]
|
||||
fn a_window_fits_the_texture_side() {
|
||||
for tex in [512, 2048, 4096] {
|
||||
let span = max_span(tex);
|
||||
assert!(span <= tex.max(SNAP) && span.is_multiple_of(SNAP), "{tex}: {span}");
|
||||
let w = window_for(48_000, 32_000, (20_000.0, 10_000.0, 21_400.0, 10_900.0), span).unwrap();
|
||||
assert!(w.w <= span && w.h <= span, "{tex}: {w:?}");
|
||||
}
|
||||
assert_eq!(max_span(100_000), MAX_SPAN);
|
||||
assert_eq!(max_span(0), SNAP);
|
||||
}
|
||||
|
||||
// Given nothing visible, or hostile numbers, there is no window (never a panic)
|
||||
#[test]
|
||||
fn nothing_visible_or_hostile_input_is_no_window() {
|
||||
assert_eq!(window_for(1000, 1000, (2000.0, 0.0, 3000.0, 100.0), MAX_SPAN), None);
|
||||
assert_eq!(window_for(1000, 1000, (10.0, 10.0, 10.0, 90.0), MAX_SPAN), None);
|
||||
assert_eq!(window_for(0, 1000, (0.0, 0.0, 10.0, 10.0), MAX_SPAN), None);
|
||||
for v in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
|
||||
assert_eq!(window_for(1000, 1000, (v, 0.0, 10.0, 10.0), MAX_SPAN), None);
|
||||
}
|
||||
let w = window_for(usize::MAX / 2, usize::MAX / 2, (0.0, 0.0, 1e30, 1e30), MAX_SPAN).unwrap();
|
||||
assert!(w.w <= MAX_SPAN && w.h <= MAX_SPAN, "{w:?}");
|
||||
}
|
||||
|
||||
// Given a view of an enormous area, the window is bounded
|
||||
#[test]
|
||||
fn a_window_is_bounded() {
|
||||
let w = window_for(100_000, 100_000, (0.0, 0.0, 90_000.0, 90_000.0), MAX_SPAN).unwrap();
|
||||
assert!(w.w <= MAX_SPAN && w.h <= MAX_SPAN);
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,636 @@
|
||||
use super::*;
|
||||
use crate::state::AppSettings;
|
||||
|
||||
const BIG: usize = 16384;
|
||||
|
||||
fn sizes(drawn: f32, canvas: f32, native: usize) -> ViewSizes {
|
||||
ViewSizes { drawn_long: drawn, canvas_long: canvas, native_long: native, texture_side: BIG, draft: None, windows: true }
|
||||
}
|
||||
|
||||
fn auto() -> AppSettings {
|
||||
AppSettings::default()
|
||||
}
|
||||
|
||||
// Given a fit view on a Retina Mac (about 2800 px), the preview is enough: no original decoded
|
||||
#[test]
|
||||
fn a_retina_fit_view_needs_no_window() {
|
||||
let p = plan(&auto(), sizes(2800.0, 2800.0, 6000));
|
||||
assert_eq!(p.main_edge, 2560, "the preview source level");
|
||||
assert_eq!(p.window_edge, None);
|
||||
}
|
||||
|
||||
// Given a 4K display, the fit view is 1.5x the preview: the window makes it sharp
|
||||
#[test]
|
||||
fn a_large_fit_view_gets_a_window() {
|
||||
let p = plan(&auto(), sizes(3840.0, 3840.0, 6000));
|
||||
assert_eq!((p.main_edge, p.window_edge), (2560, Some(3840)));
|
||||
}
|
||||
|
||||
// Given 1:1 on a 24 MP photo in a 2800 px canvas, the whole frame stays canvas-sized and the
|
||||
// window is cut from the photo's own pixels
|
||||
#[test]
|
||||
fn one_to_one_renders_the_canvas_and_a_native_window() {
|
||||
let p = plan(&auto(), sizes(6000.0, 2800.0, 6000));
|
||||
assert_eq!((p.main_edge, p.window_edge), (2560, Some(6000)));
|
||||
}
|
||||
|
||||
// Given 800 %, the window is still at 100 % (the GPU magnifies it): the cost does not grow
|
||||
#[test]
|
||||
fn beyond_one_to_one_the_window_stays_at_native_resolution() {
|
||||
let p = plan(&auto(), sizes(48000.0, 2800.0, 6000));
|
||||
assert_eq!((p.main_edge, p.window_edge), (2560, Some(6000)));
|
||||
let q = plan(&auto(), sizes(480.0 * 100.0, 2800.0, 6000));
|
||||
assert_eq!(q.window_edge, Some(6000));
|
||||
}
|
||||
|
||||
// Given a small photo zoomed in, the whole-frame render already holds all its pixels
|
||||
#[test]
|
||||
fn a_small_photo_needs_no_window() {
|
||||
let p = plan(&auto(), sizes(8000.0, 1400.0, 1000));
|
||||
assert_eq!((p.main_edge, p.window_edge), (1000, None));
|
||||
}
|
||||
|
||||
// Given a slider drag at 400 % (drafts at 0.6 scale), the window plan does not change: it is
|
||||
// the whole-frame draft that gets cheaper
|
||||
#[test]
|
||||
fn a_drag_shrinks_the_whole_frame_draft_not_the_window() {
|
||||
let still = plan(&auto(), sizes(24000.0, 2800.0, 6000));
|
||||
let drag = plan(&auto(), ViewSizes { draft: Some(0.6), ..sizes(24000.0, 2800.0, 6000) });
|
||||
assert!(drag.main_edge < still.main_edge, "{drag:?} vs {still:?}");
|
||||
assert_eq!(drag.window_edge, still.window_edge);
|
||||
}
|
||||
|
||||
// Given no window (a whole-frame diagnostic), the view at rest renders the
|
||||
// whole frame at the size it is drawn, but a slider drag drafts at the canvas size like the
|
||||
// windowed path
|
||||
#[test]
|
||||
fn without_a_window_a_drag_drafts_at_the_canvas_size() {
|
||||
let no_window = |draft: Option<f32>| ViewSizes { windows: false, draft, ..sizes(24000.0, 2800.0, 6000) };
|
||||
let still = plan(&auto(), no_window(None));
|
||||
let drag = plan(&auto(), no_window(Some(0.6)));
|
||||
assert_eq!(still.window_edge, None);
|
||||
assert!(still.main_edge > 2800, "at rest the zoomed frame: {still:?}");
|
||||
assert!(drag.main_edge <= 2800, "a drag drafts at most the canvas: {drag:?}");
|
||||
}
|
||||
|
||||
// Given a slider drag, the GPU renders it at full size and the CPU a smaller draft (#330)
|
||||
#[test]
|
||||
fn a_drag_shrinks_the_draft_only_on_the_cpu() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let mut gpu = DragScale::default();
|
||||
assert_eq!(gpu.scale(None, true, None, 0.0), None, "no drag, no draft");
|
||||
assert_eq!(gpu.scale(Some((PhotoId(1), "Exposure")), true, None, 0.0), Some(1.0));
|
||||
let mut cpu = DragScale::default();
|
||||
assert_eq!(cpu.scale(Some((PhotoId(1), "Exposure")), false, None, 0.0), Some(CPU_DRAFT_SCALE));
|
||||
}
|
||||
|
||||
// Given a drag that started before the GPU was ready, it keeps its size until it ends: the
|
||||
// resolution never jumps in the middle of a drag
|
||||
#[test]
|
||||
fn a_drag_keeps_its_draft_size_until_it_ends() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let mut d = DragScale::default();
|
||||
let drag = Some((PhotoId(1), "Exposure"));
|
||||
assert_eq!(d.scale(drag, false, None, 0.0), Some(CPU_DRAFT_SCALE));
|
||||
assert_eq!(d.scale(drag, true, None, 0.0), Some(CPU_DRAFT_SCALE), "the GPU came up mid-drag");
|
||||
assert_eq!(d.scale(None, true, None, 0.0), None);
|
||||
assert_eq!(d.scale(drag, true, None, 0.0), Some(1.0), "the next drag gets full size");
|
||||
// and the other way: the GPU lost mid-drag keeps the drag at full size until it ends
|
||||
assert_eq!(d.scale(drag, false, None, 0.0), Some(1.0));
|
||||
}
|
||||
|
||||
/// One whole drag of `drag` at full size whose drafts all take `ms` to show, ending at `now`.
|
||||
fn timed_drag(d: &mut DragScale, drag: Option<(lightcraft_catalog::PhotoId, &str)>, ms: f64, now: f64) -> Option<f32> {
|
||||
let scale = d.scale(drag, true, None, now);
|
||||
for key in 1..=6 {
|
||||
d.observe(key, ms, None);
|
||||
}
|
||||
d.scale(None, true, None, now);
|
||||
scale
|
||||
}
|
||||
|
||||
// Given full-size drafts over the slider budget twice in a row (a LUT profile or refined mask
|
||||
// the CPU renders, a photo past a GPU limit, a slow GPU), the next drag of that control on
|
||||
// that photo drafts small; once could be a busy moment. Other controls and photos keep full size
|
||||
#[test]
|
||||
fn a_drag_over_budget_twice_drafts_small_the_next_time() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let mut d = DragScale::default();
|
||||
let exposure = Some((PhotoId(1), "Exposure"));
|
||||
assert_eq!(d.scale(exposure, true, Some(7), 0.0), Some(1.0));
|
||||
d.observe(7, 400.0, None); // the render from before the drag: not a draft of it
|
||||
for (key, ms) in [(8, 300.0), (9, 30.0), (10, 25.0), (11, 9.0)] {
|
||||
d.observe(key, ms, None);
|
||||
d.observe(key, ms, None); // a render is timed once, however many frames show it
|
||||
}
|
||||
assert_eq!(d.scale(exposure, true, None, 0.0), Some(1.0), "over budget, but not far enough to give up mid-drag");
|
||||
assert_eq!(d.scale(None, true, None, 0.0), None);
|
||||
assert_eq!(timed_drag(&mut d, exposure, 25.0, 1.0), Some(1.0), "one slow drag: maybe the machine was busy");
|
||||
assert_eq!(d.scale(exposure, true, None, 2.0), Some(CPU_DRAFT_SCALE));
|
||||
d.scale(None, true, None, 2.0);
|
||||
assert_eq!(d.scale(Some((PhotoId(1), "Contrast")), true, None, 2.0), Some(1.0));
|
||||
assert_eq!(d.scale(Some((PhotoId(2), "Exposure")), true, None, 2.0), Some(1.0));
|
||||
}
|
||||
|
||||
// Given a pair found slow, it drafts small only for a while: then a drag tries full size
|
||||
// again; a slow retry doubles the wait (up to its cap), a fast one forgets the verdict
|
||||
#[test]
|
||||
fn a_slow_verdict_expires_and_is_retried() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let mut d = DragScale::default();
|
||||
let drag = Some((PhotoId(1), "Exposure"));
|
||||
timed_drag(&mut d, drag, 25.0, 0.0);
|
||||
timed_drag(&mut d, drag, 25.0, 0.0);
|
||||
assert_eq!(timed_drag(&mut d, drag, 25.0, RETRY_AFTER_S - 1.0), Some(CPU_DRAFT_SCALE), "slow until the wait is over");
|
||||
assert_eq!(timed_drag(&mut d, drag, 25.0, RETRY_AFTER_S), Some(1.0), "then a drag tries full size: still slow");
|
||||
let mut at = RETRY_AFTER_S;
|
||||
let mut wait = RETRY_AFTER_S * 2.0;
|
||||
for _ in 0..6 {
|
||||
assert_eq!(timed_drag(&mut d, drag, 25.0, at + wait - 1.0), Some(CPU_DRAFT_SCALE), "the wait doubled to {wait} s");
|
||||
assert_eq!(timed_drag(&mut d, drag, 25.0, at + wait), Some(1.0), "a retry after {wait} s, slow again");
|
||||
at += wait;
|
||||
wait = (wait * 2.0).min(RETRY_MAX_S);
|
||||
}
|
||||
assert_eq!(wait, RETRY_MAX_S, "the wait stops growing");
|
||||
assert_eq!(timed_drag(&mut d, drag, 8.0, at + wait), Some(1.0), "a retry, fast this time");
|
||||
assert_eq!(timed_drag(&mut d, drag, 25.0, at + wait + 1.0), Some(1.0), "forgotten: one slow drag is a suspicion again");
|
||||
assert_eq!(timed_drag(&mut d, drag, 25.0, at + wait + 2.0), Some(1.0), "suspect: still full size");
|
||||
assert_eq!(d.scale(drag, true, None, at + wait + 3.0), Some(CPU_DRAFT_SCALE), "slow from the start again: wait 30 s");
|
||||
}
|
||||
|
||||
// Given a drag only just over budget, it doesn't count against the pair (it would flip between
|
||||
// the two sizes); one clearly over does
|
||||
#[test]
|
||||
fn only_a_drag_clearly_over_budget_counts() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let mut d = DragScale::default();
|
||||
let drag = Some((PhotoId(1), "Exposure"));
|
||||
for t in 0..4 {
|
||||
timed_drag(&mut d, drag, 18.0, f64::from(t));
|
||||
}
|
||||
assert_eq!(d.scale(drag, true, None, 4.0), Some(1.0), "18 ms drafts: just over the 16 ms budget");
|
||||
d.scale(None, true, None, 4.0);
|
||||
timed_drag(&mut d, drag, VERDICT_MS + 5.0, 5.0);
|
||||
timed_drag(&mut d, drag, VERDICT_MS + 5.0, 6.0);
|
||||
assert_eq!(d.scale(drag, true, None, 7.0), Some(CPU_DRAFT_SCALE));
|
||||
}
|
||||
|
||||
// Given one slow drag while the machine was busy, then fast ones, the slider keeps full size
|
||||
#[test]
|
||||
fn a_busy_moment_doesnt_make_a_slider_draft_small() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let mut d = DragScale::default();
|
||||
let drag = Some((PhotoId(1), "Exposure"));
|
||||
timed_drag(&mut d, drag, 40.0, 0.0);
|
||||
for t in 1..=3 {
|
||||
assert_eq!(timed_drag(&mut d, drag, 8.0, f64::from(t)), Some(1.0));
|
||||
}
|
||||
timed_drag(&mut d, drag, 40.0, 4.0);
|
||||
assert_eq!(d.scale(drag, true, None, 5.0), Some(1.0), "the fast drags forgot the first slow one");
|
||||
}
|
||||
|
||||
// Given a hostile host clock (NaN, infinite, going backwards), a drag never panics; a clock it
|
||||
// can't read tries full size
|
||||
#[test]
|
||||
fn a_hostile_clock_never_panics() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let mut d = DragScale::default();
|
||||
let drag = Some((PhotoId(1), "Exposure"));
|
||||
timed_drag(&mut d, drag, 25.0, 100.0);
|
||||
timed_drag(&mut d, drag, 25.0, 100.0);
|
||||
assert_eq!(d.scale(drag, true, None, 50.0), Some(CPU_DRAFT_SCALE), "the clock went backwards: still slow");
|
||||
d.scale(None, true, None, 50.0);
|
||||
assert_eq!(d.scale(drag, true, None, f64::NAN), Some(1.0));
|
||||
d.scale(None, true, None, f64::NAN);
|
||||
for now in [f64::INFINITY, f64::NEG_INFINITY, f64::NAN, -1.0] {
|
||||
timed_drag(&mut d, drag, 25.0, now);
|
||||
}
|
||||
}
|
||||
|
||||
// Given full-size drafts within budget (the first one, rebuilding the stage caches, may be
|
||||
// slow), the next drag stays at full size
|
||||
#[test]
|
||||
fn a_drag_within_budget_stays_full_size() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let mut d = DragScale::default();
|
||||
let drag = Some((PhotoId(1), "Exposure"));
|
||||
d.scale(drag, true, None, 0.0);
|
||||
for (key, ms) in [(1, 120.0), (2, 4.0), (3, 6.0), (4, 30.0), (5, 5.0)] {
|
||||
d.observe(key, ms, None);
|
||||
}
|
||||
d.scale(None, true, None, 0.0);
|
||||
assert_eq!(d.scale(drag, true, None, 0.0), Some(1.0));
|
||||
// hostile times are ignored, never a panic
|
||||
for (key, ms) in [(6, f64::NAN), (7, f64::INFINITY), (8, -1.0)] {
|
||||
d.observe(key, ms, None);
|
||||
}
|
||||
d.scale(None, true, None, 0.0);
|
||||
assert_eq!(d.scale(drag, true, None, 0.0), Some(1.0));
|
||||
}
|
||||
|
||||
// Given renders within budget whose frames (the texture upload) take it over, the drag counts
|
||||
// as over budget: a render time alone said 231 of 234 drafts fit while none showed in 16 ms
|
||||
#[test]
|
||||
fn a_drafts_time_includes_the_frame_that_shows_it() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let mut d = DragScale::default();
|
||||
let drag = Some((PhotoId(1), "Exposure"));
|
||||
for _ in 0..2 {
|
||||
d.scale(drag, true, None, 0.0);
|
||||
for key in 1..=5 {
|
||||
d.observe(key, 8.0, Some(14.0)); // the previous frame, which uploaded the last draft
|
||||
}
|
||||
d.observe(5, 8.0, Some(14.0));
|
||||
d.scale(None, true, None, 0.0);
|
||||
}
|
||||
assert_eq!(d.scale(drag, true, None, 0.0), Some(CPU_DRAFT_SCALE), "8 ms renders + 14 ms frames: over budget");
|
||||
// the same renders without the host's frame times (headless) stay within budget
|
||||
let mut headless = DragScale::default();
|
||||
headless.scale(drag, true, None, 0.0);
|
||||
for _ in 0..2 {
|
||||
timed_drag(&mut headless, drag, 8.0, 0.0);
|
||||
}
|
||||
assert_eq!(headless.scale(drag, true, None, 0.0), Some(1.0));
|
||||
// a hostile frame time counts as nothing, never a panic
|
||||
let mut hostile = DragScale::default();
|
||||
hostile.scale(drag, true, None, 0.0);
|
||||
for (key, frame) in [(1, f64::NAN), (2, f64::INFINITY), (3, -1.0), (4, f64::NAN)] {
|
||||
hostile.observe(key, 8.0, Some(frame));
|
||||
}
|
||||
hostile.scale(None, true, None, 0.0);
|
||||
assert_eq!(hostile.scale(drag, true, None, 0.0), Some(1.0));
|
||||
}
|
||||
|
||||
// Given more slow drags than are remembered, only the one remembered longest ago is forgotten:
|
||||
// clearing them all would bring back a slow full-size drag for every pair
|
||||
#[test]
|
||||
fn a_full_slow_set_forgets_only_the_oldest_drag() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let mut d = DragScale::default();
|
||||
for photo in 0..=VERDICTS_KEPT as u64 {
|
||||
d.remember_slow(PhotoId(photo), "Exposure");
|
||||
}
|
||||
assert_eq!(d.scale(Some((PhotoId(0), "Exposure")), true, None, 0.0), Some(1.0), "the oldest is forgotten");
|
||||
d.scale(None, true, None, 0.0);
|
||||
for photo in [1, VERDICTS_KEPT as u64] {
|
||||
assert_eq!(d.scale(Some((PhotoId(photo), "Exposure")), true, None, 0.0), Some(CPU_DRAFT_SCALE), "photo {photo}");
|
||||
d.scale(None, true, None, 0.0);
|
||||
}
|
||||
d.remember_slow(PhotoId(1), "Exposure"); // already remembered: nothing is forgotten
|
||||
assert_eq!(d.scale(Some((PhotoId(2), "Exposure")), true, None, 0.0), Some(CPU_DRAFT_SCALE));
|
||||
}
|
||||
|
||||
// Given draft frames that took seconds (the window hidden or occluded mid-drag), they count as
|
||||
// unknown: fast renders don't mark the drag slow because the window wasn't drawing. Two such
|
||||
// drags, since one slow drag only makes the pair suspect
|
||||
#[test]
|
||||
fn a_hidden_windows_frame_times_dont_mark_a_drag_slow() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let mut d = DragScale::default();
|
||||
let drag = Some((PhotoId(1), "Exposure"));
|
||||
for t in 0..2u32 {
|
||||
assert_eq!(d.scale(drag, true, None, f64::from(t)), Some(1.0), "drag {}", t + 1);
|
||||
for key in 1..=6 {
|
||||
d.observe(u64::from(t) * 10 + key, 6.0, Some(4000.0));
|
||||
}
|
||||
d.scale(None, true, None, f64::from(t));
|
||||
}
|
||||
assert_eq!(d.scale(drag, true, None, 2.0), Some(1.0), "after two hidden-window drags: still full size");
|
||||
}
|
||||
|
||||
// Given full-size drafts far over budget (a refined mask the CPU renders at 190 ms a step),
|
||||
// the drag drops to the small draft after a few of them instead of at its end; after a second
|
||||
// such drag, the next starts small
|
||||
#[test]
|
||||
fn a_drag_far_over_budget_drafts_small_before_it_ends() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let mut d = DragScale::default();
|
||||
let drag = Some((PhotoId(1), "Exposure"));
|
||||
assert_eq!(d.scale(drag, true, None, 0.0), Some(1.0));
|
||||
let mut scales = Vec::new();
|
||||
for key in 1..=8 {
|
||||
d.observe(key, 190.0, None);
|
||||
scales.push(d.scale(drag, true, None, 0.0));
|
||||
}
|
||||
// drafts 1–4 are timed one frame late; the median of 2–4 decides on the fifth frame
|
||||
assert_eq!(scales[..4], [Some(1.0); 4], "{scales:?}");
|
||||
assert_eq!(scales[4..], [Some(CPU_DRAFT_SCALE); 4], "{scales:?}");
|
||||
d.scale(None, true, None, 0.0);
|
||||
assert_eq!(timed_drag(&mut d, drag, 190.0, 1.0), Some(1.0), "one such drag: maybe a busy moment");
|
||||
assert_eq!(d.scale(drag, true, None, 2.0), Some(CPU_DRAFT_SCALE));
|
||||
}
|
||||
|
||||
// Given a full-size drag zoomed in (a window covers the view), the hidden whole-frame draft is
|
||||
// the small one: at 1:1 on a Retina display it cost 7–9 ms a step for pixels nobody sees
|
||||
#[test]
|
||||
fn a_full_size_drag_under_a_window_drafts_the_whole_frame_small() {
|
||||
let zoomed = sizes(24000.0, 2800.0, 6000);
|
||||
let small = plan(&auto(), ViewSizes { draft: Some(CPU_DRAFT_SCALE), ..zoomed });
|
||||
let full = plan(&auto(), ViewSizes { draft: Some(1.0), ..zoomed });
|
||||
assert!(full.window_edge.is_some(), "{full:?}");
|
||||
assert_eq!(full, small);
|
||||
// at fit without a window, full size stays full size
|
||||
let fit = sizes(2800.0, 2800.0, 6000);
|
||||
assert_eq!(plan(&auto(), ViewSizes { draft: Some(1.0), ..fit }), plan(&auto(), fit));
|
||||
}
|
||||
|
||||
// Given a drag at fit at full size, the draft is as sharp as the render at rest
|
||||
#[test]
|
||||
fn a_full_size_drag_at_fit_keeps_the_resolution() {
|
||||
for drawn in [1400.0, 2800.0] {
|
||||
let still = plan(&auto(), sizes(drawn, drawn, 6000));
|
||||
let drag = plan(&auto(), ViewSizes { draft: Some(1.0), ..sizes(drawn, drawn, 6000) });
|
||||
assert_eq!(drag, still, "{drawn}");
|
||||
}
|
||||
}
|
||||
|
||||
// Given no window and a full-size drag, the draft is still at most the canvas: a drag at 1:1
|
||||
// must not render the photo's every pixel each frame
|
||||
#[test]
|
||||
fn a_full_size_drag_without_a_window_drafts_at_the_canvas_size() {
|
||||
let drag = plan(&auto(), ViewSizes { windows: false, draft: Some(1.0), ..sizes(24000.0, 2800.0, 6000) });
|
||||
assert!(drag.main_edge <= 2816, "{drag:?}");
|
||||
}
|
||||
|
||||
// Given the user capped the preview size, the whole-frame render obeys it, and a fit view
|
||||
// stays on it (no original decoded); a zoomed view still gets its sharp window
|
||||
#[test]
|
||||
fn a_preview_limit_caps_the_whole_frame_render_and_spares_the_fit_view() {
|
||||
let s = AppSettings { preview_limit: 1600, ..Default::default() };
|
||||
let fit = plan(&s, sizes(2800.0, 2800.0, 6000));
|
||||
assert_eq!((fit.main_edge, fit.window_edge), (1600, None));
|
||||
let zoomed = plan(&s, sizes(6000.0, 2800.0, 6000));
|
||||
assert_eq!((zoomed.main_edge, zoomed.window_edge), (1600, Some(6000)));
|
||||
}
|
||||
|
||||
// Given a GPU with 2048 px textures, the whole-frame render obeys it, the window frame need not
|
||||
// (a window is cut to the texture side separately)
|
||||
#[test]
|
||||
fn the_texture_side_caps_the_whole_frame_render_only() {
|
||||
let p = plan(&auto(), ViewSizes { texture_side: 2048, ..sizes(6000.0, 2800.0, 6000) });
|
||||
assert_eq!((p.main_edge, p.window_edge), (2048, Some(6000)));
|
||||
}
|
||||
|
||||
// Given a picture that is not the frame (an embedded JPEG cropped differently), no window
|
||||
#[test]
|
||||
fn a_window_needs_a_picture_with_the_frames_aspect() {
|
||||
assert!(same_aspect(1.5, 1.5) && same_aspect(1.51, 1.5));
|
||||
assert!(!same_aspect(1.5, 1.0) && !same_aspect(f32::NAN, 1.0) && !same_aspect(1.0, 0.0));
|
||||
}
|
||||
|
||||
// Given a drag at fit on a 4K or 5K display (where the fit view has a window), the window stays:
|
||||
// it is only the whole-frame draft that gets smaller
|
||||
#[test]
|
||||
fn a_drag_at_fit_on_a_large_display_keeps_the_window() {
|
||||
for drawn in [3300.0, 3840.0, 4096.0, 5120.0] {
|
||||
let still = plan(&auto(), sizes(drawn, drawn, 6000));
|
||||
let drag = plan(&auto(), ViewSizes { draft: Some(0.6), ..sizes(drawn, drawn, 6000) });
|
||||
assert!(still.window_edge.is_some(), "{drawn}: {still:?}");
|
||||
assert_eq!(drag.window_edge, still.window_edge, "{drawn}");
|
||||
}
|
||||
}
|
||||
|
||||
// Given a mode whose picture can't be a window (a whole-frame diagnostic, or a
|
||||
// spot read too far for one render), the whole-frame render is as big as the view
|
||||
#[test]
|
||||
fn without_windows_the_whole_frame_is_rendered_at_the_drawn_size() {
|
||||
let p = plan(&auto(), ViewSizes { windows: false, ..sizes(6000.0, 2800.0, 6000) });
|
||||
assert_eq!((p.main_edge, p.window_edge), (6000, None));
|
||||
let fit = plan(&auto(), ViewSizes { windows: false, ..sizes(2800.0, 2800.0, 6000) });
|
||||
assert_eq!((fit.main_edge, fit.window_edge), (2816, None));
|
||||
let deep = plan(&auto(), ViewSizes { windows: false, ..sizes(48000.0, 2800.0, 6000) });
|
||||
assert_eq!(deep.main_edge, 6000, "never above the photo's own pixels");
|
||||
let small = plan(&auto(), ViewSizes { windows: false, texture_side: 2048, ..sizes(6000.0, 2800.0, 6000) });
|
||||
assert_eq!(small.main_edge, 2048);
|
||||
}
|
||||
|
||||
// Given a user who chose a preview size above the preview source level (3840 or 5120 on a big
|
||||
// display), the whole-frame render is that big
|
||||
#[test]
|
||||
fn a_large_preview_limit_is_honoured() {
|
||||
for limit in [3840u32, 5120] {
|
||||
let s = AppSettings { preview_limit: limit, ..Default::default() };
|
||||
let p = plan(&s, sizes(5120.0, 5120.0, 6000));
|
||||
assert_eq!(p.main_edge, limit as usize, "{limit}");
|
||||
assert_eq!(p.window_edge, None, "a fit view at the limit needs no window");
|
||||
}
|
||||
let s = AppSettings { preview_limit: 2560, ..Default::default() };
|
||||
assert_eq!(plan(&s, sizes(5120.0, 5120.0, 6000)).main_edge, 2560);
|
||||
}
|
||||
|
||||
// Given a medium format photo wider than 8192 px, 1:1 is its own pixels
|
||||
#[test]
|
||||
fn a_window_frame_can_be_as_big_as_the_photo() {
|
||||
let p = plan(&auto(), sizes(40000.0, 2800.0, 11648));
|
||||
assert_eq!(p.window_edge, Some(11648));
|
||||
}
|
||||
|
||||
// Given a drag at fit on a Retina Mac, the window does not switch on (it would flicker with the drag)
|
||||
#[test]
|
||||
fn a_drag_at_fit_does_not_switch_the_window_on() {
|
||||
let p = plan(&auto(), ViewSizes { draft: Some(0.6), ..sizes(2800.0, 2800.0, 6000) });
|
||||
assert_eq!(p.window_edge, None);
|
||||
}
|
||||
|
||||
// Given a pinch from fit to 400 %, the sizes of the first frame hold until the gesture is quiet
|
||||
#[test]
|
||||
fn a_pinch_holds_the_render_sizes() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let (fit, zoomed) = (plan(&auto(), sizes(1400.0, 1400.0, 6000)), plan(&auto(), sizes(24000.0, 1400.0, 6000)));
|
||||
assert_ne!(fit, zoomed);
|
||||
let mut hold = SizeHold::default();
|
||||
let p = PhotoId(1);
|
||||
assert_eq!(hold.apply(p, 0.0, false, fit), fit);
|
||||
// the pinch runs for a second: every frame would plan something else
|
||||
for k in 1..60 {
|
||||
assert_eq!(hold.apply(p, k as f64 / 60.0, true, zoomed), fit, "frame {k}");
|
||||
}
|
||||
// it stopped at t = 59/60: still held for a moment (no flicker between two events)…
|
||||
assert!(hold.holding(1.1));
|
||||
assert_eq!(hold.apply(p, 1.1, false, zoomed), fit);
|
||||
// …then the view is planned for where it ended
|
||||
assert!(!hold.holding(59.0 / 60.0 + HOLD_SECS + 0.01));
|
||||
assert_eq!(hold.apply(p, 1.3, false, zoomed), zoomed);
|
||||
assert_eq!(hold.apply(p, 1.4, false, zoomed), zoomed);
|
||||
}
|
||||
|
||||
// Given a pinch, the display-fit size holds like the render sizes and is retaken after it
|
||||
#[test]
|
||||
fn a_pinch_holds_the_display_fit_size() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let (fit, zoomed) = (plan(&auto(), sizes(1400.0, 1400.0, 6000)), plan(&auto(), sizes(24000.0, 1400.0, 6000)));
|
||||
let mut hold = SizeHold::default();
|
||||
let p = PhotoId(1);
|
||||
hold.apply(p, 0.0, false, fit);
|
||||
assert_eq!(hold.display_fit(0.0, false, [1000, 800]), [1000, 800]);
|
||||
for k in 1..60 {
|
||||
let now = k as f64 / 60.0;
|
||||
hold.apply(p, now, true, zoomed);
|
||||
assert_eq!(hold.display_fit(now, true, [1000 + k, 800 + k]), [1000, 800], "frame {k}");
|
||||
}
|
||||
// stopped at t = 59/60: still held within the hold time…
|
||||
hold.apply(p, 1.1, false, zoomed);
|
||||
assert_eq!(hold.display_fit(1.1, false, [2560, 2048]), [1000, 800]);
|
||||
// …then the size it ended at is taken, and kept while nothing moves
|
||||
hold.apply(p, 1.3, false, zoomed);
|
||||
assert_eq!(hold.display_fit(1.3, false, [2560, 2048]), [2560, 2048]);
|
||||
assert_eq!(hold.display_fit(1.4, false, [2561, 2049]), [2561, 2049]);
|
||||
// another photo forgets the held size
|
||||
hold.apply(p, 1.5, true, zoomed);
|
||||
hold.display_fit(1.5, true, [2561, 2049]);
|
||||
hold.apply(PhotoId(2), 1.6, true, zoomed);
|
||||
assert_eq!(hold.display_fit(1.6, true, [640, 480]), [640, 480]);
|
||||
}
|
||||
|
||||
// Given another photo, the held sizes are forgotten
|
||||
#[test]
|
||||
fn another_photo_is_planned_afresh() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let (fit, zoomed) = (plan(&auto(), sizes(1400.0, 1400.0, 6000)), plan(&auto(), sizes(24000.0, 1400.0, 6000)));
|
||||
let mut hold = SizeHold::default();
|
||||
hold.apply(PhotoId(1), 0.0, false, fit);
|
||||
assert_eq!(hold.apply(PhotoId(2), 0.1, true, zoomed), zoomed);
|
||||
}
|
||||
|
||||
// Given side by side, a window is drawn in its pane only; given a wipe, on its side of the line
|
||||
#[test]
|
||||
fn a_window_is_clipped_to_its_pane_or_its_side_of_the_wipe() {
|
||||
use crate::state::BeforeAfter::*;
|
||||
let canvas = egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1000.0, 600.0));
|
||||
let (left, right) = (
|
||||
egui::Rect::from_min_max(egui::pos2(24.0, 24.0), egui::pos2(494.0, 576.0)),
|
||||
egui::Rect::from_min_max(egui::pos2(506.0, 24.0), egui::pos2(976.0, 576.0)),
|
||||
);
|
||||
assert_eq!(window_clip(SideBySide, true, left, canvas, left), left);
|
||||
assert_eq!(window_clip(SideBySide, false, right, canvas, right), right);
|
||||
let img = egui::Rect::from_min_max(egui::pos2(100.0, 50.0), egui::pos2(900.0, 550.0));
|
||||
let before = window_clip(Split, true, canvas, canvas, img);
|
||||
let after = window_clip(Split, false, canvas, canvas, img);
|
||||
assert_eq!((before.max.x, after.min.x), (500.0, 500.0), "the line is the image's middle");
|
||||
assert_eq!(window_clip(SplitTopBottom, true, canvas, canvas, img).max.y, 300.0);
|
||||
assert_eq!(window_clip(SplitTopBottom, false, canvas, canvas, img).min.y, 300.0);
|
||||
// zoomed in, the photo's middle (x = 1600) is off screen: the line is the view's middle
|
||||
let zoomed = egui::Rect::from_min_max(egui::pos2(-200.0, -900.0), egui::pos2(3400.0, 1500.0));
|
||||
assert_eq!(window_clip(Split, false, canvas, canvas, zoomed).min.x, 500.0);
|
||||
assert_eq!(window_clip(SplitTopBottom, true, canvas, canvas, zoomed).max.y, 300.0);
|
||||
assert_eq!(window_clip(Off, false, canvas, canvas, img), canvas);
|
||||
assert_eq!(window_clip(Original, true, canvas, canvas, img), canvas);
|
||||
}
|
||||
|
||||
// Given a window asked for while another is rendering and the original isn't decoded yet, it
|
||||
// waits for the decoded original instead of decoding another copy
|
||||
#[test]
|
||||
fn a_window_waits_for_the_original_another_is_decoding() {
|
||||
assert!(defer_window(true, false));
|
||||
assert!(!defer_window(true, true), "the original is in");
|
||||
assert!(!defer_window(false, false), "nothing is decoding it");
|
||||
}
|
||||
|
||||
// Hostile numbers give a plan, never a panic
|
||||
#[test]
|
||||
fn hostile_sizes_give_a_usable_plan() {
|
||||
for v in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, -3.0, 0.0] {
|
||||
let p = plan(&auto(), ViewSizes { drawn_long: v, canvas_long: v, draft: Some(v), ..sizes(1.0, 1.0, 6000) });
|
||||
assert!((8..=2560).contains(&p.main_edge), "{v}: {p:?}");
|
||||
}
|
||||
let p = plan(&auto(), sizes(1000.0, 1000.0, 0));
|
||||
assert!(p.main_edge >= 8);
|
||||
}
|
||||
|
||||
// Given a visible area in the middle of a big frame, the window covers it with a margin, on the grid
|
||||
#[test]
|
||||
fn the_window_covers_the_visible_area_with_a_margin_on_the_grid() {
|
||||
let w = window_for(48_000, 32_000, (20_000.0, 10_000.0, 21_400.0, 10_900.0), MAX_SPAN).unwrap();
|
||||
let m = margin_for(48_000);
|
||||
assert!(w.x + m <= 20_000 && w.x + w.w >= 21_400 + m, "{w:?}");
|
||||
assert!(w.y + m <= 10_000 && w.y + w.h >= 10_900 + m, "{w:?}");
|
||||
assert_eq!((w.x % SNAP, w.y % SNAP, w.w % SNAP, w.h % SNAP), (0, 0, 0, 0));
|
||||
}
|
||||
|
||||
// Given a small pan, the same window is kept (no re-render); a big pan changes it
|
||||
#[test]
|
||||
fn small_pans_keep_the_window() {
|
||||
let a = window_for(48_000, 32_000, (20_000.0, 10_000.0, 21_400.0, 10_900.0), MAX_SPAN).unwrap();
|
||||
let b = window_for(48_000, 32_000, (20_040.0, 10_030.0, 21_440.0, 10_930.0), MAX_SPAN).unwrap();
|
||||
assert_eq!(a, b);
|
||||
let c = window_for(48_000, 32_000, (26_000.0, 10_000.0, 27_400.0, 10_900.0), MAX_SPAN).unwrap();
|
||||
assert_ne!(a, c);
|
||||
}
|
||||
|
||||
// Given a frame smaller than the margin, or a view at its edge, the window is clamped into it
|
||||
#[test]
|
||||
fn the_window_stays_inside_the_frame() {
|
||||
let w = window_for(3000, 2000, (-500.0, -500.0, 900.0, 700.0), MAX_SPAN).unwrap();
|
||||
assert_eq!((w.x, w.y), (0, 0));
|
||||
let w = window_for(3000, 2000, (2500.0, 1500.0, 9000.0, 9000.0), MAX_SPAN).unwrap();
|
||||
assert!(w.x + w.w <= 3000 && w.y + w.h <= 2000, "{w:?}");
|
||||
assert_eq!((w.x + w.w, w.y + w.h), (3000, 2000));
|
||||
}
|
||||
|
||||
// Given a deeper zoom, the margin grows with the wide stages' kernels (3σ of the base, clarity
|
||||
// and dehaze planes are 0.045 of the frame), up to a cap that bounds the window
|
||||
#[test]
|
||||
fn the_margin_grows_with_the_zoom_up_to_a_cap() {
|
||||
assert_eq!(margin_for(1000), SNAP, "never less than one grid step");
|
||||
assert!(margin_for(8000) > margin_for(3000));
|
||||
assert_eq!(margin_for(48_000), 2304, "4.5 % of an 8:1 frame, under the cap");
|
||||
assert_eq!(margin_for(80_000), MAX_MARGIN);
|
||||
assert_eq!(margin_for(0), SNAP);
|
||||
assert!(margin_for(usize::MAX) <= MAX_MARGIN);
|
||||
for l in [100, 3000, 8000, 48_000, 400_000] {
|
||||
assert_eq!(margin_for(l) % SNAP, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Given a tile rendered for other settings, another photo or another zoom, it is not drawn
|
||||
#[test]
|
||||
fn a_tile_is_drawn_only_while_it_shows_what_the_loupe_shows() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let v = RegionView {
|
||||
photo: PhotoId(1),
|
||||
before: false,
|
||||
key: 7,
|
||||
full: (6000, 4000),
|
||||
window: PixelWindow { x: 0, y: 0, w: 256, h: 256 },
|
||||
settings: 11,
|
||||
};
|
||||
assert!(v.is_current(PhotoId(1), (6000, 4000), 11, false));
|
||||
assert!(!v.is_current(PhotoId(2), (6000, 4000), 11, false), "another photo");
|
||||
assert!(!v.is_current(PhotoId(1), (6000, 4001), 11, false), "another zoom");
|
||||
assert!(!v.is_current(PhotoId(1), (6000, 4000), 12, false), "another look (an edit, an undo)");
|
||||
assert!(v.is_current(PhotoId(1), (6000, 4000), 12, true), "a drag's drafts follow the value");
|
||||
assert!(!v.is_current(PhotoId(2), (6000, 4000), 12, true), "…of this photo only");
|
||||
}
|
||||
|
||||
// Given a GPU that allows 2048 px textures, no window is wider than that
|
||||
#[test]
|
||||
fn a_window_fits_the_texture_side() {
|
||||
for tex in [512, 2048, 4096] {
|
||||
let span = max_span(tex);
|
||||
assert!(span <= tex.max(SNAP) && span.is_multiple_of(SNAP), "{tex}: {span}");
|
||||
let w = window_for(48_000, 32_000, (20_000.0, 10_000.0, 21_400.0, 10_900.0), span).unwrap();
|
||||
assert!(w.w <= span && w.h <= span, "{tex}: {w:?}");
|
||||
}
|
||||
assert_eq!(max_span(100_000), MAX_SPAN);
|
||||
assert_eq!(max_span(0), SNAP);
|
||||
}
|
||||
|
||||
// Given nothing visible, or hostile numbers, there is no window (never a panic)
|
||||
#[test]
|
||||
fn nothing_visible_or_hostile_input_is_no_window() {
|
||||
assert_eq!(window_for(1000, 1000, (2000.0, 0.0, 3000.0, 100.0), MAX_SPAN), None);
|
||||
assert_eq!(window_for(1000, 1000, (10.0, 10.0, 10.0, 90.0), MAX_SPAN), None);
|
||||
assert_eq!(window_for(0, 1000, (0.0, 0.0, 10.0, 10.0), MAX_SPAN), None);
|
||||
for v in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
|
||||
assert_eq!(window_for(1000, 1000, (v, 0.0, 10.0, 10.0), MAX_SPAN), None);
|
||||
}
|
||||
let w = window_for(usize::MAX / 2, usize::MAX / 2, (0.0, 0.0, 1e30, 1e30), MAX_SPAN).unwrap();
|
||||
assert!(w.w <= MAX_SPAN && w.h <= MAX_SPAN, "{w:?}");
|
||||
}
|
||||
|
||||
// Given a view of an enormous area, the window is bounded
|
||||
#[test]
|
||||
fn a_window_is_bounded() {
|
||||
let w = window_for(100_000, 100_000, (0.0, 0.0, 90_000.0, 90_000.0), MAX_SPAN).unwrap();
|
||||
assert!(w.w <= MAX_SPAN && w.h <= MAX_SPAN);
|
||||
}
|
||||
Reference in New Issue
Block a user