Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 143 additions & 66 deletions src/diff.zig
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,6 @@ inline fn increment_coords_by(x: *u32, y: *u32, step: u32, width: u32) void {
}

pub noinline fn compareSameLayouts(base: *const Image, comp: *const Image, diff_output: *?Image, diff_count: *u32, diff_lines: ?*DiffLines, ignore_regions: ?[]struct { u32, u32 }, max_delta: i64, options: DiffOptions) !void {
var x: u32 = 0;
var y: u32 = 0;

const size = (base.height * base.width);
const base_data = base.data;
const comp_data = comp.data;
Expand All @@ -240,42 +237,52 @@ pub noinline fn compareSameLayouts(base: *const Image, comp: *const Image, diff_
const simd_end = (size / SIMD_SIZE) * SIMD_SIZE;

var offset: usize = 0;
const Vec = @Vector(SIMD_SIZE, u32);
var xs: Vec = std.simd.iota(u32, SIMD_SIZE);
var ys: Vec = @as(Vec, @splat(0));
while (offset < simd_end) : (offset += SIMD_SIZE) {
const base_vec: @Vector(SIMD_SIZE, u32) = base_data[offset .. offset + SIMD_SIZE][0..SIMD_SIZE].*;
const comp_vec: @Vector(SIMD_SIZE, u32) = comp_data[offset .. offset + SIMD_SIZE][0..SIMD_SIZE].*;
const base_vec: Vec = base_data[offset .. offset + SIMD_SIZE][0..SIMD_SIZE].*;
const comp_vec: Vec = comp_data[offset .. offset + SIMD_SIZE][0..SIMD_SIZE].*;

const diff_mask = base_vec != comp_vec;
if (!@reduce(.Or, diff_mask)) {
increment_coords_by(&x, &y, SIMD_SIZE, base.width);
continue;
}

for (0..SIMD_SIZE) |i| {
if (diff_mask[i]) {
const pixel_offset = offset + i;
const base_color = base_vec[i];
const comp_color = comp_vec[i];

try processPixelDifference(
pixel_offset,
base_color,
comp_color,
x,
y,
base,
comp,
diff_output,
diff_count,
diff_lines,
ignore_regions,
max_delta,
options,
);
if (@reduce(.Or, diff_mask)) {
for (0..SIMD_SIZE) |i| {
if (diff_mask[i]) {
const pixel_offset = offset + i;
const base_color = base_vec[i];
const comp_color = comp_vec[i];

try processPixelDifference(
pixel_offset,
base_color,
comp_color,
xs[i],
ys[i],
base,
comp,
diff_output,
diff_count,
diff_lines,
ignore_regions,
max_delta,
options,
);
}
}
increment_coords(&x, &y, base.width);
}

xs += @splat(SIMD_SIZE);
const width_vec: Vec = @splat(base.width);
const mask: @Vector(SIMD_SIZE, bool) = xs >= width_vec;
const inc_y: Vec = @select(u32, mask, @as(Vec, @splat(1)), @as(Vec, @splat(0)));
ys += inc_y;
xs -= @select(u32, mask, width_vec, @as(Vec, @splat(0)));
}

const width_usize: usize = base.width;
var x: u32 = @intCast(simd_end % width_usize);
var y: u32 = @intCast(simd_end / width_usize);

// Handle remaining pixels
while (offset < size) : (offset += 1) {
const base_color = base_data[offset];
Expand All @@ -302,47 +309,117 @@ pub noinline fn compareSameLayouts(base: *const Image, comp: *const Image, diff_
}
}

pub fn compareDifferentLayouts(base: *const Image, comp: *const Image, maybe_diff_output: *?Image, diff_count: *u32, diff_lines: ?*DiffLines, ignore_regions: ?[]struct { u32, u32 }, max_delta: i64, options: DiffOptions) !void {
var x: u32 = 0;
var y: u32 = 0;
var offset: u32 = 0;

const size = (base.height * base.width);
while (offset < size) : (offset += 1) {
const base_color = base.readRawPixel(x, y);
pub fn compareDifferentLayouts(base: *const Image, comp: *const Image, diff_output: *?Image, diff_count: *u32, diff_lines: ?*DiffLines, ignore_regions: ?[]struct { u32, u32 }, max_delta: i64, options: DiffOptions) !void {
const base_size = (base.height * base.width);
const base_data = base.data;
const comp_data = comp.data;

if (x >= comp.width or y >= comp.height) {
const alpha = (base_color >> 24) & 0xFF;
if (alpha != 0) {
diff_count.* += 1;
if (maybe_diff_output.*) |*output| {
output.setImgColor(x, y, options.diff_pixel);
const SIMD_SIZE = std.simd.suggestVectorLength(u32) orelse if (HAS_AVX512f) 16 else if (HAS_NEON) 8 else 4;
const min_width: u32 = @min(base.width, comp.width);
const min_height = @min(base.height, comp.height);
const steps: u32 = @intCast(min_width / SIMD_SIZE);
const base_delta: u32 = base.width - steps * SIMD_SIZE;
const comp_delta: u32 = comp.width - steps * SIMD_SIZE;

var base_offset: usize = 0;
var comp_offset: usize = 0;
const Vec = @Vector(SIMD_SIZE, u32);
var xs: Vec = std.simd.iota(u32, SIMD_SIZE);
var y: u32 = 0;
for (0..min_height) |_| {
for (0..steps) |_| {
const base_vec: Vec = base_data[base_offset .. base_offset + SIMD_SIZE][0..SIMD_SIZE].*;
const comp_vec: Vec = comp_data[comp_offset .. comp_offset + SIMD_SIZE][0..SIMD_SIZE].*;

const diff_mask = base_vec != comp_vec;
if (@reduce(.Or, diff_mask)) {
for (0..SIMD_SIZE) |i| {
if (diff_mask[i]) {
const pixel_offset = base_offset + i;
const base_color = base_vec[i];
const comp_color = comp_vec[i];

try processPixelDifference(
pixel_offset,
base_color,
comp_color,
xs[i],
y,
base,
comp,
diff_output,
diff_count,
diff_lines,
ignore_regions,
max_delta,
options,
);
}
}
}

if (diff_lines) |lines| {
lines.addLine(y);
xs += @as(Vec, @splat(SIMD_SIZE));
base_offset += SIMD_SIZE;
comp_offset += SIMD_SIZE;
}
// handle leftover pixels in a row
const processed: u32 = steps * SIMD_SIZE;
const remaining: u32 = base.width - processed;
for (0..remaining) |i| {
const x: u32 = processed + @as(u32, @intCast(i));
const base_color = base_data[base_offset + i];
if (x >= comp.width) {
const alpha = (base_color >> 24) & 0xFF;
if (alpha != 0) {
diff_count.* += 1;
if (diff_output.*) |*output| {
output.setImgColor(x, y, options.diff_pixel);
}

if (diff_lines) |lines| {
lines.addLine(y);
}
}
} else {
const comp_color = comp.readRawPixel(x, y);
try processPixelDifference(
base_offset + i,
base_color,
comp_color,
x,
y,
base,
comp,
diff_output,
diff_count,
diff_lines,
ignore_regions,
max_delta,
options,
);
}
} else {
const comp_color = comp.readRawPixel(x, y);

try processPixelDifference(
offset,
base_color,
comp_color,
x,
y,
base,
comp,
maybe_diff_output,
diff_count,
diff_lines,
ignore_regions,
max_delta,
options,
);
}
xs = std.simd.iota(u32, SIMD_SIZE);
y += 1;
base_offset += base_delta;
comp_offset += comp_delta;
}

// Handle remaining pixels
var x: u32 = 0;
while (base_offset < base_size) : (base_offset += 1) {
const base_color = base_data[base_offset];
const alpha = (base_color >> 24) & 0xFF;
if (alpha != 0) {
diff_count.* += 1;
if (diff_output.*) |*output| {
output.setImgColor(x, y, options.diff_pixel);
}

if (diff_lines) |lines| {
lines.addLine(y);
}
}
increment_coords(&x, &y, base.width);
}
}
Expand Down
51 changes: 46 additions & 5 deletions src/test_core.zig
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,51 @@ test "layoutDifference: diff images with different layouts" {
.output_diff_mask = false,
};

var diff_output, const diff_count, const diff_percentage, var diff_lines = try diff.compare(&img1, &img2, options, allocator);
defer if (diff_output) |*img| img.deinit(allocator);
defer if (diff_lines) |*lines| lines.deinit();
{
var diff_output, const diff_count, const diff_percentage, var diff_lines = try diff.compare(&img1, &img2, options, allocator);
defer if (diff_output) |*img| img.deinit(allocator);
defer if (diff_lines) |*lines| lines.deinit();

try expectEqual(@as(u32, 16), diff_count); // diffPixels
try expectApproxEqRel(@as(f64, 100.0), diff_percentage, 0.001); // diffPercentage
try expectEqual(@as(u32, 16), diff_count); // diffPixels
try expectApproxEqRel(@as(f64, 100.0), diff_percentage, 0.001); // diffPercentage
}
{
var diff_output, const diff_count, const diff_percentage, var diff_lines = try diff.compare(&img2, &img1, options, allocator);
defer if (diff_output) |*img| img.deinit(allocator);
defer if (diff_lines) |*lines| lines.deinit();

try expectEqual(@as(u32, 64), diff_count); // diffPixels
try expectApproxEqRel(@as(f64, 100.0), diff_percentage, 0.001); // diffPercentage
}
}

test "layoutDifference: diff images with different layouts (2)" {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();

var img1 = try loadTestImage("test/png/odiff-logo-dark.png", allocator);
defer img1.deinit(allocator);

var img2 = try loadTestImage("test/png/odiff-logo-dark-rotated.png", allocator);
defer img2.deinit(allocator);

const options = diff.DiffOptions{};

{
var diff_output, const diff_count, const diff_percentage, var diff_lines = try diff.compare(&img1, &img2, options, allocator);
defer if (diff_output) |*img| img.deinit(allocator);
defer if (diff_lines) |*lines| lines.deinit();

try expectEqual(@as(u32, 1209283), diff_count); // diffPixels
try expectApproxEqRel(@as(f64, 70.368), diff_percentage, 0.001); // diffPercentage
}
{
var diff_output, const diff_count, const diff_percentage, var diff_lines = try diff.compare(&img2, &img1, options, allocator);
defer if (diff_output) |*img| img.deinit(allocator);
defer if (diff_lines) |*lines| lines.deinit();

try expectEqual(@as(u32, 1209283), diff_count); // diffPixels
try expectApproxEqRel(@as(f64, 70.368), diff_percentage, 0.001); // diffPercentage
}
}
Binary file added test/png/odiff-logo-dark-rotated.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/png/odiff-logo-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading