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 }