lib/gui/src/paint/strip.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 
  3 const paint_root = @import("root.zig");
  4 
  5 const Allocator = std.mem.Allocator;
  6 const Command = paint_root.command.Command;
  7 const Region = paint_root.cpu.Region;
  8 
  9 pub const default_height: u32 = 16;
 10 pub const word_lanes: usize = 7;
 11 
 12 pub const Options = struct {
 13     height: u32 = default_height,
 14 };
 15 
 16 pub const Lane = enum(usize) {
 17     command_index = 0,
 18     order = 1,
 19     strip = 2,
 20     x0 = 3,
 21     y0 = 4,
 22     x1 = 5,
 23     y1 = 6,
 24 };
 25 
 26 pub const Record = struct {
 27     command_index: u32,
 28     order: u32,
 29     strip: u32,
 30     x0: u32,
 31     y0: u32,
 32     x1: u32,
 33     y1: u32,
 34 };
 35 
 36 pub const Records = struct {
 37     words: []u32,
 38     count: usize,
 39 
 40     pub fn lane(self: Records, which: Lane) []const u32 {
 41         const start = @backingInt(which) * self.count;
 42         return self.words[start .. start + self.count];
 43     }
 44 
 45     pub fn laneMut(self: *Records, which: Lane) []u32 {
 46         const start = @backingInt(which) * self.count;
 47         return self.words[start .. start + self.count];
 48     }
 49 
 50     pub fn at(self: Records, index: usize) Record {
 51         return .{
 52             .command_index = self.lane(.command_index)[index],
 53             .order = self.lane(.order)[index],
 54             .strip = self.lane(.strip)[index],
 55             .x0 = self.lane(.x0)[index],
 56             .y0 = self.lane(.y0)[index],
 57             .x1 = self.lane(.x1)[index],
 58             .y1 = self.lane(.y1)[index],
 59         };
 60     }
 61 
 62     pub fn deinit(self: *Records, allocator: Allocator) void {
 63         allocator.free(self.words);
 64         self.* = undefined;
 65     }
 66 };
 67 
 68 const StripRange = struct {
 69     first: u32,
 70     last: u32,
 71 
 72     fn count(self: StripRange) usize {
 73         return self.last - self.first + 1;
 74     }
 75 };
 76 
 77 pub fn recordCount(commands: []const Command, width: u32, height: u32, region: Region, options: Options) !usize {
 78     if (options.height == 0) return error.InvalidStripHeight;
 79     const active_region = region.clamped(width, height);
 80     if (active_region.pixelCount() == 0) return 0;
 81 
 82     var total: usize = 0;
 83     for (commands) |paint| {
 84         const bounds = paint_root.cpu.clippedBounds(width, height, paint, active_region) orelse continue;
 85         total = std.math.add(usize, total, stripRange(bounds.y0, bounds.y1, options).count()) catch return error.RecordCountTooLarge;
 86     }
 87     return total;
 88 }
 89 
 90 pub fn reifyAlloc(allocator: Allocator, commands: []const Command, width: u32, height: u32, region: Region, options: Options) !Records {
 91     const count = try recordCount(commands, width, height, region, options);
 92     const word_count = std.math.mul(usize, count, word_lanes) catch return error.RecordCountTooLarge;
 93     const words = try allocator.alloc(u32, word_count);
 94     errdefer allocator.free(words);
 95     var records = Records{ .words = words, .count = count };
 96     try writeRecords(&records, commands, width, height, region, options);
 97     return records;
 98 }
 99 
