lib/gui/src/paint/cpu/pixel.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const simd = @import("simd");
  3 
  4 const gui = @import("../../root.zig");
  5 
  6 const Color = gui.model.UiColor;
  7 const PackedVector = simd.ScalableTag(u32);
  8 
  9 const span_vector_lanes = PackedVector.lane_count;
 10 const span_vector_threshold = span_vector_lanes * 2;
 11 
 12 pub fn packRgba(color: Color) u32 {
 13     return @as(u32, color.r) |
 14         (@as(u32, color.g) << 8) |
 15         (@as(u32, color.b) << 16) |
 16         (@as(u32, color.a) << 24);
 17 }
 18 
 19 pub fn unpackRgba(pixel: u32) Color {
 20     return .{
 21         .r = @truncate(pixel),
 22         .g = @truncate(pixel >> 8),
 23         .b = @truncate(pixel >> 16),
 24         .a = @truncate(pixel >> 24),
 25     };
 26 }
 27 
 28 pub fn readPacked(rgba8: []const u8, index: usize) u32 {
 29     return std.mem.readInt(u32, rgba8[index * 4 ..][0..4], .little);
 30 }
 31 
 32 pub fn writePacked(rgba8: []u8, index: usize, pixel: u32) void {
 33     std.mem.writeInt(u32, rgba8[index * 4 ..][0..4], pixel, .little);
 34 }
 35 
 36 pub fn blendPacked(dst: u32, src: Color) u32 {
 37     if (src.a == 0) return dst;
 38     const inv = 255 - @as(u32, src.a);
 39     const dst_r = dst & 0xff;
 40     const dst_g = (dst >> 8) & 0xff;
 41     const dst_b = (dst >> 16) & 0xff;
 42     const dst_a = (dst >> 24) & 0xff;
 43     const r = blendChannel(src.r, src.a, dst_r, inv);
 44     const g = blendChannel(src.g, src.a, dst_g, inv);
 45     const b = blendChannel(src.b, src.a, dst_b, inv);
 46     const a = @as(u32, src.a) + div255(dst_a * inv);
 47     return r | (g << 8) | (b << 16) | (a << 24);
 48 }
 49 
 50 pub fn blendSpanScalar(
 51     comptime Store: type,
 52     store: Store,
 53     start: usize,
 54     count: usize,
 55     src: Color,
 56 ) void {
 57     var index = start;
 58     const end = start + count;
 59     while (index < end) : (index += 1) {
 60         store.store(index, blendPacked(store.load(index), src));
 61     }
 62 }
 63 
 64 pub fn blendSpan(
 65     comptime Store: type,
 66     store: Store,
 67     start: usize,
 68     count: usize,
 69     src: Color,
 70 ) void {
 71     if (src.a == 0) return;
 72     var index = start;
 73     const end = start + count;
 74     if (comptime span_vector_lanes > 1) {
 75         if (count >= span_vector_threshold) {
 76             const vector_end = end - count % span_vector_lanes;
 77             while (index < vector_end) : (index += span_vector_lanes) {
 78                 const dst = store.loadVector(PackedVector, index);
 79                 store.storeVector(
 80                     PackedVector,
 81                     index,
 82                     blendPackedVector(PackedVector, dst, src),
 83                 );
 84             }
 85         }
 86     }
 87     blendSpanScalar(Store, store, index, end - index, src);
 88 }
 89 
 90 pub fn blendChannel(src_value: u8, src_alpha: u8, dst_value: u32, inv_alpha: u32) u32 {
 91     return div255(@as(u32, src_value) * @as(u32, src_alpha) + dst_value * inv_alpha);
 92 }
 93 
 94 pub fn div255(value: u32) u32 {
 95     return (value + 127) / 255;
 96 }
 97 
 98 fn blendPackedVector(
 99     comptime D: type,
100     dst: D.Vector,
101     src: Color,
102 ) D.Vector {
103     const byte_mask = simd.set(D, 0xff);
104     const alpha = @as(u32, src.a);
105     const inv = simd.set(D, 255 - alpha);
106     const dst_r = simd.bitAnd(D, dst, byte_mask);
107     const dst_g = simd.bitAnd(D, shiftRight(D, dst, 8), byte_mask);
108     const dst_b = simd.bitAnd(D, shiftRight(D, dst, 16), byte_mask);
109     const dst_a = shiftRight(D, dst, 24);
110     const r = blendChannelVector(D, dst_r, src.r, alpha, inv);
111     const g = blendChannelVector(D, dst_g, src.g, alpha, inv);
112     const b = blendChannelVector(D, dst_b, src.b, alpha, inv);
113     const a = simd.add(
114         D,
115         simd.set(D, alpha),
116         div255Vector(D, simd.mul(D, dst_a, inv)),
117     );
118     return simd.bitOr(
119         D,
120         simd.or3(
121             D,
122             r,
123             shiftLeft(D, g, 8),
124             shiftLeft(D, b, 16),
125         ),
126         shiftLeft(D, a, 24),
127     );
128 }
129 
130 fn blendChannelVector(
131     comptime D: type,
132     dst: D.Vector,
133     src: u8,
134     alpha: u32,
135     inv: D.Vector,
136 ) D.Vector {
137     return div255Vector(
138         D,
139         simd.add(
140             D,
141             simd.set(D, @as(u32, src) * alpha),
142             simd.mul(D, dst, inv),
143         ),
144     );
145 }
146 
147 fn div255Vector(comptime D: type, value: D.Vector) D.Vector {
148     const rounded = simd.add(D, value, simd.set(D, 128));
149     return shiftRight(D, simd.add(D, rounded, shiftRight(D, rounded, 8)), 8);
150 }
151 
152 fn shiftLeft(comptime D: type, value: D.Vector, comptime amount: u5) D.Vector {
153     const Shift = @Vector(D.lane_count, u5);
154     return value << @as(Shift, @splat(amount));
155 }
156 
157 fn shiftRight(comptime D: type, value: D.Vector, comptime amount: u5) D.Vector {
158     const Shift = @Vector(D.lane_count, u5);
159     return value >> @as(Shift, @splat(amount));
160 }
161 
162 pub fn coverageAlpha(alpha: u8, coverage: u32) u8 {
163     return @intCast((@as(u32, alpha) * coverage + 2) / 4);
164 }
165 
166 pub fn colorWithCoverage(color: Color, coverage: u32) Color {
167     return .{
168         .r = color.r,
169         .g = color.g,
170         .b = color.b,
171         .a = coverageAlpha(color.a, coverage),
172     };
173 }
174 
175 pub const ByteStore = struct {
176     bytes: []u8,
177 
178     pub inline fn load(self: ByteStore, index: usize) u32 {
179         return readPacked(self.bytes, index);
180     }
181 
182     pub inline fn store(self: ByteStore, index: usize, value: u32) void {
183         writePacked(self.bytes, index, value);
184     }
185 
186     pub inline fn fill(self: ByteStore, start: usize, count: usize, value: u32) void {
187         var index = start;
188         const end = start + count;
189         while (index < end) : (index += 1) writePacked(self.bytes, index, value);
190     }
191 
192     pub inline fn loadVector(self: ByteStore, comptime D: type, index: usize) D.Vector {
193         var result: D.Vector = undefined;
194         inline for (0..D.lane_count) |lane_index| {
195             result[lane_index] = readPacked(self.bytes, index + lane_index);
196         }
197         return result;
198     }
199 
200     pub inline fn storeVector(
201         self: ByteStore,
202         comptime D: type,
203         index: usize,
204         value: D.Vector,
205     ) void {
206         inline for (0..D.lane_count) |lane_index| {
207             writePacked(self.bytes, index + lane_index, value[lane_index]);
208         }
209     }
210 };
211 
212 pub const WordStore = struct {
213     words: []u32,
214 
215     pub inline fn load(self: WordStore, index: usize) u32 {
216         return self.words[index];
217     }
218 
219     pub inline fn store(self: WordStore, index: usize, value: u32) void {
220         self.words[index] = value;
221     }
222 
223     pub inline fn fill(self: WordStore, start: usize, count: usize, value: u32) void {
224         @memset(self.words[start .. start + count], value);
225     }
226 
227     pub inline fn loadVector(self: WordStore, comptime D: type, index: usize) D.Vector {
228         return simd.load(D, self.words[index..]);
229     }
230 
231     pub inline fn storeVector(
232         self: WordStore,
233         comptime D: type,
234         index: usize,
235         value: D.Vector,
236     ) void {
237         simd.store(D, value, self.words[index..]);
238     }
239 };
240 
241 test "packed byte layout round-trips through both stores" {
242     const color = Color{ .r = 12, .g = 34, .b = 56, .a = 78 };
243     const pixel = packRgba(color);
244 
245     var bytes = @as([8]u8, @splat(0));
246     const byte_store = ByteStore{ .bytes = bytes[0..] };
247     byte_store.store(1, pixel);
248     try std.testing.expectEqual(pixel, byte_store.load(1));
249     try std.testing.expectEqual(@as(u8, 12), bytes[4]);
250     try std.testing.expectEqual(@as(u8, 78), bytes[7]);
251 
252     var words = @as([2]u32, @splat(0));
253     const word_store = WordStore{ .words = words[0..] };
254     word_store.store(1, pixel);
255     try std.testing.expectEqual(pixel, word_store.load(1));
256     try std.testing.expectEqual(color, unpackRgba(word_store.load(1)));
257 }
258 
259 test "coverageAlpha at full coverage preserves alpha exactly" {
260     var alpha: u32 = 0;
261     while (alpha <= 255) : (alpha += 1) {
262         try std.testing.expectEqual(@as(u8, @intCast(alpha)), coverageAlpha(@intCast(alpha), 4));
263     }
264 }
265 
266 test "blendPacked with opaque source replaces destination" {
267     const src = Color{ .r = 10, .g = 20, .b = 30, .a = 255 };
268     try std.testing.expectEqual(packRgba(src), blendPacked(0xdead_beef, src));
269 }
270 
271 test "vector div255 is bit-exact over packed blend range" {
272     var value: u32 = 0;
273     while (value <= 65_025) : (value += 1) {
274         const vector = div255Vector(PackedVector, simd.set(PackedVector, value));
275         const lanes: [PackedVector.lane_count]u32 = vector;
276         try std.testing.expectEqual(div255(value), lanes[0]);
277     }
278 }
279 
280 test "selected packed spans match scalar words and unaligned bytes" {
281     const pixel_count = span_vector_lanes * 3 + 5;
282     var initial: [pixel_count]u32 = undefined;
283     var expected: [pixel_count]u32 = undefined;
284     var words: [pixel_count]u32 = undefined;
285     var byte_storage: [pixel_count * 4 + 1]u8 = undefined;
286     const bytes = byte_storage[1..];
287     for (&initial, 0..) |*value, index| {
288         value.* = @as(u32, @truncate(index *% 0x9e37_79b9)) ^ 0xa5c3_71e9;
289     }
290     var alpha: u16 = 0;
291     while (alpha <= 255) : (alpha += 1) {
292         const src = Color{
293             .r = @truncate(alpha *% 29 + 3),
294             .g = @truncate(alpha *% 73 + 5),
295             .b = @truncate(alpha *% 151 + 7),
296             .a = @intCast(alpha),
297         };
298         var offset: usize = 0;
299         while (offset < span_vector_lanes + 1) : (offset += 1) {
300             var count: usize = 0;
301             while (count <= pixel_count - offset) : (count += 1) {
302                 @memcpy(&expected, &initial);
303                 @memcpy(&words, &initial);
304                 for (initial, 0..) |value, index| writePacked(bytes, index, value);
305                 blendSpanScalar(
306                     WordStore,
307                     .{ .words = &expected },
308                     offset,
309                     count,
310                     src,
311                 );
312                 blendSpan(WordStore, .{ .words = &words }, offset, count, src);
313                 blendSpan(ByteStore, .{ .bytes = bytes }, offset, count, src);
314                 try std.testing.expectEqualSlices(u32, &expected, &words);
315                 for (expected, 0..) |value, index| {
316                     try std.testing.expectEqual(value, readPacked(bytes, index));
317                 }
318             }
319         }
320     }
321 }