lib/simd/src/image/root.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const simd = @import("../root.zig");
3 const tag = simd.tag;
4 const highway = simd.aligned;
5
6 pub const vector_size: usize = tag.ScalableTag(u8).byte_count;
7 pub const storage_alignment: usize = @max(highway.alignment, vector_size);
8
9 pub const GeometryError = error{
10 AliasedPlanes,
11 AllocationSizeOverflow,
12 BufferTooSmall,
13 DimensionTooLarge,
14 InvalidComponentSize,
15 InsufficientRowPadding,
16 InvalidShrink,
17 InvalidStride,
18 InvalidVectorSize,
19 MisalignedStorage,
20 PlaneSizeMismatch,
21 PlaneStrideMismatch,
22 RowSizeOverflow,
23 };
24 pub const Error = std.mem.Allocator.Error || GeometryError;
25
26 const Padding = enum {
27 round_up,
28 unaligned,
29 };
30
31 const Packing = enum {
32 none,
33 owned,
34 };
35
36 pub fn vectorSize() usize {
37 return vector_size;
38 }
39
40 pub fn bytesPerRow(xsize: usize, component_size: usize) GeometryError!usize {
41 return bytesPerRowFor(xsize, component_size, vector_size);
42 }
43
44 pub fn bytesPerRowFor(
45 xsize: usize,
46 component_size: usize,
47 selected_vector_size: usize,
48 ) GeometryError!usize {
49 try validateGeometry(component_size, selected_vector_size);
50 var valid_bytes = std.math.mul(usize, xsize, component_size) catch
51 return error.RowSizeOverflow;
52 if (selected_vector_size != 1) {
53 valid_bytes = std.math.add(
54 usize,
55 valid_bytes,
56 selected_vector_size - component_size,
57 ) catch return error.RowSizeOverflow;
58 }
59 const alignment = @max(highway.alignment, selected_vector_size);
60 var stride = try roundUp(valid_bytes, alignment);
61 if (stride % highway.alignment == 0) {
62 stride = std.math.add(usize, stride, alignment) catch
63 return error.RowSizeOverflow;
64 }
65 std.debug.assert(stride % alignment == 0);
66 return stride;
67 }
68
69 pub fn Image(comptime T: type) type {
70 validateComponent(T);
71 return struct {
72 xsize_value: u32,
73 ysize_value: u32,
74 bytes_per_row_value: usize,
75 storage: []u8,
76 owned: bool,
77
78 const Self = @This();
79
80 pub fn empty() Self {
81 return zeroDimensions(0, 0);
82 }
83
84 fn zeroDimensions(width: usize, height: usize) Self {
85 return .{
86 .xsize_value = @intCast(width),
87 .ysize_value = @intCast(height),
88 .bytes_per_row_value = 0,
89 .storage = &.{},
90 .owned = false,
91 };
92 }
93
94 pub fn init(allocator: std.mem.Allocator, width: usize, height: usize) Error!Self {
95 try validateDimensions(width, height);
96 if (width == 0 or height == 0) return zeroDimensions(width, height);
97 const stride = try bytesPerRowFor(width, @sizeOf(T), vector_size);
98 const allocation_size = std.math.mul(usize, stride, height) catch
99 return error.AllocationSizeOverflow;
100 const storage = try allocator.alignedAlloc(
101 u8,
102 .fromByteUnits(storage_alignment),
103 allocation_size,
104 );
105 var result = Self{
106 .xsize_value = @intCast(width),
107 .ysize_value = @intCast(height),
108 .bytes_per_row_value = stride,
109 .storage = storage,
110 .owned = true,
111 };
112 result.initializePadding(.round_up) catch |err| {
113 allocator.free(storage);
114 return err;
115 };
116 return result;
117 }
118
119 pub fn initBorrowed(
120 width: usize,
121 height: usize,
122 stride: usize,
123 storage: []u8,
124 ) GeometryError!Self {
125 try validateDimensions(width, height);
126 if (width == 0 or height == 0) {
127 if (stride != 0) return error.InvalidStride;
128 return zeroDimensions(width, height);
129 }
130 if (stride % vector_size != 0 or stride % @alignOf(T) != 0) {
131 return error.InvalidStride;
132 }
133 const valid_bytes = std.math.mul(usize, width, @sizeOf(T)) catch
134 return error.RowSizeOverflow;
135 if (stride < valid_bytes) return error.InvalidStride;
136 const required = std.math.mul(usize, stride, height) catch
137 return error.AllocationSizeOverflow;
138 if (storage.len < required) return error.BufferTooSmall;
139 if (@intFromPtr(storage.ptr) % @max(vector_size, @alignOf(T)) != 0) {
140 return error.MisalignedStorage;
141 }
142 return .{
143 .xsize_value = @intCast(width),
144 .ysize_value = @intCast(height),
145 .bytes_per_row_value = stride,
146 .storage = storage[0..required],
147 .owned = false,
148 };
149 }
150
151 pub fn deinit(self: *Self, allocator: std.mem.Allocator) void {
152 if (self.owned) {
153 std.debug.assert(self.storage.len != 0);
154 const aligned: []align(storage_alignment) u8 = @alignCast(self.storage);
155 allocator.free(aligned);
156 }
157 self.* = empty();
158 }
159
160 pub fn swap(self: *Self, other: *Self) void {
161 std.mem.swap(Self, self, other);
162 }
163
164 pub fn shrinkTo(self: *Self, width: usize, height: usize) GeometryError!void {
165 if (width > self.xsize_value or height > self.ysize_value) {
166 return error.InvalidShrink;
167 }
168 self.xsize_value = @intCast(width);
169 self.ysize_value = @intCast(height);
170 }
171
172 pub fn initializePaddingForUnalignedAccesses(self: *Self) GeometryError!void {
173 try self.initializePadding(.unaligned);
174 }
175
176 pub fn xsize(self: *const Self) usize {
177 return self.xsize_value;
178 }
179
180 pub fn ysize(self: *const Self) usize {
181 return self.ysize_value;
182 }
183
184 pub fn bytesPerRow(self: *const Self) usize {
185 return self.bytes_per_row_value;
186 }
187
188 pub fn pixelsPerRow(self: *const Self) usize {
189 return self.bytes_per_row_value / @sizeOf(T);
190 }
191
192 pub fn bytes(self: *Self) []u8 {
193 return self.storage;
194 }
195
196 pub fn constBytes(self: *const Self) []const u8 {
197 return self.storage;
198 }
199
200 pub fn constRow(self: *const Self, y: usize) []const T {
201 std.debug.assert(y < self.ysize_value);
202 if (self.bytes_per_row_value == 0) return &.{};
203 const begin = y * self.bytes_per_row_value;
204 const row = self.storage[begin..][0..self.bytes_per_row_value];
205 const ptr: [*]const T = @ptrCast(@alignCast(row.ptr));
206 return ptr[0..self.pixelsPerRow()];
207 }
208
209 pub fn mutableRow(self: *const Self, y: usize) []T {
210 std.debug.assert(y < self.ysize_value);
211 if (self.bytes_per_row_value == 0) return &.{};
212 const begin = y * self.bytes_per_row_value;
213 const row = self.storage[begin..][0..self.bytes_per_row_value];
214 const ptr: [*]T = @ptrCast(@alignCast(row.ptr));
215 return ptr[0..self.pixelsPerRow()];
216 }
217
218 fn initializePadding(self: *Self, mode: Padding) GeometryError!void {
219 if (self.xsize_value == 0 or self.ysize_value == 0 or vector_size == 1) return;
220 const valid_bytes = std.math.mul(
221 usize,
222 self.xsize_value,
223 @sizeOf(T),
224 ) catch return error.RowSizeOverflow;
225 const initialize_size = switch (mode) {
226 .round_up => try roundUp(valid_bytes, vector_size),
227 .unaligned => std.math.add(
228 usize,
229 valid_bytes,
230 vector_size - @sizeOf(T),
231 ) catch return error.RowSizeOverflow,
232 };
233 if (initialize_size > self.bytes_per_row_value) {
234 return error.InsufficientRowPadding;
235 }
236 for (0..self.ysize_value) |y| {
237 const begin = y * self.bytes_per_row_value + valid_bytes;
238 @memset(self.storage[begin..][0 .. initialize_size - valid_bytes], 0);
239 }
240 }
241 };
242 }
243
244 pub const ImageF = Image(f32);
245
246 pub fn Image3(comptime T: type) type {
247 const ImageT = Image(T);
248 return struct {
249 planes: [3]ImageT,
250 packed_storage: []u8,
251 packing: Packing,
252
253 const Self = @This();
254 pub const num_planes: usize = 3;
255
256 pub fn empty() Self {
257 return .{
258 .planes = .{ ImageT.empty(), ImageT.empty(), ImageT.empty() },
259 .packed_storage = &.{},
260 .packing = .none,
261 };
262 }
263
264 pub fn init(allocator: std.mem.Allocator, width: usize, height: usize) Error!Self {
265 try validateDimensions(width, height);
266 if (width == 0 or height == 0) {
267 return .{
268 .planes = .{
269 ImageT.zeroDimensions(width, height),
270 ImageT.zeroDimensions(width, height),
271 ImageT.zeroDimensions(width, height),
272 },
273 .packed_storage = &.{},
274 .packing = .none,
275 };
276 }
277 const stride = try bytesPerRowFor(width, @sizeOf(T), vector_size);
278 const plane_size = std.math.mul(usize, stride, height) catch
279 return error.AllocationSizeOverflow;
280 const allocation_size = std.math.mul(usize, plane_size, num_planes) catch
281 return error.AllocationSizeOverflow;
282 const storage = try allocator.alignedAlloc(
283 u8,
284 .fromByteUnits(storage_alignment),
285 allocation_size,
286 );
287 errdefer allocator.free(storage);
288 var result = empty();
289 result.packed_storage = storage;
290 result.packing = .owned;
291 for (&result.planes, 0..) |*current_plane, index| {
292 const begin = index * plane_size;
293 current_plane.* = try ImageT.initBorrowed(
294 width,
295 height,
296 stride,
297 storage[begin..][0..plane_size],
298 );
299 try current_plane.initializePadding(.round_up);
300 }
301 return result;
302 }
303
304 pub fn initBorrowed(
305 width: usize,
306 height: usize,
307 stride: usize,
308 buffers: [num_planes][]u8,
309 ) GeometryError!Self {
310 var result = empty();
311 for (&result.planes, buffers) |*current_plane, buffer| {
312 current_plane.* = try ImageT.initBorrowed(width, height, stride, buffer);
313 }
314 return result;
315 }
316
317 pub fn initPlanes(
318 plane0: *ImageT,
319 plane1: *ImageT,
320 plane2: *ImageT,
321 ) GeometryError!Self {
322 if (plane0 == plane1 or plane0 == plane2 or plane1 == plane2) {
323 return error.AliasedPlanes;
324 }
325 if (!sameSize(plane0, plane1) or !sameSize(plane0, plane2)) {
326 return error.PlaneSizeMismatch;
327 }
328 if (plane0.bytesPerRow() != plane1.bytesPerRow() or
329 plane0.bytesPerRow() != plane2.bytesPerRow())
330 {
331 return error.PlaneStrideMismatch;
332 }
333 const result = Self{
334 .planes = .{ plane0.*, plane1.*, plane2.* },
335 .packed_storage = &.{},
336 .packing = .none,
337 };
338 plane0.* = ImageT.empty();
339 plane1.* = ImageT.empty();
340 plane2.* = ImageT.empty();
341 return result;
342 }
343
344 pub fn deinit(self: *Self, allocator: std.mem.Allocator) void {
345 switch (self.packing) {
346 .owned => {
347 std.debug.assert(self.packed_storage.len != 0);
348 const aligned: []align(storage_alignment) u8 = @alignCast(self.packed_storage);
349 allocator.free(aligned);
350 },
351 .none => for (&self.planes) |*current_plane| current_plane.deinit(allocator),
352 }
353 self.* = empty();
354 }
355
356 pub fn swap(self: *Self, other: *Self) void {
357 std.mem.swap(Self, self, other);
358 }
359
360 pub fn shrinkTo(self: *Self, width: usize, height: usize) GeometryError!void {
361 if (width > self.xsize() or height > self.ysize()) {
362 return error.InvalidShrink;
363 }
364 for (&self.planes) |*current_plane| try current_plane.shrinkTo(width, height);
365 }
366
367 pub fn xsize(self: *const Self) usize {
368 return self.planes[0].xsize();
369 }
370
371 pub fn ysize(self: *const Self) usize {
372 return self.planes[0].ysize();
373 }
374
375 pub fn bytesPerRow(self: *const Self) usize {
376 return self.planes[0].bytesPerRow();
377 }
378
379 pub fn pixelsPerRow(self: *const Self) usize {
380 return self.planes[0].pixelsPerRow();
381 }
382
383 pub fn plane(self: *const Self, index: usize) *const ImageT {
384 std.debug.assert(index < num_planes);
385 return &self.planes[index];
386 }
387
388 pub fn constPlaneRow(self: *const Self, component: usize, y: usize) []const T {
389 std.debug.assert(component < num_planes);
390 return self.planes[component].constRow(y);
391 }
392
393 pub fn mutablePlaneRow(self: *const Self, component: usize, y: usize) []T {
394 std.debug.assert(component < num_planes);
395 return self.planes[component].mutableRow(y);
396 }
397 };
398 }
399
400 pub const Image3F = Image3(f32);
401
402 pub const Rect = struct {
403 x0_value: usize,
404 y0_value: usize,
405 xsize_value: usize,
406 ysize_value: usize,
407
408 pub fn empty() Rect {
409 return init(0, 0, 0, 0);
410 }
411
412 pub fn init(xbegin: usize, ybegin: usize, width: usize, height: usize) Rect {
413 return .{
414 .x0_value = xbegin,
415 .y0_value = ybegin,
416 .xsize_value = width,
417 .ysize_value = height,
418 };
419 }
420
421 pub fn initClamped(
422 xbegin: usize,
423 ybegin: usize,
424 xsize_max: usize,
425 ysize_max: usize,
426 xend: usize,
427 yend: usize,
428 ) Rect {
429 return .{
430 .x0_value = xbegin,
431 .y0_value = ybegin,
432 .xsize_value = clampedSize(xbegin, xsize_max, xend),
433 .ysize_value = clampedSize(ybegin, ysize_max, yend),
434 };
435 }
436
437 pub fn fromImage(image: anytype) Rect {
438 return init(0, 0, image.xsize(), image.ysize());
439 }
440
441 pub fn subrect(
442 self: Rect,
443 xbegin: usize,
444 ybegin: usize,
445 xsize_max: usize,
446 ysize_max: usize,
447 ) GeometryError!Rect {
448 const absolute_x = std.math.add(usize, self.x0_value, xbegin) catch
449 return error.RowSizeOverflow;
450 const absolute_y = std.math.add(usize, self.y0_value, ybegin) catch
451 return error.RowSizeOverflow;
452 const xend = std.math.add(usize, self.x0_value, self.xsize_value) catch
453 return error.RowSizeOverflow;
454 const yend = std.math.add(usize, self.y0_value, self.ysize_value) catch
455 return error.RowSizeOverflow;
456 return initClamped(
457 absolute_x,
458 absolute_y,
459 xsize_max,
460 ysize_max,
461 xend,
462 yend,
463 );
464 }
465
466 pub fn isInside(self: Rect, image: anytype) bool {
467 const xend = std.math.add(usize, self.x0_value, self.xsize_value) catch
468 return false;
469 const yend = std.math.add(usize, self.y0_value, self.ysize_value) catch
470 return false;
471 return xend <= image.xsize() and yend <= image.ysize();
472 }
473
474 pub fn constRow(self: Rect, comptime T: type, image: *const Image(T), y: usize) []const T {
475 std.debug.assert(self.isInside(image));
476 std.debug.assert(y < self.ysize_value);
477 return image.constRow(y + self.y0_value)[self.x0_value..];
478 }
479
480 pub fn mutableRow(self: Rect, comptime T: type, image: *const Image(T), y: usize) []T {
481 std.debug.assert(self.isInside(image));
482 std.debug.assert(y < self.ysize_value);
483 return image.mutableRow(y + self.y0_value)[self.x0_value..];
484 }
485
486 pub fn constPlaneRow(
487 self: Rect,
488 comptime T: type,
489 image: *const Image3(T),
490 component: usize,
491 y: usize,
492 ) []const T {
493 std.debug.assert(self.isInside(image));
494 std.debug.assert(y < self.ysize_value);
495 return image.constPlaneRow(component, y + self.y0_value)[self.x0_value..];
496 }
497
498 pub fn mutablePlaneRow(
499 self: Rect,
500 comptime T: type,
501 image: *const Image3(T),
502 component: usize,
503 y: usize,
504 ) []T {
505 std.debug.assert(self.isInside(image));
506 std.debug.assert(y < self.ysize_value);
507 return image.mutablePlaneRow(component, y + self.y0_value)[self.x0_value..];
508 }
509
510 pub fn x0(self: Rect) usize {
511 return self.x0_value;
512 }
513
514 pub fn y0(self: Rect) usize {
515 return self.y0_value;
516 }
517
518 pub fn xsize(self: Rect) usize {
519 return self.xsize_value;
520 }
521
522 pub fn ysize(self: Rect) usize {
523 return self.ysize_value;
524 }
525 };
526
527 pub fn sameSize(first: anytype, second: anytype) bool {
528 return first.xsize() == second.xsize() and first.ysize() == second.ysize();
529 }
530
531 pub fn mirror(coord: i64, size: usize) usize {
532 std.debug.assert(size != 0);
533 std.debug.assert(size <= std.math.maxInt(i64));
534 const size_u64: u64 = @intCast(size);
535 const period = 2 * size_u64;
536 const phase = if (coord >= 0)
537 @as(u64, @intCast(coord)) % period
538 else blk: {
539 const bits: u64 = @bitCast(coord);
540 const remainder = (0 -% bits) % period;
541 break :blk if (remainder == 0) 0 else period - remainder;
542 };
543 if (phase < size_u64) return @intCast(phase);
544 return @intCast(period - 1 - phase);
545 }
546
547 pub const WrapMirror = struct {
548 pub fn call(_: WrapMirror, coord: i64, size: usize) usize {
549 return mirror(coord, size);
550 }
551 };
552
553 pub const WrapUnchanged = struct {
554 pub fn call(_: WrapUnchanged, coord: i64, size: usize) usize {
555 std.debug.assert(coord >= 0);
556 std.debug.assert(coord < size);
557 return @intCast(coord);
558 }
559 };
560
561 pub const WrapRowMirror = struct {
562 first_row: [*]const f32,
563 last_row: [*]const f32,
564
565 pub fn init(image: anytype, ysize: usize) WrapRowMirror {
566 std.debug.assert(ysize != 0);
567 return .{
568 .first_row = image.constRow(0).ptr,
569 .last_row = image.constRow(ysize - 1).ptr,
570 };
571 }
572
573 pub fn call(self: WrapRowMirror, row: [*]const f32, stride: i64) [*]const f32 {
574 std.debug.assert(stride > 0);
575 const first = @intFromPtr(self.first_row);
576 const last = @intFromPtr(self.last_row);
577 const address = @intFromPtr(row);
578 const stride_elements: usize = @intCast(stride);
579 if (address < first) {
580 const distance_bytes = first - address;
581 std.debug.assert(distance_bytes % @sizeOf(f32) == 0);
582 const distance = distance_bytes / @sizeOf(f32);
583 std.debug.assert(distance >= stride_elements);
584 return self.first_row + (distance - stride_elements);
585 }
586 if (address > last) {
587 const distance_bytes = address - last;
588 std.debug.assert(distance_bytes % @sizeOf(f32) == 0);
589 const distance = distance_bytes / @sizeOf(f32);
590 std.debug.assert(distance >= stride_elements);
591 return self.last_row - (distance - stride_elements);
592 }
593 return row;
594 }
595 };
596
597 pub const WrapRowUnchanged = struct {
598 pub fn call(_: WrapRowUnchanged, row: [*]const f32, _: i64) [*]const f32 {
599 return row;
600 }
601 };
602
603 fn validateComponent(comptime T: type) void {
604 if (@sizeOf(T) != 1 and @sizeOf(T) != 2 and @sizeOf(T) != 4 and @sizeOf(T) != 8) {
605 @compileError("Highway images require 1/2/4/8-byte component types");
606 }
607 if (@typeInfo(T) == .pointer or @typeInfo(T) == .optional) {
608 @compileError("Highway images require plain component values");
609 }
610 }
611
612 fn validateGeometry(component_size: usize, selected_vector_size: usize) GeometryError!void {
613 if (component_size != 1 and component_size != 2 and
614 component_size != 4 and component_size != 8)
615 {
616 return error.InvalidComponentSize;
617 }
618 if (!std.math.isPowerOfTwo(selected_vector_size)) return error.InvalidVectorSize;
619 if (selected_vector_size != 1 and selected_vector_size < component_size) {
620 return error.InvalidVectorSize;
621 }
622 }
623
624 fn validateDimensions(xsize: usize, ysize: usize) GeometryError!void {
625 if (xsize > std.math.maxInt(u32) or ysize > std.math.maxInt(u32)) {
626 return error.DimensionTooLarge;
627 }
628 }
629
630 fn roundUp(value: usize, alignment: usize) GeometryError!usize {
631 std.debug.assert(std.math.isPowerOfTwo(alignment));
632 const mask = alignment - 1;
633 const biased = std.math.add(usize, value, mask) catch return error.RowSizeOverflow;
634 return biased & ~mask;
635 }
636
637 fn clampedSize(begin: usize, size_max: usize, end: usize) usize {
638 if (end <= begin) return 0;
639 return @min(size_max, end - begin);
640 }