100 fn writeRecords(records: *Records, commands: []const Command, width: u32, height: u32, region: Region, options: Options) !void {
101     if (options.height == 0) return error.InvalidStripHeight;
102     const active_region = region.clamped(width, height);
103     if (active_region.pixelCount() == 0) return;
104 
105     var cursor: usize = 0;
106     for (commands, 0..) |paint, command_index| {
107         const command_index_u32 = std.math.cast(u32, command_index) orelse return error.CommandCountTooLarge;
108         const bounds = paint_root.cpu.clippedBounds(width, height, paint, active_region) orelse continue;
109         const range = stripRange(bounds.y0, bounds.y1, options);
110         var strip = range.first;
111         while (strip <= range.last) : (strip += 1) {
112             const y0 = @max(bounds.y0, stripStart(strip, options.height));
113             const y1 = @min(bounds.y1, stripEnd(strip, options.height));
114             writeRecord(records, cursor, .{
115                 .command_index = command_index_u32,
116                 .order = paint.order,
117                 .strip = strip,
118                 .x0 = bounds.x0,
119                 .y0 = y0,
120                 .x1 = bounds.x1,
121                 .y1 = y1,
122             });
123             cursor += 1;
124         }
125     }
126     std.debug.assert(cursor == records.count);
127 }
128 
129 fn writeRecord(records: *Records, index: usize, record: Record) void {
130     records.laneMut(.command_index)[index] = record.command_index;
131     records.laneMut(.order)[index] = record.order;
132     records.laneMut(.strip)[index] = record.strip;
133     records.laneMut(.x0)[index] = record.x0;
134     records.laneMut(.y0)[index] = record.y0;
135     records.laneMut(.x1)[index] = record.x1;
136     records.laneMut(.y1)[index] = record.y1;
137 }
138 
139 fn stripRange(y0: u32, y1: u32, options: Options) StripRange {
140     std.debug.assert(y0 < y1);
141     return .{
142         .first = y0 / options.height,
143         .last = (y1 - 1) / options.height,
144     };
145 }
146 
147 fn stripStart(strip: u32, height: u32) u32 {
148     return @intCast(@as(u64, strip) * height);
149 }
150 
151 fn stripEnd(strip: u32, height: u32) u32 {
152     return @intCast(@min(@as(u64, std.math.maxInt(u32)), (@as(u64, strip) + 1) * height));
153 }
154 
155 test "reifyAlloc emits globally aligned strip records" {
156     const allocator = std.testing.allocator;
157     const commands = [_]Command{
158         .{
159             .kind = .fill,
160             .rect = .{ .x = 2, .y = 0, .width = 12, .height = 40 },
161             .clip = .{ .x = 0, .y = 0, .width = 64, .height = 64 },
162             .color = .{ .r = 20, .g = 30, .b = 40, .a = 255 },
163             .order = 3,
164         },
165         .{
166             .kind = .fill,
167             .rect = .{ .x = 8, .y = 10, .width = 18, .height = 10 },
168             .clip = .{ .x = 0, .y = 0, .width = 64, .height = 64 },
169             .color = .{ .r = 80, .g = 90, .b = 100, .a = 255 },
170             .order = 7,
171         },
172     };
173 
174     var records = try reifyAlloc(allocator, commands[0..], 64, 64, Region.full(64, 64), .{ .height = 16 });
175     defer records.deinit(allocator);
176 
177     try std.testing.expectEqual(@as(usize, 5), records.count);
178     try std.testing.expectEqualSlices(u32, &.{ 0, 0, 0, 1, 1 }, records.lane(.command_index));
179     try std.testing.expectEqualSlices(u32, &.{ 3, 3, 3, 7, 7 }, records.lane(.order));
180     try std.testing.expectEqualSlices(u32, &.{ 0, 1, 2, 0, 1 }, records.lane(.strip));
181     try std.testing.expectEqualSlices(u32, &.{ 0, 16, 32, 10, 16 }, records.lane(.y0));
182     try std.testing.expectEqualSlices(u32, &.{ 16, 32, 40, 16, 20 }, records.lane(.y1));
183 }
184 
185 test "recordCount clips to the active region" {
186     const commands = [_]Command{.{
187         .kind = .fill,
188         .rect = .{ .x = 0, .y = 0, .width = 32, .height = 32 },
189         .clip = .{ .x = 0, .y = 0, .width = 32, .height = 32 },
190         .color = .{ .r = 20, .g = 30, .b = 40, .a = 255 },
191     }};
192 
193     const count = try recordCount(commands[0..], 32, 32, .{ .x = 0, .y = 8, .width = 32, .height = 8 }, .{ .height = 16 });
194     try std.testing.expectEqual(@as(usize, 1), count);
195 }