ReviewOS

also looking at this

home-lang/home

feat(video): add checked capture plane packing

#915
Merged chris wants to merge feat/checked-plane-packing into main
3 files +68 -0
packages/video/docs/capture-planes.mdadded+23-0
Changes to packages/video/docs/capture-planes.md
@@ -0,0 +1,23 @@
1# Packing captured image planes
2
3`core/plane_pack.zig` provides allocation-free checked copies for capture buffers
4whose row padding is not part of the image. It is also exported as
5`video.plane_pack`.
6
7- `rows(source, stride, active_row_bytes, height, destination)` copies active bytes.
8- `packI420(planes, strides, width, height, destination)` packs separate Y, U, V
9 planes into tight I420; each source plane can have its own stride.
10
11Both validate sizes with checked arithmetic before writing. Final row padding
12need not be present. I420 requires positive even dimensions. Truncated chroma,
13undersized output and overflow are errors; the destination remains unchanged.
14Sources remain borrowed, the caller owns the destination, and source and output
15must not overlap. These helpers do not convert color, resize, or synchronize DMA.
16
17zig-camera uses these helpers after beginning CPU access to a leased DMA buffer.
18The camera ISP produces its low-resolution stream; packing only happens for an
19explicit owned copy. A mapped view keeps the original padding and ownership.
20
21Focused validation: `zig test packages/video/src/core/plane_pack.zig` on Zig
220.15.2 and Home's 0.17 development toolchain. This does not establish correctness
23of the entire video package's unrelated codecs or conversion paths.
packages/video/src/core/plane_pack.zigadded+43-0
Changes to packages/video/src/core/plane_pack.zig
@@ -0,0 +1,43 @@
1//! Checked copies from padded capture planes into tightly packed owned buffers.
2//! Callers retain source ownership and must provide non-overlapping destination.
3const std = ;
4pub fn rows(source: []const u8, stride: usize, width: usize, height: usize, destination: []u8) !void {
5 if (width == 0 or height == 0 or stride < width) return error.InvalidGeometry;
6 const size = std.math.mul(usize, width, height) catch return error.InvalidGeometry;
7 const last = std.math.mul(usize, stride, height - 1) catch return error.InvalidGeometry;
8 const needed = std.math.add(usize, last, width) catch return error.InvalidGeometry;
9 if (source.len < needed or destination.len < size) return error.BufferTooSmall;
10 for (0..height) |y| @memcpy(destination[y * width ..][0..width], source[y * stride ..][0..width]);
11}
12/// I420 Y,U,V planes; strides are independent. Dimensions must be even.
13/// Validate all three planes before touching destination (failure is atomic).
14pub fn packI420(planes: [3][]const u8, strides: [3]usize, width: usize, height: usize, destination: []u8) !void {
15 if (width == 0 or height == 0 or (width | height) & 1 != 0) return error.InvalidGeometry;
16 const pixels = std.math.mul(usize, width, height) catch return error.InvalidGeometry;
17 const size = std.math.add(usize, pixels, pixels / 2) catch return error.InvalidGeometry;
18 if (destination.len < size) return error.BufferTooSmall;
19 for (0..3) |i| {
20 const w = if (i == 0) width else width / 2;
21 const h = if (i == 0) height else height / 2;
22 if (strides[i] < w) return error.InvalidGeometry;
23 const last = std.math.mul(usize, strides[i], h - 1) catch return error.InvalidGeometry;
24 const needed = std.math.add(usize, last, w) catch return error.InvalidGeometry;
25 if (planes[i].len < needed) return error.BufferTooSmall;
26 }
27 try rows(planes[0], strides[0], width, height, destination[0..pixels]);
28 try rows(planes[1], strides[1], width / 2, height / 2, destination[pixels..][0 .. pixels / 4]);
29 try rows(planes[2], strides[2], width / 2, height / 2, destination[pixels + pixels / 4 ..][0 .. pixels / 4]);
30}
31test "I420 padded planes omit padding and accept absent final padding" {
32 var out: [12]u8 = undefined;
33 try packI420(.{ &.{ 1, 2, 3, 4, 99, 99, 5, 6, 7, 8 }, &.{ 9, 10 }, &.{ 11, 12 } }, .{ 6, 4, 3 }, 4, 2, &out);
34 try std.testing.expectEqualSlices(u8, &.{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }, &out);
35}
36test "invalid geometry, overflow and truncated chroma never write output" {
37 var out: [12]u8 = @splat(42);
38 try std.testing.expectError(error.BufferTooSmall, packI420(.{ &.{ 1, 2, 3, 4, 5, 6, 7, 8 }, &.{ 9, 10 }, &.{11} }, .{ 4, 2, 2 }, 4, 2, &out));
39 try std.testing.expectEqualSlices(u8, &@as([12]u8, @splat(42)), &out);
40 try std.testing.expectError(error.InvalidGeometry, rows(&.{}, std.math.maxInt(usize), 2, 3, &out));
41 try std.testing.expectError(error.InvalidGeometry, packI420(.{ &.{}, &.{}, &.{} }, .{ 0, 0, 0 }, 3, 2, &out));
42 try std.testing.expectError(error.BufferTooSmall, rows(&.{ 1, 2, 3, 4 }, 2, 2, 2, out[0..3]));
43}
packages/video/src/video.zigmodified+2-0
Changes to packages/video/src/video.zig
@@ -842,6 +842,7 @@ pub const applyAudioGain = sample_convert.applyGain;
842842pub const normalizeAudio = sample_convert.normalize;
843843pub const AudioChannelLayout = sample_convert.ChannelLayout;
844844
845pub const plane_pack = ;
845846pub const pixel_convert = ;
846847pub const PixelConvertFormat = pixel_convert.PixelFormat;
847848pub const ConvertColorSpace = pixel_convert.ColorSpace;
@@ -1218,6 +1219,7 @@ test "Video library imports" {
12181219 _ = dash;
12191220 _ = subtitle_convert;
12201221 _ = sample_convert;
1222 _ = plane_pack;
12211223 _ = pixel_convert;
12221224 _ = nal_convert;
12231225 _ = remux;