lib/gui/src/paint/compositor.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const gpu = @import("gpu");
3 const command = @import("command.zig");
4 const cpu = @import("cpu/root.zig");
5 const executor = @import("executor.zig");
6 const gui = @import("../root.zig");
7 const paint_image = @import("image.zig");
8
9 const Allocator = std.mem.Allocator;
10 const BackendHandle = gpu.BackendHandle;
11 const Color = gui.model.UiColor;
12 const Command = command.Command;
13 const Executor = executor.Executor;
14 const ImageSet = command.ImageSet;
15 const Region = cpu.Region;
16 const Target = cpu.Target;
17 const PackedTarget = cpu.PackedTarget;
18
19 pub const CompositorOptions = struct {
20 prefer_accy: bool = true,
21 device_index: i32 = 0,
22 executor: executor.Options = .{},
23 };
24
25 pub const Kind = enum {
26 cpu,
27 accy,
28 };
29
30 pub const Fallback = struct {
31 init_error: ?anyerror = null,
32 render_error: ?anyerror = null,
33 render_fallbacks: usize = 0,
34 };
35
36 pub const Status = struct {
37 kind: Kind,
38 init_error: ?anyerror,
39 render_error: ?anyerror,
40 render_fallbacks: usize,
41 executor_storage: ?executor.StorageStatus,
42 };
43
44 pub const Compositor = struct {
45 engine: Engine = .cpu,
46 fallback: Fallback = .{},
47
48 pub fn init(allocator: Allocator, options: CompositorOptions) Compositor {
49 if (options.prefer_accy) {
50 if (AccyEngine.initVulkan(allocator, options.device_index, options.executor)) |engine| {
51 return .{ .engine = .{ .accy = engine } };
52 } else |err| {
53 return .{ .fallback = .{ .init_error = err } };
54 }
55 }
56 return initCpu();
57 }
58
59 pub fn initCpu() Compositor {
60 return .{};
61 }
62
63 pub fn initWithHandle(allocator: Allocator, handle: BackendHandle, options: executor.Options) !Compositor {
64 return .{ .engine = .{ .accy = try AccyEngine.initWithHandle(allocator, handle, options) } };
65 }
66
67 pub fn deinit(self: *Compositor) void {
68 switch (self.engine) {
69 .cpu => {},
70 .accy => |*engine| engine.deinit(),
71 }
72 self.* = .{};
73 }
74
75 pub fn kind(self: *const Compositor) Kind {
76 return switch (self.engine) {
77 .cpu => .cpu,
78 .accy => .accy,
79 };
80 }
81
82 pub fn status(self: *const Compositor) Status {
83 return .{
84 .kind = self.kind(),
85 .init_error = self.fallback.init_error,
86 .render_error = self.fallback.render_error,
87 .render_fallbacks = self.fallback.render_fallbacks,
88 .executor_storage = switch (self.engine) {
89 .cpu => null,
90 .accy => |*engine| engine.executor.storageStatus(),
91 },
92 };
93 }
94
95 fn noteRenderFallback(self: *Compositor, err: anyerror) void {
96 self.fallback.render_error = err;
97 self.fallback.render_fallbacks += 1;
98 }
99
100 pub fn renderCommands(self: *Compositor, commands: []const Command, target: Target, clear: Color) !void {
101 switch (self.engine) {
102 .cpu => try cpu.renderCommands(commands, target, clear),
103 .accy => |*engine| engine.executor.renderCommands(commands, target, clear) catch |err| {
104 self.noteRenderFallback(err);
105 try cpu.renderCommands(commands, target, clear);
106 },
107 }
108 }
109
110 pub fn renderCommandsWithImages(
111 self: *Compositor,
112 commands: []const Command,
113 target: Target,
114 clear: Color,
115 images: ImageSet,
116 ) !void {
117 switch (self.engine) {
118 .cpu => try cpu.renderCommandsWithImages(commands, target, clear, images),
119 .accy => |*engine| engine.executor.renderCommandsWithImages(commands, target, clear, images) catch |err| {
120 self.noteRenderFallback(err);
121 try cpu.renderCommandsWithImages(commands, target, clear, images);
122 },
123 }
124 }
125
126 pub fn renderCommandsRegionWithImages(
127 self: *Compositor,
128 commands: []const Command,
129 target: Target,
130 clear: Color,
131 images: ImageSet,
132 region: Region,
133 ) !void {
134 switch (self.engine) {
135 .cpu => try cpu.renderCommandsRegionWithImages(commands, target, clear, images, region),
136 .accy => |*engine| engine.executor.renderCommandsRegionWithImages(commands, target, clear, images, region) catch |err| {
137 self.noteRenderFallback(err);
138 try cpu.renderCommandsRegionWithImages(commands, target, clear, images, region);
139 },
140 }
141 }
142
143 pub fn renderCommandsPacked(self: *Compositor, commands: []const Command, target: PackedTarget, clear: Color) !void {
144 switch (self.engine) {
145 .cpu => try cpu.renderCommandsPacked(commands, target, clear),
146 .accy => |*engine| engine.executor.renderCommandsPacked(commands, target, clear) catch |err| {
147 self.noteRenderFallback(err);
148 try cpu.renderCommandsPacked(commands, target, clear);
149 },
150 }
151 }
152
153 pub fn renderCommandsPackedWithImages(
154 self: *Compositor,
155 commands: []const Command,
156 target: PackedTarget,
157 clear: Color,
158 images: ImageSet,
159 ) !void {
160 switch (self.engine) {
161 .cpu => try cpu.renderCommandsPackedWithImages(commands, target, clear, images),
162 .accy => |*engine| engine.executor.renderCommandsPackedWithImages(commands, target, clear, images) catch |err| {
163 self.noteRenderFallback(err);
164 try cpu.renderCommandsPackedWithImages(commands, target, clear, images);
165 },
166 }
167 }
168
169 pub fn renderCommandsPackedRegionWithImages(
170 self: *Compositor,
171 commands: []const Command,
172 target: PackedTarget,
173 clear: Color,
174 images: ImageSet,
175 region: Region,
176 ) !void {
177 switch (self.engine) {
178 .cpu => try cpu.renderCommandsPackedRegionWithImages(commands, target, clear, images, region),
179 .accy => |*engine| engine.executor.renderCommandsPackedRegionWithImages(commands, target, clear, images, region) catch |err| {
180 self.noteRenderFallback(err);
181 try cpu.renderCommandsPackedRegionWithImages(commands, target, clear, images, region);
182 },
183 }
184 }
185 };
186
187 const Engine = union(enum) {
188 cpu,
189 accy: AccyEngine,
190 };
191
192 const AccyEngine = struct {
193 allocator: Allocator,
194 executor: Executor,
195 vulkan_state: ?*gpu.vulkan.State = null,
196
197 fn initWithHandle(allocator: Allocator, handle: BackendHandle, options: executor.Options) !AccyEngine {
198 var paint_executor = try Executor.init(allocator, handle, options);
199 errdefer paint_executor.deinit();
200 return .{
201 .allocator = allocator,
202 .executor = paint_executor,
203 };
204 }
205
206 fn initVulkan(allocator: Allocator, device_index: i32, options: executor.Options) !AccyEngine {
207 const state = try allocator.create(gpu.vulkan.State);
208 errdefer allocator.destroy(state);
209
210 state.* = try gpu.vulkan.State.initDevice(allocator, device_index);
211 errdefer state.deinit();
212
213 var paint_executor = try Executor.init(allocator, state.handle(), options);
214 errdefer paint_executor.deinit();
215
216 return .{
217 .allocator = allocator,
218 .executor = paint_executor,
219 .vulkan_state = state,
220 };
221 }
222
223 fn deinit(self: *AccyEngine) void {
224 self.executor.deinit();
225 if (self.vulkan_state) |state| {
226 state.deinit();
227 self.allocator.destroy(state);
228 }
229 self.* = undefined;
230 }
231 };
232
233 test "paint compositor CPU path matches rgba8 renderer" {
234 const testing = std.testing;
235 var expected = @as([(8 * 4 * 4)]u8, @splat(0));
236 var actual = @as([(8 * 4 * 4)]u8, @splat(0));
237 const commands = [_]Command{
238 .{
239 .kind = .fill,
240 .rect = .{ .x = 1, .y = 1, .width = 4, .height = 2 },
241 .clip = .{ .x = 0, .y = 0, .width = 8, .height = 4 },
242 .color = .{ .r = 200, .g = 80, .b = 30, .a = 255 },
243 },
244 };
245 const clear = Color{ .r = 1, .g = 2, .b = 3, .a = 255 };
246
247 try cpu.renderCommandsWithImages(commands[0..], .{ .width = 8, .height = 4, .rgba8 = expected[0..] }, clear, .{});
248
249 var compositor = Compositor.initCpu();
250 defer compositor.deinit();
251 try compositor.renderCommandsWithImages(commands[0..], .{ .width = 8, .height = 4, .rgba8 = actual[0..] }, clear, .{});
252
253 try testing.expectEqualSlices(u8, expected[0..], actual[0..]);
254 }
255
256 test "paint compositor CPU region preserves packed pixels outside damage" {
257 const testing = std.testing;
258 var pixels = @as([16]u32, @splat(0xeeee_eeee));
259 const commands = [_]Command{
260 .{
261 .kind = .fill,
262 .rect = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
263 .clip = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
264 .color = .{ .r = 10, .g = 20, .b = 30, .a = 255 },
265 },
266 };
267 var compositor = Compositor.initCpu();
268 defer compositor.deinit();
269
270 try compositor.renderCommandsPackedRegionWithImages(commands[0..], .{
271 .width = 4,
272 .height = 4,
273 .pixels = pixels[0..],
274 }, .{ .r = 1, .g = 2, .b = 3, .a = 255 }, .{}, .{
275 .x = 1,
276 .y = 1,
277 .width = 2,
278 .height = 2,
279 });
280
281 const painted = cpu.packRgba(.{ .r = 10, .g = 20, .b = 30, .a = 255 });
282 try testing.expectEqual(@as(u32, 0xeeee_eeee), pixels[0]);
283 try testing.expectEqual(painted, pixels[5]);
284 try testing.expectEqual(painted, pixels[6]);
285 try testing.expectEqual(@as(u32, 0xeeee_eeee), pixels[15]);
286 }
287
288 test "paint compositor CPU status reports no fallback" {
289 var compositor = Compositor.initCpu();
290 defer compositor.deinit();
291 const state = compositor.status();
292 try std.testing.expectEqual(Kind.cpu, state.kind);
293 try std.testing.expectEqual(@as(?anyerror, null), state.init_error);
294 try std.testing.expectEqual(@as(?anyerror, null), state.render_error);
295 try std.testing.expectEqual(@as(usize, 0), state.render_fallbacks);
296 try std.testing.expectEqual(@as(?executor.StorageStatus, null), state.executor_storage);
297 }
298
299 test "paint compositor records render fallback and still paints through CPU" {
300 const testing = std.testing;
301 var state = gpu.recording.BackendState{
302 .allocator = testing.allocator,
303 .kind = .vulkan,
304 .format = .vulkan_spirv,
305 };
306 var compositor = try Compositor.initWithHandle(testing.allocator, state.handle(), .{});
307 defer compositor.deinit();
308 state.fail_launch_after_count = state.launch_count;
309
310 var expected = @as([16]u32, @splat(0xeeee_eeee));
311 var actual = @as([16]u32, @splat(0xeeee_eeee));
312 const commands = [_]Command{
313 .{
314 .kind = .fill,
315 .rect = .{ .x = 1, .y = 1, .width = 2, .height = 2 },
316 .clip = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
317 .color = .{ .r = 10, .g = 20, .b = 30, .a = 255 },
318 },
319 };
320 const clear = Color{ .r = 1, .g = 2, .b = 3, .a = 255 };
321
322 try cpu.renderCommandsPackedWithImages(commands[0..], .{ .width = 4, .height = 4, .pixels = expected[0..] }, clear, .{});
323 try compositor.renderCommandsPackedWithImages(commands[0..], .{ .width = 4, .height = 4, .pixels = actual[0..] }, clear, .{});
324
325 try testing.expectEqualSlices(u32, expected[0..], actual[0..]);
326 const after = compositor.status();
327 try testing.expectEqual(Kind.accy, after.kind);
328 try testing.expectEqual(@as(?anyerror, error.RuntimeUnavailable), after.render_error);
329 try testing.expectEqual(@as(usize, 1), after.render_fallbacks);
330
331 try compositor.renderCommandsPackedWithImages(commands[0..], .{ .width = 4, .height = 4, .pixels = actual[0..] }, clear, .{});
332 try testing.expectEqual(@as(usize, 2), compositor.status().render_fallbacks);
333 }
334
335 test "paint compositor launches Accy executor through backend handle" {
336 const testing = std.testing;
337 var state = gpu.recording.BackendState{
338 .allocator = testing.allocator,
339 .kind = .vulkan,
340 .format = .vulkan_spirv,
341 };
342 const executor_limits = executor.Limits{
343 .mode = .device_csr,
344 .commands = 1,
345 .pixels = 4,
346 .tiles = 1,
347 .tile_pairs = 1,
348 };
349 const image_limits = paint_image.Limits{ .dst_pixels = 4, .src_pixels = 4 };
350 var compositor = try Compositor.initWithHandle(testing.allocator, state.handle(), .{
351 .initial_storage = executor_limits,
352 .image_storage = image_limits,
353 });
354 defer compositor.deinit();
355
356 const initial_status = compositor.status().executor_storage.?;
357 try testing.expectEqual(executor_limits, initial_status.limits.?);
358 try testing.expectEqual(image_limits, initial_status.image_processor.?.limits.?);
359
360 var pixels = @as([16]u32, @splat(0xffff_ffff));
361 const commands = [_]Command{
362 .{
363 .kind = .fill,
364 .rect = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
365 .clip = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
366 .color = .{ .r = 255, .g = 0, .b = 0, .a = 255 },
367 },
368 };
369
370 try testing.expectEqual(Kind.accy, compositor.kind());
371 try compositor.renderCommandsPackedRegionWithImages(commands[0..], .{
372 .width = 4,
373 .height = 4,
374 .pixels = pixels[0..],
375 }, .{ .r = 0, .g = 0, .b = 0, .a = 255 }, .{}, .{
376 .x = 1,
377 .y = 1,
378 .width = 2,
379 .height = 2,
380 });
381
382 try testing.expectEqual(@as(usize, 7), state.launch_count);
383 try testing.expectEqual(@as(usize, 1), state.sync_count);
384 try testing.expectEqual(@as(usize, 1), state.read_count);
385 try testing.expectEqual(@as(u32, 1), state.last_launch_scalar_u32_values[5]);
386 try testing.expectEqual(@as(u32, 1), state.last_launch_scalar_u32_values[6]);
387 try testing.expectEqual(@as(u32, 2), state.last_launch_scalar_u32_values[7]);
388 try testing.expectEqual(@as(u32, 4), state.last_launch_scalar_u32_values[8]);
389 try testing.expectEqual(@as(u32, 0xffff_ffff), pixels[0]);
390 try testing.expectEqual(@as(u32, 0), pixels[5]);
391 try testing.expectEqual(@as(u32, 0), pixels[6]);
392 try testing.expectEqual(@as(u32, 0xffff_ffff), pixels[15]);
393 }