Skip to content

Commit

Permalink
Replace img_hash dependency
Browse files Browse the repository at this point in the history
While a visual hash is a good idea, the underlying `tiny-skia` is meant
to pixel perfectly match `Skia`. This means that our hash doesn't
actually need to be visual and can be a simple checksum. The `img_hash`
crate is very unmaintained, so we are forced to compile ever more so
outdated dependencies. So replacing it by a simple checksum via the
`seahash` crate makes a lot of sense. The `seahash` crate claims to be
very fast and also claims that the hashes are suitable for checksums. So
it sounds like a good crate to use for this. However this means that on
platforms that don't correctly implement IEEE-754, we can't run the
rendering tests anymore.
  • Loading branch information
CryZe committed Sep 24, 2023
1 parent 25aa0cf commit 4a993ef
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 60 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ windows-sys = { version = "0.48.0", features = [
libc = { version = "0.2.101", optional = true }

[dev-dependencies]
img_hash = "3.1.0"
seahash = "4.1.0"

[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
criterion = "0.5.0"
Expand Down
94 changes: 35 additions & 59 deletions tests/rendering.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
#![cfg(feature = "software-rendering")]
#![cfg(all(
feature = "software-rendering",
not(all(target_arch = "x86", not(target_feature = "sse"))),
))]

mod layout_files;
mod run_files;
#[path = "../src/util/tests_helper.rs"]
mod tests_helper;

