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 }