ReviewOS

also looking at this

home-lang/home

feat(video): add bounded packet history, delayed FIFO and owned snapshots

#916
Merged chris wants to merge feat/keyframe-packet-ring into main
3 files +229 -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.

packages/video/docs/packet-ring.mdadded+24-0
Changes to packages/video/docs/packet-ring.md
@@ -0,0 +1,24 @@
1# Keyframe-aligned packet history
2
3`core/packet_ring.zig` (exported as `video.packet_ring`) adapts the existing video
4ring-buffer structure to encoded pre-roll: storage consists of one fixed byte
5arena and a fixed entry array. Push copies borrowed packets without allocating
6or blocking. The caller serializes all access and chooses timestamp units.
7
8Byte capacity, frame capacity and duration are hard upper bounds. Eviction
9removes dependent packets until the oldest retained packet is a keyframe.
10If the limits cannot hold a GOP, history can be empty until the next key.
11Oversized packets and non-increasing timestamps are rejected without modifying
12retained history. Parameter sets/codec configuration belong outside the ring.
13`entry` returns metadata; `copy` explicitly copies a retained payload to caller
14storage, including when it wraps in the byte arena. Views expire on mutation.
15
16Three standalone regression tests cover arena wrap, byte/frame/time eviction,
17keyframe gating, rejected-input atomicity, zero capacities and allocator failure
18cleanup. Tested on Zig 0.15.2 and the installed 0.17 development toolchain. This
19focused validation does not establish correctness of the wider video graph.
20
21zig-camera uses this module with Home JPEG/Matroska and x264/x265/fragmented MP4.
22It retains compressed access units, without retaining camera DMA leases or raw
23RGB frames. Explicit snapshots need scratch space for one largest retained
24packet; remuxing starts with fresh codec headers and resets file timestamps.
packages/video/src/core/packet_ring.zigadded+114-0
Changes to packages/video/src/core/packet_ring.zig
@@ -0,0 +1,114 @@
1//! Owned encoded-packet history. One caller serializes all access. Fixed byte
2//! arena and entry array; push never allocates, blocks, or keeps borrowed input.
3//! Eviction drops dependent packets until the oldest retained packet is a key.
4const std = ;
5pub const Entry = struct { offset: usize, length: usize, timestamp: u64, key: bool };
6pub const Ring = struct {
7 allocator: std.mem.Allocator,
8 bytes: []u8,
9 entries: []Entry,
10 duration: u64,
11 head: usize = 0,
12 count: usize = 0,
13 used: usize = 0,
14 write_pos: usize = 0,
15 last: ?u64 = null,
16 evicted: u64 = 0,
17 skipped: u64 = 0,
18 pub fn init(allocator: std.mem.Allocator, byte_capacity: usize, frame_capacity: usize, duration: u64) !Ring {
19 if (byte_capacity == 0 or frame_capacity == 0 or frame_capacity > std.math.maxInt(usize) / 2 or duration == 0) return error.InvalidConfiguration;
20 const bytes = try allocator.alloc(u8, byte_capacity);
21 errdefer allocator.free(bytes);
22 const entries = try allocator.alloc(Entry, frame_capacity);
23 return .{ .allocator = allocator, .bytes = bytes, .entries = entries, .duration = duration };
24 }
25 pub fn deinit(s: *Ring) void {
26 s.allocator.free(s.bytes);
27 s.allocator.free(s.entries);
28 }
29 fn drop(s: *Ring) void {
30 s.used -= s.entries[s.head].length;
31 s.head = (s.head + 1) % s.entries.len;
32 s.count -= 1;
33 s.evicted += 1;
34 }
35 pub fn entry(s: *const Ring, index: usize) ?Entry {
36 if (index >= s.count) return null;
37 return s.entries[(s.head + index) % s.entries.len];
38 }
39 /// False means waiting for a key after eviction. Limits are upper bounds;
40 /// history can be empty when a GOP is larger than the retention limits.
41 /// Oversized/invalid input is rejected before modifying retained history.
42 pub fn push(s: *Ring, packet: []const u8, timestamp: u64, key: bool) !bool {
43 if (packet.len == 0 or packet.len > s.bytes.len) return error.PacketTooLarge;
44 if (s.last) |last| if (timestamp <= last) return error.InvalidTimestamp;
45 while (s.count > 0 and (s.count == s.entries.len or packet.len > s.bytes.len - s.used or timestamp - s.entry(0).?.timestamp > s.duration)) s.drop();
46 while (s.count > 0 and !s.entry(0).?.key) s.drop();
47 s.last = timestamp;
48 if (s.count == 0 and !key) {
49 s.skipped += 1;
50 return false;
51 }
52 const first = @min(packet.len, s.bytes.len - s.write_pos);
53 @memcpy(s.bytes[s.write_pos..][0..first], packet[0..first]);
54 @memcpy(s.bytes[0 .. packet.len - first], packet[first..]);
55 s.entries[(s.head + s.count) % s.entries.len] = .{ .offset = s.write_pos, .length = packet.len, .timestamp = timestamp, .key = key };
56 s.write_pos = if (packet.len - first != 0) packet.len - first else if (s.write_pos + first == s.bytes.len) 0 else s.write_pos + first;
57 s.count += 1;
58 s.used += packet.len;
59 return true;
60 }
61 /// Explicit owned snapshot copy; data and metadata views otherwise expire
62 /// on the next mutation. Returns the active destination slice.
63 pub fn copy(s: *const Ring, index: usize, out: []u8) ![]const u8 {
64 const e = s.entry(index) orelse return error.InvalidIndex;
65 if (out.len < e.length) return error.BufferTooSmall;
66 const first = @min(e.length, s.bytes.len - e.offset);
67 @memcpy(out[0..first], s.bytes[e.offset..][0..first]);
68 @memcpy(out[first..e.length], s.bytes[0 .. e.length - first]);
69 return out[0..e.length];
70 }
71};
72test "byte wrap and GOP eviction keep only independently decodable history" {
73 var ring = try Ring.init(std.testing.allocator, 10, 4, 100);
74 defer ring.deinit();
75 try std.testing.expect(try ring.push("aaaa", 0, true));
76 try std.testing.expect(try ring.push("bb", 1, false));
77 try std.testing.expect(try ring.push("ccc", 2, true));
78 try std.testing.expect(try ring.push("dddd", 3, false));
79 try std.testing.expectEqual(@as(usize, 2), ring.count);
80 try std.testing.expectEqual(@as(usize, 7), ring.used);
81 var out: [10]u8 = undefined;
82 try std.testing.expectEqualStrings("ccc", try ring.copy(0, &out));
83 try std.testing.expectEqualStrings("dddd", try ring.copy(1, &out));
84 try std.testing.expect(!try ring.push("12345678", 4, false));
85 try std.testing.expectEqual(@as(usize, 0), ring.count);
86 try std.testing.expect(try ring.push("key", 5, true));
87 try std.testing.expectEqualStrings("key", try ring.copy(0, &out));
88}
89test "frame and time limits; rejected inputs preserve existing payload" {
90 var ring = try Ring.init(std.testing.allocator, 10, 2, 3);
91 defer ring.deinit();
92 try std.testing.expect(!try ring.push("p", 0, false));
93 try std.testing.expect(try ring.push("a", 1, true));
94 try std.testing.expect(try ring.push("b", 2, false));
95 try std.testing.expect(!try ring.push("c", 3, false));
96 try std.testing.expect(try ring.push("d", 4, true));
97 try std.testing.expectError(error.PacketTooLarge, ring.push("01234567890", 5, true));
98 try std.testing.expectError(error.InvalidTimestamp, ring.push("x", 4, true));
99 var out: [1]u8 = undefined;
100 try std.testing.expectEqualStrings("d", try ring.copy(0, &out));
101 try std.testing.expect(!try ring.push("e", 8, false));
102 try std.testing.expectEqual(@as(usize, 0), ring.count);
103 try std.testing.expectError(error.InvalidIndex, ring.copy(0, &out));
104}
105test "initialization rejects zero limits and frees partial allocations" {
106 try std.testing.expectError(error.InvalidConfiguration, Ring.init(std.testing.allocator, 0, 1, 1));
107 const Check = struct {
108 fn run(a: std.mem.Allocator) !void {
109 var r = try Ring.init(a, 10, 2, 3);
110 defer r.deinit();
111 }
112 };
113 try std.testing.checkAllAllocationFailures(std.testing.allocator, Check.run, .{});
114}
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 packet_ring = ;
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 _ = packet_ring;
12211223 _ = pixel_convert;
12221224 _ = nal_convert;
12231225 _ = remux;