use image::Rgba;
use img_hash::{HasherConfig, ImageHash};
use livesplit_core::{
component::{self, timer},
layout::{self, Component, ComponentState, Layout, LayoutDirection, LayoutState},
Expand Down Expand Up @@ -38,7 +40,7 @@ fn default() {

let state = layout.state(&timer.snapshot());

check(&state, "luIAAABAPLM=", "default");
check(&state, "670e0e09bf3dbfed", "default");
}

#[test]
Expand Down Expand Up @@ -108,7 +110,7 @@ fn font_fallback() {
// different set of fonts installed, or possibly even none at all, so we
// can't do the same check there.
#[cfg(all(feature = "font-loading", windows))]
check(&_state, "zeSAgJgEMbI=", "font_fallback");
check(&_state, "f6e103c71d701039", "font_fallback");
}

#[test]
Expand All @@ -119,7 +121,7 @@ fn actual_split_file() {

check(
&layout.state(&timer.snapshot()),
"jMDAARBAPLM=",
"cd9735cf9575f503",
"actual_split_file",
);
}
Expand All @@ -133,7 +135,7 @@ fn wsplit() {
check_dims(
&layout.state(&timer.snapshot()),
[250, 300],
"j/n8/PnZv/c=",
"9c69454a9258e768",
"wsplit",
);
}
Expand All @@ -149,7 +151,7 @@ fn timer_delta_background() {
check_dims(
&layout.state(&timer.snapshot()),
[250, 300],
"a+nRyKBfXc0=",
"fc8e7890593f9da6",
"timer_delta_background_ahead",
);

Expand All @@ -158,7 +160,7 @@ fn timer_delta_background() {
check_dims(
&layout.state(&timer.snapshot()),
[250, 300],
"a+nZyaFfX80=",
"bc5b8383ebb556b8",
"timer_delta_background_stopped",
);
}
Expand All @@ -176,9 +178,14 @@ fn all_components() {

let state = layout.state(&timer.snapshot());

check_dims(&state, [300, 800], "4en3ocnJp/E=", "all_components");
check_dims(&state, [300, 800], "e4db4770453a6d06", "all_components");

check_dims(&state, [150, 800], "SXfHSWVpRkc=", "all_components_thin");
check_dims(
&state,
[150, 800],
"0ecd0bad25453ff6",
"all_components_thin",
);
}

#[test]
Expand All @@ -197,7 +204,7 @@ fn score_split() {
state.components.push(ComponentState::Timer(timer_state));
state.components.push(prev_seg);

check_dims(&state, [300, 400], "jOCAAQTABjc=", "score_split");
check_dims(&state, [300, 400], "6ec6913f5ace6ab6", "score_split");
}

#[test]
Expand All @@ -208,7 +215,7 @@ fn dark_layout() {

check(
&layout.state(&timer.snapshot()),
"T8AQQABqwYc=",
"a47c590792c1bab5",
"dark_layout",
);
}
Expand All @@ -228,7 +235,7 @@ fn subsplits_layout() {
check_dims(
&layout.state(&timer.snapshot()),
[300, 800],
"8/vz8/Pz/+c=",
"57165de23ce37b9c",
"subsplits_layout",
);
}
Expand All @@ -251,7 +258,7 @@ fn display_two_rows() {
check_dims(
&layout.state(&timer.snapshot()),
[200, 100],
"Q0UaMs1J0sA=",
"d174c2f9a0c54d66",
"display_two_rows",
);
}
Expand All @@ -274,7 +281,7 @@ fn single_line_title() {
check_dims(
&layout.state(&timer.snapshot()),
[300, 60],
"ABJtmxt4YZA=",
"5f0a41091c33ecad",
"single_line_title",
);
}
Expand Down Expand Up @@ -308,74 +315,44 @@ fn horizontal() {
check_dims(
&layout.state(&timer.snapshot()),
[1500, 40],
"YnJjcnJSUmM=",
"987157e649936cbb",
"horizontal",
);
}

fn get_comparison_tolerance() -> u32 {
// Without MMX the floating point calculations don't follow IEEE 754, so the tests require a
// tolerance that is greater than 0.
// FIXME: We use SSE as an approximation for the cfg because MMX isn't supported by Rust yet.
if cfg!(all(target_arch = "x86", not(target_feature = "sse"))) {
3
} else {
0
}
}

#[track_caller]
fn check(state: &LayoutState, expected_hash_data: &str, name: &str) {
check_dims(state, [300, 500], expected_hash_data, name);
}

#[track_caller]
fn check_dims(
state: &LayoutState,
dims @ [width, height]: [u32; 2],
expected_hash_data: &str,
name: &str,
) {
fn check_dims(state: &LayoutState, dims: [u32; 2], expected_hash: &str, name: &str) {
let mut renderer = Renderer::new();
renderer.render(state, dims);
let hash_image =
img_hash::image::RgbaImage::from_raw(width, height, renderer.into_image_data()).unwrap();
let image =
image::ImageBuffer::<Rgba<u8>, _>::from_raw(width, height, hash_image.as_raw().as_slice())
.unwrap();

let hasher = HasherConfig::with_bytes_type::<[u8; 8]>().to_hasher();

let calculated_hash = hasher.hash_image(&hash_image);
let calculated_hash_data = calculated_hash.to_base64();
let expected_hash = ImageHash::<[u8; 8]>::from_base64(expected_hash_data).unwrap();
let distance = calculated_hash.dist(&expected_hash);
let hash_image = renderer.image();
let calculated_hash = seahash::hash(&hash_image);
let calculated_hash = format!("{calculated_hash:016x}");

let mut path = PathBuf::from_iter(["target", "renders"]);
fs::create_dir_all(&path).ok();

let mut actual_path = path.clone();
actual_path.push(format!(
"{name}_{}.png",
calculated_hash_data.replace('/', "-"),
));
image.save(&actual_path).ok();
actual_path.push(format!("{name}_{calculated_hash}.png"));
hash_image.save(&actual_path).ok();

if distance > get_comparison_tolerance() {
if calculated_hash != expected_hash {
path.push("diff");
fs::create_dir_all(&path).ok();
path.pop();

let mut expected_path = path.clone();
expected_path.push(format!(
"{name}_{}.png",
expected_hash_data.replace('/', "-"),
));
expected_path.push(format!("{name}_{expected_hash}.png"));
let diff_path = if let Ok(expected_image) = image::open(&expected_path) {
let mut expected_image = expected_image.to_rgba8();
for (x, y, Rgba([r, g, b, a])) in expected_image.enumerate_pixels_mut() {
if x < hash_image.width() && y < hash_image.height() {
let img_hash::image::Rgba([r2, g2, b2, a2]) = *hash_image.get_pixel(x, y);
let image::Rgba([r2, g2, b2, a2]) = *hash_image.get_pixel(x, y);
*r = r.abs_diff(r2);
*g = g.abs_diff(g2);
*b = b.abs_diff(b2);
Expand All @@ -394,10 +371,9 @@ fn check_dims(

panic!(
"Render mismatch for {name}
expected: {expected_hash_data} {}
actual: {calculated_hash_data} {}
diff: {}
distance: {distance}",
expected: {expected_hash} {}
actual: {calculated_hash} {}
diff: {}",
expected_path.display(),
actual_path.display(),
diff_path.display(),
Expand Down

0 comments on commit 4a993ef

Please sign in to comment.