lib/windowing/src/presentation.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 pub const default_retained_frame_byte_count: usize = 3840 * 2160 * 4;
4
5 pub const Limits = struct {
6 pending_feedback_count: usize = 256,
7 retained_outcome_count: usize = 256,
8 active_buffer_count: usize = 3,
9 retired_buffer_count: usize = 3,
10 retained_frame_byte_count: usize = default_retained_frame_byte_count,
11 };
12
13 pub const CapacityError = error{
14 FrameStorageEmpty,
15 CapacityOverflow,
16 };
17
18 pub const Capacity = struct {
19 retained_frame_byte_count: usize,
20 x11_packed_region_bytes: usize,
21 wayland_pending_frame_bytes: usize,
22 cocoa_frame_slot_count: usize,
23 cocoa_frame_storage_bytes: usize,
24 maximum_backend_frame_storage_bytes: usize,
25
26 pub fn derive(limits: Limits) CapacityError!Capacity {
27 if (limits.retained_frame_byte_count == 0) return error.FrameStorageEmpty;
28 const cocoa_frame_slot_count = 2;
29 const cocoa_frame_storage_bytes = std.math.mul(
30 usize,
31 limits.retained_frame_byte_count,
32 cocoa_frame_slot_count,
33 ) catch return error.CapacityOverflow;
34 return .{
35 .retained_frame_byte_count = limits.retained_frame_byte_count,
36 .x11_packed_region_bytes = limits.retained_frame_byte_count,
37 .wayland_pending_frame_bytes = limits.retained_frame_byte_count,
38 .cocoa_frame_slot_count = cocoa_frame_slot_count,
39 .cocoa_frame_storage_bytes = cocoa_frame_storage_bytes,
40 .maximum_backend_frame_storage_bytes = cocoa_frame_storage_bytes,
41 };
42 }
43 };
44
45 pub const StorageStatus = struct {
46 retired_buffer_capacity_rejection_count: u64 = 0,
47 frame_capacity_rejection_count: u64 = 0,
48 wayland_output_capacity_rejection_count: u64 = 0,
49 /// The count of presents in which at least one accumulator ran out of room
50 /// and fell back to a single covering rectangle. The Wayland backend raises
51 /// the count in `Presenter.markFrame` by one per present, whatever the
52 /// number of accumulators that collapsed within it. A present whose
53 /// accumulator collapsed still reaches the compositor and still carries the
54 /// right pixels, at the cost of sending more of them. The count stops at
55 /// the largest value the field holds. A caller reads the field through the
56 /// presentation storage status the window reports to know when the backend
57 /// sent more pixels than the regions it reported cover.
58 damage_capacity_collapse_count: u64 = 0,
59 };
60
61 pub const Status = enum {
62 unavailable,
63 active,
64 failed,
65 };
66
67 pub const TimestampError = error{InvalidTimestamp};
68
69 pub const Timestamp = struct {
70 seconds: u64,
71 nanoseconds: u32,
72
73 pub fn init(seconds: u64, nanoseconds: u32) TimestampError!Timestamp {
74 if (nanoseconds >= std.time.ns_per_s) return error.InvalidTimestamp;
75 return .{
76 .seconds = seconds,
77 .nanoseconds = nanoseconds,
78 };
79 }
80
81 pub fn fromNanoseconds(nanoseconds: u64) Timestamp {
82 return .{
83 .seconds = nanoseconds / std.time.ns_per_s,
84 .nanoseconds = @intCast(nanoseconds % std.time.ns_per_s),
85 };
86 }
87
88 pub fn totalNanoseconds(self: Timestamp) u128 {
89 return @as(u128, self.seconds) * std.time.ns_per_s + self.nanoseconds;
90 }
91
92 pub fn elapsedSince(self: Timestamp, earlier: Timestamp) ?u128 {
93 const current = self.totalNanoseconds();
94 const start = earlier.totalNanoseconds();
95 return if (current >= start) current - start else null;
96 }
97 };
98
99 pub const Kind = packed struct(u32) {
100 vsync: bool = false,
101 hw_clock: bool = false,
102 hw_completion: bool = false,
103 zero_copy: bool = false,
104 _: u28 = 0,
105 };
106
107 pub const Submission = struct {
108 id: u64,
109 clock_id: u32,
110 timestamp: Timestamp,
111 };
112
113 pub const Presented = struct {
114 submission: Submission,
115 timestamp: Timestamp,
116 refresh_nanoseconds: u32,
117 sequence: u64,
118 kind: Kind,
119 synchronized_outputs: u32,
120
121 pub fn latencyNanoseconds(self: Presented) ?u128 {
122 return self.timestamp.elapsedSince(self.submission.timestamp);
123 }
124 };
125
126 pub const Discarded = struct {
127 submission: Submission,
128 };
129
130 pub const Outcome = union(enum) {
131 presented: Presented,
132 discarded: Discarded,
133
134 pub fn submission(self: Outcome) Submission {
135 return switch (self) {
136 .presented => |value| value.submission,
137 .discarded => |value| value.submission,
138 };
139 }
140 };
141
142 pub fn rgba8ByteCount(width: u32, height: u32) ?usize {
143 if (width == 0 or height == 0) return null;
144 const pixels = std.math.mul(usize, width, height) catch return null;
145 return std.math.mul(usize, pixels, 4) catch null;
146 }
147
148 test "presentation capacity derives exact backend frame storage" {
149 const capacity = try Capacity.derive(.{ .retained_frame_byte_count = 13 });
150 try std.testing.expectEqual(@as(usize, 13), capacity.retained_frame_byte_count);
151 try std.testing.expectEqual(@as(usize, 13), capacity.x11_packed_region_bytes);
152 try std.testing.expectEqual(@as(usize, 13), capacity.wayland_pending_frame_bytes);
153 try std.testing.expectEqual(@as(usize, 2), capacity.cocoa_frame_slot_count);
154 try std.testing.expectEqual(@as(usize, 26), capacity.cocoa_frame_storage_bytes);
155 try std.testing.expectEqual(@as(usize, 26), capacity.maximum_backend_frame_storage_bytes);
156 try std.testing.expectError(error.FrameStorageEmpty, Capacity.derive(.{
157 .retained_frame_byte_count = 0,
158 }));
159 try std.testing.expectError(error.CapacityOverflow, Capacity.derive(.{
160 .retained_frame_byte_count = std.math.maxInt(usize),
161 }));
162 }
163
164 test "RGBA frame bytes reject empty and overflowing dimensions" {
165 try std.testing.expectEqual(@as(?usize, 48), rgba8ByteCount(4, 3));
166 try std.testing.expectEqual(@as(?usize, null), rgba8ByteCount(0, 3));
167 try std.testing.expectEqual(
168 @as(?usize, null),
169 rgba8ByteCount(std.math.maxInt(u32), std.math.maxInt(u32)),
170 );
171 }
172
173 test "timestamps validate their nanosecond component" {
174 const timestamp = try Timestamp.init(0x0123_4567_89ab_cdef, 987_654_321);
175 try std.testing.expectEqual(@as(u64, 0x0123_4567_89ab_cdef), timestamp.seconds);
176 try std.testing.expectEqual(@as(u32, 987_654_321), timestamp.nanoseconds);
177 try std.testing.expectError(error.InvalidTimestamp, Timestamp.init(0, std.time.ns_per_s));
178 }
179
180 test "presented outcomes derive latency in the compositor clock domain" {
181 const submitted = Timestamp.fromNanoseconds(4_000_000_005);
182 const presented = Timestamp.fromNanoseconds(4_016_666_672);
183 const outcome = Presented{
184 .submission = .{ .id = 1, .clock_id = 7, .timestamp = submitted },
185 .timestamp = presented,
186 .refresh_nanoseconds = 16_666_667,
187 .sequence = 3,
188 .kind = .{ .vsync = true },
189 .synchronized_outputs = 1,
190 };
191 try std.testing.expectEqual(@as(?u128, 16_666_667), outcome.latencyNanoseconds());
192 }