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 }