lib/gui/src/properties/raster.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const gui = @import("gui");
3 const hypothesis = @import("hypothesis");
4
5 const Allocator = std.mem.Allocator;
6 const Color = gui.model.UiColor;
7 const Command = gui.paint.Command;
8 const Image = gui.paint.Image;
9 const ImageSet = gui.paint.ImageSet;
10 const Rect = gui.layout.Rect;
11 const Region = gui.paint.Region;
12
13 const max_width = 20;
14 const max_height = 14;
15 const max_pixels = max_width * max_height;
16 const max_commands = 5;
17 const max_images = 2;
18 const max_image_side = 4;
19
20 const ReferenceBounds = struct {
21 x0: u32,
22 y0: u32,
23 x1: u32,
24 y1: u32,
25 };
26
27 pub fn settings() hypothesis.Settings {
28 const base = hypothesis.Settings{
29 .max_examples = 800,
30 .max_choices = 8192,
31 .max_shrinks = 20_000,
32 .target_examples = 800,
33 };
34 return base
35 .withSeed(0x6775_6952_6173_7472)
36 .withDatabase("zig-out/hypothesis-failures/gui");
37 }
38
39 fn drawInt(data: *hypothesis.ConjectureData, min: i64, max: i64, shrink_towards: i64) !i64 {
40 const span: u64 = @intCast(max - min);
41 const towards: u64 = @intCast(shrink_towards - min);
42 const value = try data.drawInteger(0, span, towards);
43 return min + @as(i64, @intCast(value));
44 }
45
46 fn drawCoord(data: *hypothesis.ConjectureData) !f32 {
47 if (try drawInt(data, 0, 7, 1) == 0) {
48 return switch (try drawInt(data, 0, 5, 0)) {
49 0 => -1000.5,
50 1 => 1000.25,
51 2 => 0.1,
52 3 => 3.9999,
53 4 => std.math.nan(f32),
54 else => std.math.inf(f32),
55 };
56 }
57 const whole = try drawInt(data, -6, max_width + 6, 0);
58 const quarter = try drawInt(data, 0, 3, 0);
59 return @as(f32, @floatFromInt(whole)) + @as(f32, @floatFromInt(quarter)) * 0.25;
60 }
61
62 fn drawSpan(data: *hypothesis.ConjectureData, limit: i64) !f32 {
63 const whole = try drawInt(data, 0, limit, 4);
64 const quarter = try drawInt(data, 0, 3, 0);
65 return @as(f32, @floatFromInt(whole)) + @as(f32, @floatFromInt(quarter)) * 0.25;
66 }
67
68 fn drawAlpha(data: *hypothesis.ConjectureData) !u8 {
69 const choices = [_]u8{ 255, 255, 255, 0, 1, 64, 127, 128, 200, 254 };
70 if (try data.drawBoolean()) {
71 return choices[@intCast(try drawInt(data, 0, choices.len - 1, 0))];
72 }
73 return @intCast(try drawInt(data, 0, 255, 255));
74 }
75
76 fn drawColor(data: *hypothesis.ConjectureData) !Color {
77 return .{
78 .r = @intCast(try drawInt(data, 0, 255, 0)),
79 .g = @intCast(try drawInt(data, 0, 255, 0)),
80 .b = @intCast(try drawInt(data, 0, 255, 0)),
81 .a = try drawAlpha(data),
82 };
83 }
84
85 fn drawRect(data: *hypothesis.ConjectureData) !Rect {
86 return .{
87 .x = try drawCoord(data),
88 .y = try drawCoord(data),
89 .width = try drawSpan(data, max_width + 4),
90 .height = try drawSpan(data, max_height + 4),
91 };
92 }
93
94 fn drawClip(data: *hypothesis.ConjectureData, width: u32, height: u32) !Rect {
95 if (try data.drawBoolean()) {
96 return .{
97 .x = 0,
98 .y = 0,
99 .width = @floatFromInt(width),
100 .height = @floatFromInt(height),
101 };
102 }
103 return try drawRect(data);
104 }
105
106 fn drawSource(data: *hypothesis.ConjectureData) !Rect {
107 return switch (try drawInt(data, 0, 2, 0)) {
108 0 => .{},
109 1 => .{
110 .x = @floatFromInt(try drawInt(data, 0, 2, 0)),
111 .y = @floatFromInt(try drawInt(data, 0, 2, 0)),
112 .width = @floatFromInt(try drawInt(data, 1, max_image_side, 1)),
113 .height = @floatFromInt(try drawInt(data, 1, max_image_side, 1)),
114 },
115 else => .{
116 .x = try drawCoord(data),
117 .y = try drawCoord(data),
118 .width = try drawSpan(data, max_image_side * 2),
119 .height = try drawSpan(data, max_image_side * 2),
120 },
121 };
122 }
123
124 fn drawCommand(data: *hypothesis.ConjectureData) !Command {
125 const kind: gui.paint.Kind = switch (try drawInt(data, 0, 5, 0)) {
126 0 => .fill,
127 1 => .stroke,
128 2 => .image,
129 3 => .shadow,
130 4 => .glyph,
131 else => .linear_gradient,
132 };
133 return .{
134 .kind = kind,
135 .rect = try drawRect(data),
136 .clip = try drawClip(data, max_width, max_height),
137 .source = try drawSource(data),
138 .color = try drawColor(data),
139 .color_end = try drawColor(data),
140 .gradient_start = try drawGradientPoint(data),
141 .gradient_end = try drawGradientPoint(data),
142 .radius = try drawSpan(data, 8),
143 .width = try drawSpan(data, 6),
144 .image_index = @intCast(try drawInt(data, 0, max_images, 0)),
145 };
146 }
147
148 fn drawGradientPoint(data: *hypothesis.ConjectureData) !gui.model.UiPoint {
149 return .{
150 .x = @as(f32, @floatFromInt(try drawInt(data, -8, max_width + 8, 0))) * 0.25,
151 .y = @as(f32, @floatFromInt(try drawInt(data, -8, max_height + 8, 0))) * 0.25,
152 };
153 }
154
155 const ImagePool = struct {
156 pixels: [max_images][max_image_side * max_image_side]u32 = undefined,
157 images: [max_images]Image = undefined,
158 count: usize = 0,
159
160 fn draw(self: *ImagePool, data: *hypothesis.ConjectureData) !ImageSet {
161 self.count = @intCast(try drawInt(data, 0, max_images, 1));
162 for (0..self.count) |index| {
163 const width: u32 = @intCast(try drawInt(data, 1, max_image_side, 2));
164 const height: u32 = @intCast(try drawInt(data, 1, max_image_side, 2));
165 for (self.pixels[index][0 .. width * height]) |*value| {
166 const color = try drawColor(data);
167 value.* = gui.paint.packRgba(color);
168 }
169 self.images[index] = .{
170 .width = width,
171 .height = height,
172 .pixels = self.pixels[index][0 .. width * height],
173 };
174 }
175 return .{ .images = self.images[0..self.count] };
176 }
177 };
178
179 pub const Property = struct {
180 pub fn property(data: *hypothesis.ConjectureData, allocator: Allocator) !void {
181 _ = allocator;
182 const width: u32 = @intCast(try drawInt(data, 1, max_width, max_width));
183 const height: u32 = @intCast(try drawInt(data, 1, max_height, max_height));
184 const command_count: usize = @intCast(try drawInt(data, 1, max_commands, 2));
185 var pool = ImagePool{};
186 const images = try pool.draw(data);
187 var commands: [max_commands]Command = undefined;
188 for (commands[0..command_count]) |*command| {
189 command.* = try drawCommand(data);
190 }
191 gui.paint.assignOrders(commands[0..command_count]);
192 const clear = try drawColor(data);
193 const region: Region = if (try data.drawBoolean())
194 Region.full(width, height)
195 else
196 .{
197 .x = @intCast(try drawInt(data, 0, max_width, 0)),
198 .y = @intCast(try drawInt(data, 0, max_height, 0)),
199 .width = @intCast(try drawInt(data, 0, max_width, max_width)),
200 .height = @intCast(try drawInt(data, 0, max_height, max_height)),
201 };
202 const fill_byte: u8 = @intCast(try drawInt(data, 0, 255, 0));
203
204 const pixel_count = @as(usize, width) * @as(usize, height);
205 var actual_bytes: [max_pixels * 4]u8 = undefined;
206 @memset(actual_bytes[0..], fill_byte);
207 var expected_bytes: [max_pixels * 4]u8 = undefined;
208 @memset(expected_bytes[0..], fill_byte);
209
210 try gui.paint.renderCommandsRegionWithImages(
211 commands[0..command_count],
212 .{ .width = width, .height = height, .rgba8 = actual_bytes[0 .. pixel_count * 4] },
213 clear,
214 images,
215 region,
216 );
217 try reference.render(
218 commands[0..command_count],
219 width,
220 height,
221 expected_bytes[0 .. pixel_count * 4],
222 clear,
223 images,
224 region,
225 );
226 try std.testing.expectEqualSlices(u8, expected_bytes[0 .. pixel_count * 4], actual_bytes[0 .. pixel_count * 4]);
227
228 var packed_pixels: [max_pixels]u32 = undefined;
229 const fill_pixel = std.mem.readInt(u32, &.{ fill_byte, fill_byte, fill_byte, fill_byte }, .little);
230 @memset(packed_pixels[0..], fill_pixel);
231 try gui.paint.renderCommandsPackedRegionWithImages(
232 commands[0..command_count],
233 .{ .width = width, .height = height, .pixels = packed_pixels[0..pixel_count] },
234 clear,
235 images,
236 region,
237 );
238 var packed_bytes: [max_pixels * 4]u8 = undefined;
239 try gui.paint.rgba8FromPacked(packed_bytes[0 .. pixel_count * 4], packed_pixels[0..pixel_count]);
240 try std.testing.expectEqualSlices(u8, expected_bytes[0 .. pixel_count * 4], packed_bytes[0 .. pixel_count * 4]);
241 }
242 };
243
244 const FragmentBoundsProperty = struct {
245 pub fn property(
246 data: *hypothesis.ConjectureData,
247 allocator: Allocator,
248 ) !void {
249 const width: u32 = @intCast(
250 try drawInt(data, 1, max_width, max_width),
251 );
252 const height: u32 = @intCast(
253 try drawInt(data, 1, max_height, max_height),
254 );
255 var pool = ImagePool{};
256 const images = try pool.draw(data);
257 var commands = gui.paint.CommandBuffer.init(allocator);
258 defer commands.deinit();
259 const start = commands.items().len;
260 try commands.append(try drawCommand(data));
261 try commands.commitFragment(.{
262 .root_id = 1,
263 .element_id = 1,
264 .namespace = gui.paint.command.fragment_namespace_widget,
265 .part = gui.paint.command.fragment_part_chrome,
266 }, start);
267
268 const damage = if (commands.fragmentItems().len == 0)
269 null
270 else
271 gui.paint.regions.fromRect(
272 commands.fragmentItems()[0].bounds,
273 1,
274 width,
275 height,
276 );
277 const clear = Color{ .r = 17, .g = 31, .b = 47, .a = 255 };
278 const clear_pixel = gui.paint.packRgba(clear);
279 const pixel_count = @as(usize, width) * @as(usize, height);
280 var pixels: [max_pixels]u32 = undefined;
281 try gui.paint.renderCommandsPackedWithImages(
282 commands.items(),
283 .{
284 .width = width,
285 .height = height,
286 .pixels = pixels[0..pixel_count],
287 },
288 clear,
289 images,
290 );
291
292 var y: u32 = 0;
293 while (y < height) : (y += 1) {
294 var x: u32 = 0;
295 while (x < width) : (x += 1) {
296 const index = @as(usize, y) * @as(usize, width) +
297 @as(usize, x);
298 if (pixels[index] == clear_pixel) continue;
299 const region = damage orelse
300 return error.PaintedPixelOutsideFragmentBounds;
301 if (x < region.x or
302 x >= region.x +| region.width or
303 y < region.y or
304 y >= region.y +| region.height)
305 {
306 return error.PaintedPixelOutsideFragmentBounds;
307 }
308 }
309 }
310 }
311 };
312
313 fn expectMatchesReference(commands: []const Command, images: ImageSet) !void {
314 const width = 20;
315 const height = 14;
316 var actual: [width * height * 4]u8 = undefined;
317 @memset(actual[0..], 7);
318 var expected: [width * height * 4]u8 = undefined;
319 @memset(expected[0..], 7);
320 try gui.paint.renderCommandsRegionWithImages(commands, .{ .width = width, .height = height, .rgba8 = actual[0..] }, .{ .r = 30, .g = 40, .b = 50, .a = 255 }, images, Region.full(width, height));
321 try reference.render(commands, width, height, expected[0..], .{ .r = 30, .g = 40, .b = 50, .a = 255 }, images, Region.full(width, height));
322 try std.testing.expectEqualSlices(u8, expected[0..], actual[0..]);
323 }
324
325 test "differential: every command kind matches the reference on fixed frames" {
326 const atlas_pixels = [_]u32{
327 gui.paint.packRgba(.{ .r = 255, .g = 255, .b = 255, .a = 0 }),
328 gui.paint.packRgba(.{ .r = 255, .g = 255, .b = 255, .a = 96 }),
329 gui.paint.packRgba(.{ .r = 200, .g = 100, .b = 50, .a = 255 }),
330 gui.paint.packRgba(.{ .r = 10, .g = 20, .b = 30, .a = 128 }),
331 };
332 const images = ImageSet{ .images = &.{.{ .width = 2, .height = 2, .pixels = atlas_pixels[0..] }} };
333 const clip = Rect{ .x = 1, .y = 0, .width = 17.5, .height = 13 };
334 const cases = [_]Command{
335 .{ .kind = .fill, .rect = .{ .x = 2, .y = 2, .width = 9, .height = 6 }, .clip = clip, .color = .{ .r = 200, .g = 20, .b = 10, .a = 255 } },
336 .{ .kind = .fill, .rect = .{ .x = 1.5, .y = 2.25, .width = 9.5, .height = 6.75 }, .clip = clip, .color = .{ .r = 200, .g = 20, .b = 10, .a = 140 } },
337 .{ .kind = .fill, .rect = .{ .x = 2, .y = 1, .width = 12, .height = 9 }, .clip = clip, .color = .{ .r = 5, .g = 120, .b = 210, .a = 255 }, .radius = 3 },
338 .{ .kind = .fill, .rect = .{ .x = 2.25, .y = 1.5, .width = 12.5, .height = 9.25 }, .clip = clip, .color = .{ .r = 5, .g = 120, .b = 210, .a = 90 }, .radius = 2.5 },
339 .{ .kind = .linear_gradient, .rect = .{ .x = 2, .y = 1, .width = 12, .height = 9 }, .clip = clip, .color = .{ .r = 10, .g = 30, .b = 80, .a = 255 }, .color_end = .{ .r = 90, .g = 160, .b = 240, .a = 255 }, .gradient_start = .{ .x = 2, .y = 1 }, .gradient_end = .{ .x = 14, .y = 10 }, .radius = 3 },
340 .{ .kind = .linear_gradient, .rect = .{ .x = 2.25, .y = 1.5, .width = 12.5, .height = 9.25 }, .clip = clip, .color = .{ .r = 251, .g = 73, .b = 52, .a = 220 }, .color_end = .{ .r = 0, .g = 0, .b = 0, .a = 0 }, .gradient_start = .{ .x = 2.5, .y = 5 }, .gradient_end = .{ .x = 2.75, .y = 5 }, .radius = 2.5 },
341 .{ .kind = .stroke, .rect = .{ .x = 2, .y = 2, .width = 13, .height = 10 }, .clip = clip, .color = .{ .r = 40, .g = 50, .b = 60, .a = 255 }, .radius = 3, .width = 2 },
342 .{ .kind = .stroke, .rect = .{ .x = 1.5, .y = 1.25, .width = 14, .height = 11 }, .clip = clip, .color = .{ .r = 40, .g = 50, .b = 60, .a = 128 }, .width = 1.5 },
343 .{ .kind = .glyph, .rect = .{ .x = 3, .y = 4, .width = 8, .height = 6 }, .clip = clip, .source = .{ .x = 0, .y = 0, .width = 2, .height = 2 }, .color = .{ .r = 20, .g = 30, .b = 40, .a = 255 }, .image_index = 0 },
344 .{ .kind = .glyph, .rect = .{ .x = 3.25, .y = 4.5, .width = 7.5, .height = 5.25 }, .clip = clip, .source = .{ .x = 0, .y = 0, .width = 2, .height = 2 }, .color = .{ .r = 20, .g = 30, .b = 40, .a = 180 }, .image_index = 0 },
345 .{ .kind = .image, .rect = .{ .x = 5, .y = 3, .width = 9, .height = 8 }, .clip = clip, .source = .{}, .color = .{ .r = 255, .g = 255, .b = 255, .a = 255 }, .image_index = 0 },
346 .{ .kind = .image, .rect = .{ .x = 5.75, .y = 3.25, .width = 8.5, .height = 7.75 }, .clip = clip, .source = .{}, .color = .{ .r = 255, .g = 255, .b = 255, .a = 128 }, .image_index = 0 },
347 .{ .kind = .shadow, .rect = .{ .x = 5, .y = 4, .width = 8, .height = 5 }, .clip = clip, .color = .{ .r = 0, .g = 0, .b = 0, .a = 200 }, .radius = 2, .width = 3 },
348 .{ .kind = .shadow, .rect = .{ .x = 5.5, .y = 4.25, .width = 8.25, .height = 5.5 }, .clip = clip, .color = .{ .r = 0, .g = 0, .b = 0, .a = 255 }, .width = 0 },
349 };
350 for (cases) |case| {
351 try expectMatchesReference(&.{case}, images);
352 }
353 try expectMatchesReference(cases[0..], images);
354 }
355
356 test "property: span raster matches the per-pixel reference renderer" {
357 try hypothesis.checkNamed(Property, "gui-raster-differential", settings());
358 }
359
360 test "property: fragment damage contains every painted pixel" {
361 var bounds_settings = settings();
362 bounds_settings.max_examples = 400;
363 bounds_settings.target_examples = 400;
364 try hypothesis.checkNamed(
365 FragmentBoundsProperty,
366 "gui-raster-fragment-bounds",
367 bounds_settings,
368 );
369 }
370
371 const AccyProperty = struct {
372 pub fn property(data: *hypothesis.ConjectureData, allocator: Allocator) !void {
373 const width: u32 = @intCast(try drawInt(data, 1, max_width, max_width));
374 const height: u32 = @intCast(try drawInt(data, 1, max_height, max_height));
375 const command_count: usize = @intCast(try drawInt(data, 1, max_commands, 2));
376 var pool = ImagePool{};
377 const images = try pool.draw(data);
378 var commands: [max_commands]Command = undefined;
379 for (commands[0..command_count]) |*command| {
380 command.* = try drawCommand(data);
381 }
382 gui.paint.assignOrders(commands[0..command_count]);
383 const clear = try drawColor(data);
384 const region: Region = if (try data.drawBoolean())
385 Region.full(width, height)
386 else
387 .{
388 .x = @intCast(try drawInt(data, 0, max_width, 0)),
389 .y = @intCast(try drawInt(data, 0, max_height, 0)),
390 .width = @intCast(try drawInt(data, 0, max_width, max_width)),
391 .height = @intCast(try drawInt(data, 0, max_height, max_height)),
392 };
393 const fill_byte: u8 = @intCast(try drawInt(data, 0, 255, 0));
394 const fill_pixel = std.mem.readInt(u32, &.{ fill_byte, fill_byte, fill_byte, fill_byte }, .little);
395
396 const pixel_count = @as(usize, width) * @as(usize, height);
397 var expected_pixels: [max_pixels]u32 = undefined;
398 @memset(expected_pixels[0..], fill_pixel);
399 var actual_pixels: [max_pixels]u32 = undefined;
400 @memset(actual_pixels[0..], fill_pixel);
401
402 try gui.paint.renderCommandsPackedRegionWithImages(
403 commands[0..command_count],
404 .{ .width = width, .height = height, .pixels = expected_pixels[0..pixel_count] },
405 clear,
406 images,
407 region,
408 );
409 try gui.paint.accy.renderCommandsCpuRegionWithImages(
410 allocator,
411 commands[0..command_count],
412 width,
413 height,
414 actual_pixels[0..pixel_count],
415 clear,
416 images,
417 region,
418 );
419 try std.testing.expectEqualSlices(u32, expected_pixels[0..pixel_count], actual_pixels[0..pixel_count]);
420 }
421 };
422
423 test "property: tiled accy kernel matches the packed raster on the interpreter" {
424 var accy_settings = settings();
425 accy_settings.max_examples = 64;
426 accy_settings.target_examples = 64;
427 try hypothesis.checkNamed(AccyProperty, "gui-raster-accy-differential", accy_settings);
428 }
429
430 const reference = struct {
431 fn render(commands: []const Command, width: u32, height: u32, rgba8: []u8, clear: Color, images: ImageSet, region: Region) !void {
432 if (rgba8.len < @as(usize, width) * @as(usize, height) * 4) return error.BufferTooSmall;
433 const active_region = region.clamped(width, height);
434 if (active_region.pixelCount() == 0) return;
435 const clear_pixel = packRgba(clear);
436 clearRgba8Region(rgba8, width, active_region, clear_pixel);
437 for (commands) |paint| {
438 rasterCommand(rgba8, width, height, paint, images, active_region);
439 }
440 }
441
442 fn packRgba(color: Color) u32 {
443 return @as(u32, color.r) |
444 (@as(u32, color.g) << 8) |
445 (@as(u32, color.b) << 16) |
446 (@as(u32, color.a) << 24);
447 }
448
449 fn rasterCommand(rgba8: []u8, width: u32, height: u32, paint: Command, images: ImageSet, region: Region) void {
450 const bounds = clippedBounds(width, height, paint, region) orelse return;
451 if (paint.kind == .glyph) return rasterGlyphCommand(rgba8, width, paint, images, bounds);
452 var y = bounds.y0;
453 while (y < bounds.y1) : (y += 1) {
454 var x = bounds.x0;
455 while (x < bounds.x1) : (x += 1) {
456 const fx = @as(f32, @floatFromInt(x)) + 0.5;
457 const fy = @as(f32, @floatFromInt(y)) + 0.5;
458 const coverage = coverageCount(paint, fx, fy);
459 if (coverage == 0) continue;
460 const src = commandColorAt(paint, images, fx, fy, coverage) orelse continue;
461 const index = @as(usize, y) * width + x;
462 writePacked(rgba8, index, blendPacked(readPacked(rgba8, index), src));
463 }
464 }
465 }
466
467 fn rasterGlyphCommand(rgba8: []u8, width: u32, paint: Command, images: ImageSet, bounds: ReferenceBounds) void {
468 const image = imageAt(images, paint.image_index) orelse return;
469 const source = sourceRect(paint.source, image);
470 var y = bounds.y0;
471 while (y < bounds.y1) : (y += 1) {
472 var x = bounds.x0;
473 while (x < bounds.x1) : (x += 1) {
474 const fx = @as(f32, @floatFromInt(x)) + 0.5;
475 const fy = @as(f32, @floatFromInt(y)) + 0.5;
476 const coverage = glyphCoverageCount(paint.rect, paint.clip, fx, fy);
477 if (coverage == 0) continue;
478 const sampled = imageSampleKnown(paint, image, source, fx, fy);
479 const src = Color{
480 .r = paint.color.r,
481 .g = paint.color.g,
482 .b = paint.color.b,
483 .a = coverageAlpha(@intCast(div255(@as(u32, sampled.a) * @as(u32, paint.color.a))), coverage),
484 };
485 const index = @as(usize, y) * width + x;
486 writePacked(rgba8, index, blendPacked(readPacked(rgba8, index), src));
487 }
488 }
489 }
490
491 fn glyphCoverageCount(rect: Rect, clip: Rect, x: f32, y: f32) u32 {
492 var count: u32 = 0;
493 inline for (.{ -0.25, 0.25 }) |dy| {
494 inline for (.{ -0.25, 0.25 }) |dx| {
495 if (inside(clip, x + dx, y + dy) and inside(rect, x + dx, y + dy)) count += 1;
496 }
497 }
498 return count;
499 }
500
501 fn clippedBounds(width: u32, height: u32, paint: Command, region: Region) ?ReferenceBounds {
502 const bounds = commandBounds(paint);
503 const region_rect = regionRect(region);
504 const x0 = floorClamp(@max(@max(bounds.x, paint.clip.x), region_rect.x), width);
505 const y0 = floorClamp(@max(@max(bounds.y, paint.clip.y), region_rect.y), height);
506 const x1 = ceilClamp(@min(@min(bounds.x + bounds.width, paint.clip.x + paint.clip.width), region_rect.x + region_rect.width), width);
507 const y1 = ceilClamp(@min(@min(bounds.y + bounds.height, paint.clip.y + paint.clip.height), region_rect.y + region_rect.height), height);
508 if (x0 >= x1 or y0 >= y1) return null;
509 return .{ .x0 = x0, .y0 = y0, .x1 = x1, .y1 = y1 };
510 }
511
512 fn regionRect(region: Region) Rect {
513 return .{
514 .x = @floatFromInt(region.x),
515 .y = @floatFromInt(region.y),
516 .width = @floatFromInt(region.width),
517 .height = @floatFromInt(region.height),
518 };
519 }
520
521 fn clearRgba8Region(rgba8: []u8, width: u32, region: Region, clear_pixel: u32) void {
522 var y = region.y;
523 while (y < region.y + region.height) : (y += 1) {
524 var x = region.x;
525 while (x < region.x + region.width) : (x += 1) {
526 writePacked(rgba8, @as(usize, y) * width + x, clear_pixel);
527 }
528 }
529 }
530
531 fn commandBounds(paint: Command) Rect {
532 return switch (paint.kind) {
533 .shadow => inset(paint.rect, -@max(paint.width, 0)),
534 else => paint.rect,
535 };
536 }
537
538 fn floorClamp(value: f32, limit: u32) u32 {
539 if (!std.math.isFinite(value) or value <= 0) return 0;
540 const floored = @floor(value);
541 if (floored >= @as(f32, @floatFromInt(limit))) return limit;
542 return @intFromFloat(floored);
543 }
544
545 fn ceilClamp(value: f32, limit: u32) u32 {
546 if (!std.math.isFinite(value) or value <= 0) return 0;
547 const ceiled = @ceil(value);
548 if (ceiled >= @as(f32, @floatFromInt(limit))) return limit;
549 return @intFromFloat(ceiled);
550 }
551
552 fn covers(paint: Command, x: f32, y: f32) bool {
553 if (!inside(paint.clip, x, y)) return false;
554 return switch (paint.kind) {
555 .fill, .linear_gradient => insideRounded(paint.rect, @max(paint.radius, 0), x, y),
556 .stroke => strokeCovers(paint.rect, @max(paint.radius, 0), @max(paint.width, 0), x, y),
557 .image => inside(paint.rect, x, y),
558 .shadow => shadowCovers(paint, x, y),
559 .glyph => inside(paint.rect, x, y),
560 };
561 }
562
563 fn coverageCount(paint: Command, x: f32, y: f32) u32 {
564 var count: u32 = 0;
565 inline for (.{ -0.25, 0.25 }) |dy| {
566 inline for (.{ -0.25, 0.25 }) |dx| {
567 if (covers(paint, x + dx, y + dy)) count += 1;
568 }
569 }
570 return count;
571 }
572
573 fn colorWithCoverage(color: Color, coverage: u32) Color {
574 return .{
575 .r = color.r,
576 .g = color.g,
577 .b = color.b,
578 .a = coverageAlpha(color.a, coverage),
579 };
580 }
581
582 fn coverageAlpha(alpha: u8, coverage: u32) u8 {
583 return @intCast((@as(u32, alpha) * coverage + 2) / 4);
584 }
585
586 fn commandColorAt(paint: Command, images: ImageSet, x: f32, y: f32, coverage: u32) ?Color {
587 return switch (paint.kind) {
588 .fill, .stroke => colorWithCoverage(paint.color, coverage),
589 .linear_gradient => colorWithCoverage(referenceGradientColorAt(paint, x, y), coverage),
590 .image => imageColorAt(paint, images, x, y, coverage),
591 .shadow => shadowColorAt(paint, x, y, coverage),
592 .glyph => glyphColorAt(paint, images, x, y, coverage),
593 };
594 }
595
596 fn referenceGradientColorAt(paint: Command, x: f32, y: f32) Color {
597 const dx = paint.gradient_end.x - paint.gradient_start.x;
598 const dy = paint.gradient_end.y - paint.gradient_start.y;
599 const length_squared = dx * dx + dy * dy;
600 const t = if (!std.math.isFinite(length_squared) or length_squared <= std.math.floatEps(f32))
601 @as(f32, 1)
602 else
603 std.math.clamp(
604 ((x - paint.gradient_start.x) * dx + (y - paint.gradient_start.y) * dy) / length_squared,
605 @as(f32, 0),
606 @as(f32, 1),
607 );
608 const alpha_value = referenceInterpolate(paint.color.a, paint.color_end.a, t);
609 if (alpha_value <= 0) return .{ .a = 0 };
610 return .{
611 .r = referenceUnpremultiplied(paint.color.r, paint.color.a, paint.color_end.r, paint.color_end.a, t, alpha_value),
612 .g = referenceUnpremultiplied(paint.color.g, paint.color.a, paint.color_end.g, paint.color_end.a, t, alpha_value),
613 .b = referenceUnpremultiplied(paint.color.b, paint.color.a, paint.color_end.b, paint.color_end.a, t, alpha_value),
614 .a = referenceRoundedChannel(alpha_value),
615 };
616 }
617
618 fn referenceUnpremultiplied(start: u8, start_alpha: u8, end: u8, end_alpha: u8, t: f32, alpha_value: f32) u8 {
619 const start_value = @as(f32, @floatFromInt(start)) * @as(f32, @floatFromInt(start_alpha));
620 const end_value = @as(f32, @floatFromInt(end)) * @as(f32, @floatFromInt(end_alpha));
621 return referenceRoundedChannel((start_value + (end_value - start_value) * t) / alpha_value);
622 }
623
624 fn referenceInterpolate(start: u8, end: u8, t: f32) f32 {
625 const start_value: f32 = @floatFromInt(start);
626 const end_value: f32 = @floatFromInt(end);
627 return start_value + (end_value - start_value) * t;
628 }
629
630 fn referenceRoundedChannel(value: f32) u8 {
631 return @intFromFloat(std.math.clamp(@round(value), @as(f32, 0), @as(f32, 255)));
632 }
633
634 fn shadowColorAt(paint: Command, x: f32, y: f32, coverage: u32) Color {
635 const alpha_value = shadowAlphaAt(paint, x, y);
636 return .{
637 .r = paint.color.r,
638 .g = paint.color.g,
639 .b = paint.color.b,
640 .a = coverageAlpha(alpha_value, coverage),
641 };
642 }
643
644 fn shadowAlphaAt(paint: Command, x: f32, y: f32) u8 {
645 const blur = @max(paint.width, 0);
646 const distance = roundedRectOutsideDistance(paint.rect, @max(paint.radius, 0), x, y);
647 if (!std.math.isFinite(distance)) return 0;
648 if (blur <= 0) return if (distance <= 0) paint.color.a else 0;
649 if (distance >= blur) return 0;
650 const t = 1 - distance / blur;
651 return alphaScale(paint.color.a, t * t);
652 }
653
654 fn alphaScale(alpha_value: u8, scale: f32) u8 {
655 const value = @as(f32, @floatFromInt(alpha_value)) * std.math.clamp(scale, @as(f32, 0), @as(f32, 1));
656 return @intFromFloat(std.math.clamp(@round(value), @as(f32, 0), @as(f32, 255)));
657 }
658
659 fn imageColorAt(paint: Command, images: ImageSet, x: f32, y: f32, coverage: u32) ?Color {
660 var color = imageSampleAt(paint, images, x, y) orelse return null;
661 color.a = coverageAlpha(@intCast(div255(@as(u32, color.a) * @as(u32, paint.color.a))), coverage);
662 return color;
663 }
664
665 fn glyphColorAt(paint: Command, images: ImageSet, x: f32, y: f32, coverage: u32) ?Color {
666 const sampled = imageSampleAt(paint, images, x, y) orelse return null;
667 return .{
668 .r = paint.color.r,
669 .g = paint.color.g,
670 .b = paint.color.b,
671 .a = coverageAlpha(@intCast(div255(@as(u32, sampled.a) * @as(u32, paint.color.a))), coverage),
672 };
673 }
674
675 fn imageSampleAt(paint: Command, images: ImageSet, x: f32, y: f32) ?Color {
676 const image = imageAt(images, paint.image_index) orelse return null;
677 const source = sourceRect(paint.source, image);
678 return imageSampleKnown(paint, image, source, x, y);
679 }
680
681 fn imageSampleKnown(paint: Command, image: Image, source: Rect, x: f32, y: f32) Color {
682 const u = std.math.clamp((x - paint.rect.x) / paint.rect.width, @as(f32, 0), @as(f32, 0.999999));
683 const v = std.math.clamp((y - paint.rect.y) / paint.rect.height, @as(f32, 0), @as(f32, 0.999999));
684 const sx = sampleCoord(source.x + u * source.width, image.width);
685 const sy = sampleCoord(source.y + v * source.height, image.height);
686 return unpackRgba(image.pixels[@as(usize, sy) * image.width + sx]);
687 }
688
689 fn imageAt(images: ImageSet, index: u32) ?Image {
690 const image_index: usize = index;
691 if (image_index >= images.images.len) return null;
692 const image = images.images[image_index];
693 image.validate() catch return null;
694 return image;
695 }
696
697 fn sourceRect(source: Rect, image: Image) Rect {
698 if (source.width > 0 and source.height > 0) return source;
699 return .{
700 .x = 0,
701 .y = 0,
702 .width = @floatFromInt(image.width),
703 .height = @floatFromInt(image.height),
704 };
705 }
706
707 fn sampleCoord(value: f32, limit: u32) u32 {
708 if (!std.math.isFinite(value) or value <= 0) return 0;
709 const floored = @floor(value);
710 const max_value = @as(f32, @floatFromInt(limit - 1));
711 if (floored >= max_value) return limit - 1;
712 return @intFromFloat(floored);
713 }
714
715 fn unpackRgba(pixel: u32) Color {
716 return .{
717 .r = @truncate(pixel),
718 .g = @truncate(pixel >> 8),
719 .b = @truncate(pixel >> 16),
720 .a = @truncate(pixel >> 24),
721 };
722 }
723
724 fn strokeCovers(rect: Rect, radius: f32, width: f32, x: f32, y: f32) bool {
725 if (width <= 0) return false;
726 if (!insideRounded(rect, radius, x, y)) return false;
727 const inner = inset(rect, width);
728 if (inner.width <= 0 or inner.height <= 0) return true;
729 return !insideRounded(inner, @max(radius - width, 0), x, y);
730 }
731
732 fn shadowCovers(paint: Command, x: f32, y: f32) bool {
733 const blur = @max(paint.width, 0);
734 if (blur <= 0) return insideRounded(paint.rect, @max(paint.radius, 0), x, y);
735 return roundedRectOutsideDistance(paint.rect, @max(paint.radius, 0), x, y) < blur;
736 }
737
738 fn inside(rect: Rect, x: f32, y: f32) bool {
739 return x >= rect.x and y >= rect.y and x < rect.x + rect.width and y < rect.y + rect.height;
740 }
741
742 fn roundedRectOutsideDistance(rect: Rect, radius: f32, x: f32, y: f32) f32 {
743 const clamped = @min(@max(radius, 0), @min(rect.width, rect.height) * 0.5);
744 const inner_x0 = rect.x + clamped;
745 const inner_y0 = rect.y + clamped;
746 const inner_x1 = rect.x + rect.width - clamped;
747 const inner_y1 = rect.y + rect.height - clamped;
748 const cx = @max(inner_x0, @min(x, inner_x1));
749 const cy = @max(inner_y0, @min(y, inner_y1));
750 const dx = x - cx;
751 const dy = y - cy;
752 return @max(@sqrt(dx * dx + dy * dy) - clamped, 0);
753 }
754
755 fn insideRounded(rect: Rect, radius: f32, x: f32, y: f32) bool {
756 if (!inside(rect, x, y)) return false;
757 const clamped = @min(@max(radius, 0), @min(rect.width, rect.height) * 0.5);
758 if (clamped <= 0) return true;
759 const inner_x0 = rect.x + clamped;
760 const inner_y0 = rect.y + clamped;
761 const inner_x1 = rect.x + rect.width - clamped;
762 const inner_y1 = rect.y + rect.height - clamped;
763 const cx = @max(inner_x0, @min(x, inner_x1));
764 const cy = @max(inner_y0, @min(y, inner_y1));
765 const dx = x - cx;
766 const dy = y - cy;
767 return dx * dx + dy * dy <= clamped * clamped;
768 }
769
770 fn inset(rect: Rect, value: f32) Rect {
771 return .{
772 .x = rect.x + value,
773 .y = rect.y + value,
774 .width = @max(rect.width - value * 2, 0),
775 .height = @max(rect.height - value * 2, 0),
776 };
777 }
778
779 fn readPacked(rgba8: []const u8, index: usize) u32 {
780 const base = index * 4;
781 return @as(u32, rgba8[base]) |
782 (@as(u32, rgba8[base + 1]) << 8) |
783 (@as(u32, rgba8[base + 2]) << 16) |
784 (@as(u32, rgba8[base + 3]) << 24);
785 }
786
787 fn writePacked(rgba8: []u8, index: usize, pixel: u32) void {
788 const base = index * 4;
789 rgba8[base] = @truncate(pixel);
790 rgba8[base + 1] = @truncate(pixel >> 8);
791 rgba8[base + 2] = @truncate(pixel >> 16);
792 rgba8[base + 3] = @truncate(pixel >> 24);
793 }
794
795 fn blendPacked(dst: u32, src: Color) u32 {
796 if (src.a == 0) return dst;
797 const inv = 255 - @as(u32, src.a);
798 const dst_r = dst & 0xff;
799 const dst_g = (dst >> 8) & 0xff;
800 const dst_b = (dst >> 16) & 0xff;
801 const dst_a = (dst >> 24) & 0xff;
802 const r = blendChannel(src.r, src.a, dst_r, inv);
803 const g = blendChannel(src.g, src.a, dst_g, inv);
804 const b = blendChannel(src.b, src.a, dst_b, inv);
805 const a = @as(u32, src.a) + div255(dst_a * inv);
806 return r | (g << 8) | (b << 16) | (a << 24);
807 }
808
809 fn blendChannel(src_value: u8, src_alpha: u8, dst_value: u32, inv_alpha: u32) u32 {
810 return div255(@as(u32, src_value) * @as(u32, src_alpha) + dst_value * inv_alpha);
811 }
812
813 fn div255(value: u32) u32 {
814 return (value + 127) / 255;
815 }
816 };