also looking at this
feat(video): add checked capture plane packing
#915
3 files
+68
-0
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -0,0 +1,23 @@ | ||
| 1 | # Packing captured image planes | |
| 2 | ||
| 3 | `core/plane_pack.zig` provides allocation-free checked copies for capture buffers | |
| 4 | whose 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 | ||
| 11 | Both validate sizes with checked arithmetic before writing. Final row padding | |
| 12 | need not be present. I420 requires positive even dimensions. Truncated chroma, | |
| 13 | undersized output and overflow are errors; the destination remains unchanged. | |
| 14 | Sources remain borrowed, the caller owns the destination, and source and output | |
| 15 | must not overlap. These helpers do not convert color, resize, or synchronize DMA. | |
| 16 | ||
| 17 | zig-camera uses these helpers after beginning CPU access to a leased DMA buffer. | |
| 18 | The camera ISP produces its low-resolution stream; packing only happens for an | |
| 19 | explicit owned copy. A mapped view keeps the original padding and ownership. | |
| 20 | ||
| 21 | Focused validation: `zig test packages/video/src/core/plane_pack.zig` on Zig | |
| 22 | 0.15.2 and Home's 0.17 development toolchain. This does not establish correctness | |
| 23 | of the entire video package's unrelated codecs or conversion paths. | |
| @@ -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. | |
| 3 | const std = ; | |
| 4 | pub 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). | |
| 14 | pub 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 | } | |
| 31 | test "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 | } | |
| 36 | test "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 | } | |
| @@ -842,6 +842,7 @@ pub const applyAudioGain = sample_convert.applyGain; | ||
| 842 | 842 | pub const normalizeAudio = sample_convert.normalize; |
| 843 | 843 | pub const AudioChannelLayout = sample_convert.ChannelLayout; |
| 844 | 844 | |
| 845 | pub const plane_pack = ; | |
| 845 | 846 | pub const pixel_convert = ; |
| 846 | 847 | pub const PixelConvertFormat = pixel_convert.PixelFormat; |
| 847 | 848 | pub const ConvertColorSpace = pixel_convert.ColorSpace; |
| @@ -1218,6 +1219,7 @@ test "Video library imports" { | ||
| 1218 | 1219 | _ = dash; |
| 1219 | 1220 | _ = subtitle_convert; |
| 1220 | 1221 | _ = sample_convert; |
| 1222 | _ = plane_pack; | |
| 1221 | 1223 | _ = pixel_convert; |
| 1222 | 1224 | _ = nal_convert; |
| 1223 | 1225 | _ = remux; |