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 };