lib/png/src/encode/storage.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const alloc_phase = @import("alloc_phase");
3 const flate = std.compress.flate;
4 const capacity_mod = @import("capacity.zig");
5 const model = @import("model.zig");
6 const plan_mod = @import("plan.zig");
7
8 pub const Regions = struct {
9 window: []u8,
10 raw: []u8,
11 output: []u8,
12 };
13
14 pub const Status = struct {
15 phase: alloc_phase.capacity.Phase,
16 in_use: bool,
17 storage_bytes: usize,
18 image_pixels: usize,
19 raw_bytes: usize,
20 zlib_bytes: usize,
21 output_bytes: usize,
22 };
23
24 pub const Storage = struct {
25 phase: alloc_phase.capacity.Phase,
26 capacity: capacity_mod.Capacity,
27 bytes: []align(capacity_mod.storage_alignment) u8,
28 window: []u8,
29 raw: []u8,
30 count: []u8,
31 output: []u8,
32 in_use: bool = false,
33
34 pub const Limits: type = capacity_mod.Limits;
35 pub const Capacity: type = capacity_mod.Capacity;
36 pub const Exhaustion: type = model.Exhaustion;
37 pub const InitError = std.mem.Allocator.Error || capacity_mod.DeriveError;
38 pub const AcquireError: type = plan_mod.Error;
39
40 pub const claim: alloc_phase.capacity.Declaration = .{
41 .source = .{
42 .id = "png.encode_storage",
43 .kind = .phase_static,
44 .limit_source = .caller,
45 .storage = .{
46 .covered = &.{
47 .{
48 .id = "exact_png_encoding_plan",
49 .lifetime = .steady,
50 .detail = "exact PNG encoding plan",
51 },
52 .{
53 .id = "deflate_window_and_streaming_scratch",
54 .lifetime = .steady,
55 .detail = "DEFLATE window and streaming scratch",
56 },
57 .{
58 .id = "encoded_png_output_bytes",
59 .lifetime = .steady,
60 .detail = "encoded PNG output bytes",
61 },
62 },
63 .excluded = &.{
64 "caller-owned RGBA input bytes",
65 "filesystem and downstream decoder storage",
66 },
67 },
68 .capacity = .{
69 .inputs = &.{
70 alloc_phase.capacity.bindInput(Limits, "image_width", "image.width"),
71 alloc_phase.capacity.bindInput(Limits, "image_height", "image.height"),
72 alloc_phase.capacity.bindInput(Limits, "image_rgba8", "image.rgba8"),
73 },
74 .type_selectors = &.{},
75 .nodes = &.{
76 .{ .collection = .{ .length = 2 } },
77 .{ .constant = 65536 },
78 .{ .constant = 8192 },
79 .{ .add = .{ .left = 1, .right = 2 } },
80 .{ .add = .{ .left = 3, .right = 0 } },
81 },
82 .assertions = &.{.{
83 .scope = .closure_total,
84 .measure = .retained,
85 .relation = .upper_bound,
86 .expression = 4,
87 }},
88 },
89 .overload = .{
90 .kind = .reject_before_mutation,
91 .detail = "Bounds, input mismatch, and concurrent use reject before output mutation.",
92 },
93 .risks = .{
94 .transitive = .{
95 .status = .witnessed,
96 .detail = "Fixed regions cover scanlines, DEFLATE, chunk framing, and output.",
97 },
98 .foreign = .{
99 .status = .excluded,
100 .detail = "input ownership and output consumption remain caller effects",
101 },
102 },
103 .obligations = &.{
104 .{ .key = "png_encode_capacity", .role = .capacity_model },
105 .{ .key = "png_encode_acquisition", .role = .custom },
106 .{ .key = "png_encode_oom", .role = .custom },
107 .{ .key = "png_encode_boundaries", .role = .overload },
108 .{ .key = "png_encode_reuse", .role = .overload },
109 .{ .key = "png_encode_sealed", .role = .transitive_risk },
110 .{ .key = "png_encode_root", .role = .custom },
111 .{ .key = "png_encode_consumer", .role = .foreign_risk },
112 },
113 },
114 .bindings = .{
115 .owner = @This(),
116 .seal = .{
117 .family = alloc_phase.capacity.selector(@This().activate),
118 .premise = .{
119 .class = .checked_semantic_fact,
120 .authority = .checker,
121 },
122 },
123 .teardown = .{
124 .family = alloc_phase.capacity.selector(@This().deinit),
125 .premise = .{
126 .class = .checked_semantic_fact,
127 .authority = .checker,
128 },
129 },
130 },
131 };
132
133 pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!Storage {
134 const capacity = try Capacity.derive(limits);
135 const bytes = try allocator.alignedAlloc(
136 u8,
137 .fromByteUnits(capacity_mod.storage_alignment),
138 capacity.storage_bytes,
139 );
140 return .{
141 .phase = .initialization,
142 .capacity = capacity,
143 .bytes = bytes,
144 .window = bytes[capacity.window_offset..][0..flate.max_window_len],
145 .raw = bytes[capacity.raw_offset..][0..plan_mod.stream_bytes],
146 .count = bytes[capacity.count_offset..][0..plan_mod.stream_bytes],
147 .output = bytes[capacity.output_offset..][0..capacity.plan.output_bytes],
148 };
149 }
150
151 pub fn activate(self: *Storage) void {
152 std.debug.assert(self.phase == .initialization);
153 std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
154 self.phase = .steady;
155 }
156
157 pub fn acquire(self: *Storage, image: model.ImageView) AcquireError!Regions {
158 std.debug.assert(self.phase == .steady);
159 if (self.in_use) return error.EncodeStorageInUse;
160 const actual = try plan_mod.Plan.inspectRegions(image, self.capacity.plan.bounds, .{
161 .window = self.window,
162 .raw = self.raw,
163 .count = self.count,
164 });
165 if (!std.meta.eql(actual, self.capacity.plan)) return error.EncodeInputMismatch;
166 self.in_use = true;
167 return .{ .window = self.window, .raw = self.raw, .output = self.output };
168 }
169
170 pub fn reset(self: *Storage) void {
171 std.debug.assert(self.phase == .steady);
172 std.debug.assert(self.in_use);
173 self.in_use = false;
174 }
175
176 pub fn status(self: *const Storage) Status {
177 return .{
178 .phase = self.phase,
179 .in_use = self.in_use,
180 .storage_bytes = self.capacity.storage_bytes,
181 .image_pixels = self.capacity.plan.image_pixels,
182 .raw_bytes = self.capacity.plan.raw_bytes,
183 .zlib_bytes = self.capacity.plan.zlib_bytes,
184 .output_bytes = self.capacity.plan.output_bytes,
185 };
186 }
187
188 pub fn deinit(self: *Storage, allocator: std.mem.Allocator) void {
189 std.debug.assert(self.phase != .teardown);
190 std.debug.assert(!self.in_use);
191 std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
192 self.phase = .teardown;
193 allocator.free(self.bytes);
194 self.bytes = &.{};
195 self.window = &.{};
196 self.raw = &.{};
197 self.count = &.{};
198 self.output = &.{};
199 }
200 };
201
202 const witness_pixels = [_]u8{ 1, 2, 3, 255, 4, 5, 6, 255 };
203 const witness_image = model.ImageView{ .rgba8 = &witness_pixels, .width = 2, .height = 1 };
204
205 fn checkInitFailures(allocator: std.mem.Allocator) !void {
206 var scratch: plan_mod.Scratch = undefined;
207 var storage = try Storage.init(allocator, .{
208 .image = witness_image,
209 .bounds = .{ .image_pixels = 2 },
210 .scratch = &scratch,
211 });
212 storage.deinit(allocator);
213 }
214
215 test "PNG encode storage acquires one exact aligned region" {
216 comptime {
217 @stardustClaim(
218 @import("alloc_phase").capacity.witness(Storage, "png_encode_acquisition"),
219 null,
220 null,
221 null,
222 null,
223 null,
224 null,
225 );
226 }
227
228 var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
229 var scratch: plan_mod.Scratch = undefined;
230 const limits = capacity_mod.Limits{
231 .image = witness_image,
232 .bounds = .{ .image_pixels = 2 },
233 .scratch = &scratch,
234 };
235 const capacity = try capacity_mod.Capacity.derive(limits);
236 var storage = try Storage.init(counting.allocator(), limits);
237 defer storage.deinit(counting.allocator());
238 try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
239 try std.testing.expectEqual(capacity.storage_bytes, counting.allocated_bytes);
240 storage.activate();
241 const regions = try storage.acquire(witness_image);
242 defer storage.reset();
243 const base = @intFromPtr(storage.bytes.ptr);
244 try std.testing.expectEqual(base + capacity.window_offset, @intFromPtr(regions.window.ptr));
245 try std.testing.expectEqual(base + capacity.raw_offset, @intFromPtr(regions.raw.ptr));
246 try std.testing.expectEqual(base + capacity.output_offset, @intFromPtr(regions.output.ptr));
247 }
248
249 test "PNG encode storage retries after every allocation failure" {
250 comptime {
251 @stardustClaim(
252 @import("alloc_phase").capacity.witness(Storage, "png_encode_oom"),
253 null,
254 null,
255 null,
256 null,
257 null,
258 null,
259 );
260 }
261
262 try std.testing.checkAllAllocationFailures(std.testing.allocator, checkInitFailures, .{});
263 }
264
265 comptime {
266 alloc_phase.capacity.requireAllocatorRejectingOwnerShape(Storage);
267 }