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 }