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.

However when it comes to font fallback, it does seem like there's some
changes between Windows 10 and 11, so we do need to differentiate
between those now.
  • Loading branch information
CryZe committed Sep 24, 2023
1 parent 25aa0cf commit 576f48a
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 79 deletions.
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,10 @@ 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(windows)'.dev-dependencies]
sysinfo = { version = "0.29.10", default-features = false }

[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
criterion = "0.5.0"
Expand Down
133 changes: 55 additions & 78 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,26 +40,28 @@ fn default() {

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

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

// Font fallback inherently requires fonts from the operating system to
// work. On Windows we have a consistent set of fonts installed for all the
// different languages. We could do the same check on macOS and possibly a
// few other operating systems, which also provide a consistent set of
// fonts, but with a different hash. On Linux however you may have a
// 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))]
#[test]
fn font_fallback() {
// This list is based on the most commonly used writing systems in the
// world:
// https://en.wikipedia.org/wiki/List_of_writing_systems#List_of_writing_systems_by_adoption

let mut run = tests_helper::create_run(&[
// FIXME: Unfortunately we can't use emojis because the vendors like to
// update the look of the emojis every now and then. So for example the
// emojis changed between Windows 10 and 11. We'd have to either detect
// the version of the emoji font or would have to detect the operating
// system version. I believe the latter is something they plan on adding
// into std, so maybe we can eventually use that.
use sysinfo::SystemExt;

let mut run = tests_helper::create_run(&[
// Emoji
// "❤✔👌🤔😂😁🎉💀🤣",

"❤✔👌🤔😂😁🎉💀🤣",
// Braille
"⠃⠗⠁⠊⠇⠇⠑",
// Hebrew
Expand All @@ -84,8 +88,6 @@ fn font_fallback() {
"ไทย",
// Burmese
"မြန်မာ",
// Canadian Aboriginal Syllabics
"ᖃᓂᐅᔮᖅᐸᐃᑦ ᒐᐦᑲᓯᓇᐦᐃᑫᐤ ᑯᖾᖹ ᖿᐟᖻ ᓱᖽᐧᖿ ᑐᑊᘁᗕᑋᗸ",
// Hanzi, Kana
"汉字 漢字 かな カナ",
]);
Expand All @@ -100,15 +102,16 @@ fn font_fallback() {

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

// Font fallback inherently requires fonts from the operating system to
// work. On Windows we have a consistent set of fonts installed for all the
// different languages. We could do the same check on macOS and possibly a
// few other operating systems, which also provide a consistent set of
// fonts, but with a different hash. On Linux however you may have a
// 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");
let system = sysinfo::System::new();
let build_number: u64 = system.kernel_version().unwrap().parse().unwrap();
let expected_hash = if build_number >= 22000 {
// Windows 11
"d16b447322881767"
} else {
// Windows 10
"todo"
};
check(&_state, expected_hash, "font_fallback");
}

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

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

Expand All @@ -158,7 +161,7 @@ fn timer_delta_background() {
check_dims(
&layout.state(&timer.snapshot()),
[250, 300],
"a+nZyaFfX80=",
"bc5b8383ebb556b8",
"timer_delta_background_stopped",
);
}
Expand All @@ -176,9 +179,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 +205,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 +216,7 @@ fn dark_layout() {

check(
&layout.state(&timer.snapshot()),
"T8AQQABqwYc=",
"a47c590792c1bab5",
"dark_layout",
);
}
Expand All @@ -228,7 +236,7 @@ fn subsplits_layout() {
check_dims(
&layout.state(&timer.snapshot()),
[300, 800],
"8/vz8/Pz/+c=",
"57165de23ce37b9c",
"subsplits_layout",
);
}
Expand All @@ -251,7 +259,7 @@ fn display_two_rows() {
check_dims(
&layout.state(&timer.snapshot()),
[200, 100],
"Q0UaMs1J0sA=",
"d174c2f9a0c54d66",
"display_two_rows",
);
}
Expand All @@ -274,7 +282,7 @@ fn single_line_title() {
check_dims(
&layout.state(&timer.snapshot()),
[300, 60],
"ABJtmxt4YZA=",
"5f0a41091c33ecad",
"single_line_title",
);
}
Expand Down Expand Up @@ -308,74 +316,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 +372,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 576f48a

Please sign in to comment.