lib/gui/src/paint/cpu/span.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const gui = @import("../../root.zig");
2 const paint_root = @import("../root.zig");
3
4 const coverage = @import("coverage.zig");
5 const geometry = @import("geometry.zig");
6 const gradient = paint_root.gradient;
7 const pixel = @import("pixel.zig");
8 const sample = @import("sample.zig");
9
10 const Bounds = geometry.Bounds;
11 const Color = gui.model.UiColor;
12 const Command = paint_root.command.Command;
13 const ImageSet = paint_root.command.ImageSet;
14 const Kind = paint_root.command.Kind;
15 const Region = geometry.Region;
16
17 pub fn renderRegion(comptime Store: type, store: Store, width: u32, height: u32, commands: []const Command, clear: Color, images: ImageSet, region: Region) void {
18 const clear_pixel = pixel.packRgba(clear);
19 var y = region.y;
20 while (y < region.y + region.height) : (y += 1) {
21 store.fill(@as(usize, y) * width + region.x, region.width, clear_pixel);
22 }
23 for (commands) |paint| {
24 rasterCommand(Store, store, width, height, paint, images, region);
25 }
26 }
27
28 pub fn rasterCommand(comptime Store: type, store: Store, width: u32, height: u32, paint: Command, images: ImageSet, region: Region) void {
29 const bounds = geometry.clippedBounds(width, height, paint, region) orelse return;
30 if (!geometry.finiteRects(paint.rect, paint.clip)) {
31 coverage.rasterBlock(Store, store, width, paint, images, bounds.y0, bounds.y1, bounds.x0, bounds.x1);
32 return;
33 }
34 switch (paint.kind) {
35 .fill => rasterFill(Store, store, width, paint, images, bounds),
36 .linear_gradient => rasterLinearGradient(Store, store, width, paint, images, bounds),
37 .stroke => rasterStroke(Store, store, width, height, paint, images, bounds),
38 .image => rasterTextured(.image, Store, store, width, paint, images, bounds),
39 .glyph => rasterTextured(.glyph, Store, store, width, paint, images, bounds),
40 .shadow => rasterShadow(Store, store, width, paint, images, bounds),
41 }
42 }
43
44 const Range = struct {
45 lo: u32,
46 hi: u32,
47
48 fn width(self: Range) usize {
49 return self.hi - self.lo;
50 }
51 };
52
53 const AxisBand = struct {
54 lo: f32,
55 hi: f32,
56 closed: bool = false,
57 };
58
59 const AxisBands = struct {
60 bands: [3]AxisBand = undefined,
61 count: usize = 0,
62
63 fn add(self: *AxisBands, band: AxisBand) void {
64 self.bands[self.count] = band;
65 self.count += 1;
66 }
67
68 fn includes(self: *const AxisBands, index: u32) bool {
69 const center = @as(f32, @floatFromInt(index)) + 0.5;
70 const low = center - 0.25;
71 const high = center + 0.25;
72 for (self.bands[0..self.count]) |band| {
73 if (!(low >= band.lo)) return false;
74 if (band.closed) {
75 if (!(high <= band.hi)) return false;
76 } else {
77 if (!(high < band.hi)) return false;
78 }
79 }
80 return true;
81 }
82 };
83
84 fn interiorRange(b0: u32, b1: u32, bands: *const AxisBands) Range {
85 var lo = b0;
86 while (lo < b1 and !bands.includes(lo)) lo += 1;
87 var hi = b1;
88 while (hi > lo and !bands.includes(hi - 1)) hi -= 1;
89 return .{ .lo = lo, .hi = hi };
90 }
91
92 fn edgeBlocks(comptime Store: type, store: Store, width: u32, paint: Command, images: ImageSet, bounds: Bounds, rows: Range, cols: Range) void {
93 coverage.rasterBlock(Store, store, width, paint, images, bounds.y0, rows.lo, bounds.x0, bounds.x1);
94 coverage.rasterBlock(Store, store, width, paint, images, rows.hi, bounds.y1, bounds.x0, bounds.x1);
95 if (rows.lo < rows.hi) {
96 coverage.rasterBlock(Store, store, width, paint, images, rows.lo, rows.hi, bounds.x0, cols.lo);
97 coverage.rasterBlock(Store, store, width, paint, images, rows.lo, rows.hi, cols.hi, bounds.x1);
98 }
99 }
100
101 fn fillRows(comptime Store: type, store: Store, width: u32, color: Color, rows: Range, cols: Range) void {
102 const count = cols.width();
103 if (color.a == 255) {
104 const value = pixel.packRgba(color);
105 var y = rows.lo;
106 while (y < rows.hi) : (y += 1) {
107 store.fill(@as(usize, y) * width + cols.lo, count, value);
108 }
109 return;
110 }
111 var y = rows.lo;
112 while (y < rows.hi) : (y += 1) {
113 const index = @as(usize, y) * width + cols.lo;
114 pixel.blendSpan(Store, store, index, count, color);
115 }
116 }
117
118 fn rasterFill(comptime Store: type, store: Store, width: u32, paint: Command, images: ImageSet, bounds: Bounds) void {
119 var col_bands = AxisBands{};
120 col_bands.add(.{
121 .lo = @max(paint.rect.x, paint.clip.x),
122 .hi = @min(paint.rect.x + paint.rect.width, paint.clip.x + paint.clip.width),
123 });
124 var row_bands = AxisBands{};
125 row_bands.add(.{
126 .lo = @max(paint.rect.y, paint.clip.y),
127 .hi = @min(paint.rect.y + paint.rect.height, paint.clip.y + paint.clip.height),
128 });
129 const clamped = geometry.clampedRadius(paint.rect, @max(paint.radius, 0));
130 if (clamped > 0) {
131 col_bands.add(.{
132 .lo = paint.rect.x + clamped,
133 .hi = paint.rect.x + paint.rect.width - clamped,
134 .closed = true,
135 });
136 row_bands.add(.{
137 .lo = paint.rect.y + clamped,
138 .hi = paint.rect.y + paint.rect.height - clamped,
139 .closed = true,
140 });
141 }
142 const rows = interiorRange(bounds.y0, bounds.y1, &row_bands);
143 const cols = interiorRange(bounds.x0, bounds.x1, &col_bands);
144 edgeBlocks(Store, store, width, paint, images, bounds, rows, cols);
145 if (rows.lo < rows.hi and cols.lo < cols.hi) {
146 fillRows(Store, store, width, paint.color, rows, cols);
147 }
148 }
149
150 fn rasterLinearGradient(comptime Store: type, store: Store, width: u32, paint: Command, images: ImageSet, bounds: Bounds) void {
151 var col_bands = AxisBands{};
152 col_bands.add(.{
153 .lo = @max(paint.rect.x, paint.clip.x),
154 .hi = @min(paint.rect.x + paint.rect.width, paint.clip.x + paint.clip.width),
155 });
156 var row_bands = AxisBands{};
157 row_bands.add(.{
158 .lo = @max(paint.rect.y, paint.clip.y),
159 .hi = @min(paint.rect.y + paint.rect.height, paint.clip.y + paint.clip.height),
160 });
161 const clamped = geometry.clampedRadius(paint.rect, @max(paint.radius, 0));
162 if (clamped > 0) {
163 col_bands.add(.{
164 .lo = paint.rect.x + clamped,
165 .hi = paint.rect.x + paint.rect.width - clamped,
166 .closed = true,
167 });
168 row_bands.add(.{
169 .lo = paint.rect.y + clamped,
170 .hi = paint.rect.y + paint.rect.height - clamped,
171 .closed = true,
172 });
173 }
174 const rows = interiorRange(bounds.y0, bounds.y1, &row_bands);
175 const cols = interiorRange(bounds.x0, bounds.x1, &col_bands);
176 edgeBlocks(Store, store, width, paint, images, bounds, rows, cols);
177 if (rows.lo < rows.hi and cols.lo < cols.hi) {
178 gradientRows(Store, store, width, paint, rows, cols);
179 }
180 }
181
182 fn gradientRows(comptime Store: type, store: Store, width: u32, paint: Command, rows: Range, cols: Range) void {
183 const sampler = gradient.Sampler.init(paint);
184 var y = rows.lo;
185 while (y < rows.hi) : (y += 1) {
186 const fy = @as(f32, @floatFromInt(y)) + 0.5;
187 const row_projection = sampler.rowProjection(fy);
188 const target_row = @as(usize, y) * width;
189 var x = cols.lo;
190 while (x < cols.hi) : (x += 1) {
191 const fx = @as(f32, @floatFromInt(x)) + 0.5;
192 const src = sampler.colorAtRow(fx, row_projection);
193 if (src.a == 0) continue;
194 const index = target_row + x;
195 if (src.a == 255) {
196 store.store(index, pixel.packRgba(src));
197 } else {
198 store.store(index, pixel.blendPacked(store.load(index), src));
199 }
200 }
201 }
202 }
203
204 fn rasterShadow(comptime Store: type, store: Store, width: u32, paint: Command, images: ImageSet, bounds: Bounds) void {
205 const blur = @max(paint.width, 0);
206 const clamped = geometry.clampedRadius(paint.rect, @max(paint.radius, 0));
207 var col_bands = AxisBands{};
208 col_bands.add(.{ .lo = paint.clip.x, .hi = paint.clip.x + paint.clip.width });
209 col_bands.add(.{
210 .lo = paint.rect.x + clamped,
211 .hi = paint.rect.x + paint.rect.width - clamped,
212 .closed = true,
213 });
214 var row_bands = AxisBands{};
215 row_bands.add(.{ .lo = paint.clip.y, .hi = paint.clip.y + paint.clip.height });
216 row_bands.add(.{
217 .lo = paint.rect.y + clamped,
218 .hi = paint.rect.y + paint.rect.height - clamped,
219 .closed = true,
220 });
221 if (blur <= 0) {
222 col_bands.add(.{ .lo = paint.rect.x, .hi = paint.rect.x + paint.rect.width });
223 row_bands.add(.{ .lo = paint.rect.y, .hi = paint.rect.y + paint.rect.height });
224 }
225 const rows = interiorRange(bounds.y0, bounds.y1, &row_bands);
226 const cols = interiorRange(bounds.x0, bounds.x1, &col_bands);
227 edgeBlocks(Store, store, width, paint, images, bounds, rows, cols);
228 if (rows.lo < rows.hi and cols.lo < cols.hi) {
229 fillRows(Store, store, width, paint.color, rows, cols);
230 }
231 }
232
233 fn rasterStroke(comptime Store: type, store: Store, width: u32, height: u32, paint: Command, images: ImageSet, bounds: Bounds) void {
234 const stroke_width = @max(paint.width, 0);
235 if (stroke_width <= 0) return;
236 const inner = geometry.inset(paint.rect, stroke_width);
237 if (inner.width <= 0 or inner.height <= 0) {
238 coverage.rasterBlock(Store, store, width, paint, images, bounds.y0, bounds.y1, bounds.x0, bounds.x1);
239 return;
240 }
241 const clamped = geometry.clampedRadius(paint.rect, @max(paint.radius, 0));
242 const band = @max(stroke_width, clamped) + 1;
243 const top_end = @min(bounds.y1, @max(bounds.y0, geometry.ceilClamp(paint.rect.y + band, height)));
244 const bottom_start = @max(top_end, @min(bounds.y1, geometry.floorClamp(paint.rect.y + paint.rect.height - band, height)));
245 coverage.rasterBlock(Store, store, width, paint, images, bounds.y0, top_end, bounds.x0, bounds.x1);
246 coverage.rasterBlock(Store, store, width, paint, images, bottom_start, bounds.y1, bounds.x0, bounds.x1);
247 if (top_end < bottom_start) {
248 const left_end = @min(bounds.x1, @max(bounds.x0, geometry.ceilClamp(paint.rect.x + band, width)));
249 const right_start = @max(left_end, @min(bounds.x1, geometry.floorClamp(paint.rect.x + paint.rect.width - band, width)));
250 coverage.rasterBlock(Store, store, width, paint, images, top_end, bottom_start, bounds.x0, left_end);
251 coverage.rasterBlock(Store, store, width, paint, images, top_end, bottom_start, right_start, bounds.x1);
252 }
253 }
254
255 const span_column_limit = 512;
256
257 fn rasterTextured(comptime kind: Kind, comptime Store: type, store: Store, width: u32, paint: Command, images: ImageSet, bounds: Bounds) void {
258 const image = sample.imageAt(images, paint.image_index) orelse return;
259 const source = sample.sourceRect(paint.source, image);
260 var col_bands = AxisBands{};
261 col_bands.add(.{
262 .lo = @max(paint.rect.x, paint.clip.x),
263 .hi = @min(paint.rect.x + paint.rect.width, paint.clip.x + paint.clip.width),
264 });
265 var row_bands = AxisBands{};
266 row_bands.add(.{
267 .lo = @max(paint.rect.y, paint.clip.y),
268 .hi = @min(paint.rect.y + paint.rect.height, paint.clip.y + paint.clip.height),
269 });
270 const rows = interiorRange(bounds.y0, bounds.y1, &row_bands);
271 const cols = interiorRange(bounds.x0, bounds.x1, &col_bands);
272 texturedEdges(kind, Store, store, width, paint, image, source, bounds, rows, cols);
273 if (rows.lo < rows.hi and cols.lo < cols.hi) {
274 texturedRows(kind, Store, store, width, paint, image, source, rows, cols);
275 }
276 }
277
278 fn texturedEdges(comptime kind: Kind, comptime Store: type, store: Store, width: u32, paint: Command, image: paint_root.command.Image, source: gui.layout.Rect, bounds: Bounds, rows: Range, cols: Range) void {
279 coverage.texturedBlock(kind, Store, store, width, paint, image, source, bounds.y0, rows.lo, bounds.x0, bounds.x1);
280 coverage.texturedBlock(kind, Store, store, width, paint, image, source, rows.hi, bounds.y1, bounds.x0, bounds.x1);
281 if (rows.lo < rows.hi) {
282 coverage.texturedBlock(kind, Store, store, width, paint, image, source, rows.lo, rows.hi, bounds.x0, cols.lo);
283 coverage.texturedBlock(kind, Store, store, width, paint, image, source, rows.lo, rows.hi, cols.hi, bounds.x1);
284 }
285 }
286
287 fn texturedRows(comptime kind: Kind, comptime Store: type, store: Store, width: u32, paint: Command, image: paint_root.command.Image, source: gui.layout.Rect, rows: Range, cols: Range) void {
288 var columns: [span_column_limit]u32 = undefined;
289 const opaque_pixel = pixel.packRgba(.{ .r = paint.color.r, .g = paint.color.g, .b = paint.color.b, .a = 255 });
290 var chunk = cols.lo;
291 while (chunk < cols.hi) {
292 const chunk_end = @min(cols.hi, chunk + span_column_limit);
293 var x = chunk;
294 while (x < chunk_end) : (x += 1) {
295 const fx = @as(f32, @floatFromInt(x)) + 0.5;
296 columns[x - chunk] = sample.sampleColumn(paint, source, image, fx);
297 }
298 var y = rows.lo;
299 while (y < rows.hi) : (y += 1) {
300 const fy = @as(f32, @floatFromInt(y)) + 0.5;
301 const sy = sample.sampleRow(paint, source, image, fy);
302 const image_row = @as(usize, sy) * image.width;
303 const target_row = @as(usize, y) * width;
304 x = chunk;
305 while (x < chunk_end) : (x += 1) {
306 const sampled = image.pixels[image_row + columns[x - chunk]];
307 const sampled_alpha: u32 = sampled >> 24;
308 const alpha = pixel.div255(sampled_alpha * @as(u32, paint.color.a));
309 if (alpha == 0) continue;
310 const index = target_row + x;
311 switch (kind) {
312 .glyph => {
313 if (alpha == 255) {
314 store.store(index, opaque_pixel);
315 } else {
316 store.store(index, pixel.blendPacked(store.load(index), .{
317 .r = paint.color.r,
318 .g = paint.color.g,
319 .b = paint.color.b,
320 .a = @intCast(alpha),
321 }));
322 }
323 },
324 .image => {
325 const src = Color{
326 .r = @truncate(sampled),
327 .g = @truncate(sampled >> 8),
328 .b = @truncate(sampled >> 16),
329 .a = @intCast(alpha),
330 };
331 if (alpha == 255) {
332 store.store(index, pixel.packRgba(src));
333 } else {
334 store.store(index, pixel.blendPacked(store.load(index), src));
335 }
336 },
337 else => comptime unreachable,
338 }
339 }
340 }
341 chunk = chunk_end;
342 }
343 }