tiny.gui.paint.image
Defined in paint.
API (39)
Actions
Public operations.
Capacity.admitsCapacity.deriveImageHostStorage.activateImageHostStorage.deinitImageHostStorage.initProcessor.blurPassProcessor.deinitProcessor.expandShadowsAllocProcessor.initProcessor.launchBlurPassProcessor.launchResizeBilinearProcessor.resizeAllocProcessor.resizeBilinearProcessor.shadowAllocProcessor.storageStatusProcessor.thumbnailAllocResizedImage.deinitShadow.deinitShadowExpansion.deinitShadowExpansion.imageSetShadowExpansion.refreshReusableCommandsThumbnail.deinit
Types and contracts
Public types and contracts.
AxisCapacityImageHostStorageImageHostStorage.CapacityImageHostStorage.LimitsLimitsOptionsProcessorResizedImageShadowShadowExpansionShadowExpansionOptionsShadowOptionsStorageStatusThumbnailThumbnailOptions
Values and defaults
Public values and defaults.
Source
Source: lib/gui/src/paint/image.zig
zig
const std = @import("std");const gpu = @import("gpu");const choir_abi = @import("choir_abi");const accy = @import("accy");const alloc_phase = @import("alloc_phase");const command = @import("command.zig");const cpu = @import("cpu/root.zig");const Allocator = std.mem.Allocator;const library = accy.kernel.library;const image_library = library.image;const host_loop_launch_shape_arg_count: usize = choir_abi.launch_shape_argument_count;const runtime_argument_count_max: usize = 7;const scalar_argument_count_max: usize = runtime_argument_count_max + host_loop_launch_shape_arg_count;const Color = @import("../root.zig").model.UiColor;const Rect = @import("../root.zig").layout.Rect;const ImageThreads = @TypeOf(image_library.imageThreadsForExtents(1, 1));pub const Axis = image_library.Axis;const ImageLimits = struct { dst_pixels: usize = 0, src_pixels: usize = 0, scratch_pixels: usize = 0, weight_taps: usize = 0, fn merged(self: Limits, demand: Limits) Limits { return .{ .dst_pixels = @max(self.dst_pixels, demand.dst_pixels), .src_pixels = @max(self.src_pixels, demand.src_pixels), .scratch_pixels = @max(self.scratch_pixels, demand.scratch_pixels), .weight_taps = @max(self.weight_taps, demand.weight_taps), }; }};pub const Limits = ImageLimits;const ImageCapacity = struct { limits: Limits, host_storage_bytes: usize, device_storage_bytes: usize, device_buffer_count: usize, total_storage_bytes: usize, pub fn derive(limits: Limits) error{CapacityOverflow}!Capacity { const host_elements = std.math.add(usize, limits.dst_pixels, limits.weight_taps) catch return error.CapacityOverflow; const host_storage_bytes = std.math.mul(usize, host_elements, @sizeOf(u32)) catch return error.CapacityOverflow; var device_elements: usize = 0; var device_buffer_count: usize = 0; const counts = [_]usize{ limits.dst_pixels, limits.src_pixels, limits.scratch_pixels, limits.weight_taps }; for (counts) |count| { device_elements = std.math.add(usize, device_elements, count) catch return error.CapacityOverflow; device_buffer_count += @intFromBool(count != 0); } const device_storage_bytes = std.math.mul(usize, device_elements, @sizeOf(u32)) catch return error.CapacityOverflow; return .{ .limits = limits, .host_storage_bytes = host_storage_bytes, .device_storage_bytes = device_storage_bytes, .device_buffer_count = device_buffer_count, .total_storage_bytes = std.math.add(usize, host_storage_bytes, device_storage_bytes) catch return error.CapacityOverflow, }; } pub fn admits(self: Capacity, demand: Limits) bool { return self.limits.dst_pixels >= demand.dst_pixels and self.limits.src_pixels >= demand.src_pixels and self.limits.scratch_pixels >= demand.scratch_pixels and self.limits.weight_taps >= demand.weight_taps; }};pub const Capacity = ImageCapacity;pub const StorageStatus = struct { limits: ?Limits, capacity: ?Capacity, replacements: usize,};pub const Options = struct { artifact_format: ?gpu.ArtifactFormat = null, initial_storage: ?Limits = null,};const ImageLaunchScalars = struct { storage: [scalar_argument_count_max]choir_abi.ScalarArgument, count: usize, fn slice(self: *const ImageLaunchScalars) []const choir_abi.ScalarArgument { return self.storage[0..self.count]; }};fn imageLaunchScalars( entry: anytype, format: gpu.ArtifactFormat, geometry: choir_abi.LaunchGeometry, runtime_arguments: []const choir_abi.ScalarArgument,) !ImageLaunchScalars { if (runtime_arguments.len > runtime_argument_count_max) return error.LaunchArgumentMismatch; var result = ImageLaunchScalars{ .storage = undefined, .count = runtime_arguments.len }; @memcpy(result.storage[0..runtime_arguments.len], runtime_arguments); if (gpu.artifactFormatUsesHostLoopLaunch(format)) { if (entry.static_arguments.len != host_loop_launch_shape_arg_count) return error.InvalidArtifact; const shape = try choir_abi.launchShape(try geometry.threadCount(), geometry); try shape.scalarArguments(result.storage[result.count..]); result.count += host_loop_launch_shape_arg_count; } else { if (entry.static_arguments.len > host_loop_launch_shape_arg_count) return error.InvalidArtifact; @memcpy(result.storage[result.count..][0..entry.static_arguments.len], entry.static_arguments); result.count += entry.static_arguments.len; } return result;}pub const ShadowOptions = struct { image_index: u32 = 0, sigma: ?f32 = null,};pub const ShadowExpansionOptions = struct { sigma: ?f32 = null,};pub const ThumbnailOptions = struct { max_width: u32, max_height: u32,};pub const ResizedImage = struct { image: command.Image, pixels: []u32, pub fn deinit(self: *ResizedImage, allocator: Allocator) void { allocator.free(self.pixels); self.* = undefined; }};pub const Thumbnail = ResizedImage;pub const Shadow = struct { command: command.Command, image: command.Image, pixels: []u32, pub fn deinit(self: *Shadow, allocator: Allocator) void { allocator.free(self.pixels); self.* = undefined; }};pub const ShadowExpansion = struct { allocator: Allocator, commands: []const command.Command, image_entries: []const command.Image, shadows: []Shadow, pub fn imageSet(self: *const ShadowExpansion) command.ImageSet { return .{ .images = self.image_entries }; } pub fn refreshReusableCommands(self: *ShadowExpansion, commands: []const command.Command) !void { if (commands.len != self.commands.len) return error.InvalidShadowCommand; const rewritten = @constCast(self.commands); var shadow_index: usize = 0; for (commands, 0..) |paint, index| { if (paint.kind == .shadow) { if (shadow_index >= self.shadows.len) return error.InvalidShadowCommand; tintShadowPixels(self.shadows[shadow_index].pixels, paint.color); rewritten[index] = self.shadows[shadow_index].command; shadow_index += 1; } else { rewritten[index] = paint; } } if (shadow_index != self.shadows.len) return error.InvalidShadowCommand; } pub fn deinit(self: *ShadowExpansion) void { for (self.shadows) |*shadow| shadow.deinit(self.allocator); self.allocator.free(@constCast(self.commands)); self.allocator.free(@constCast(self.image_entries)); self.allocator.free(self.shadows); self.* = undefined; }};pub const Processor = struct { allocator: Allocator, handle: gpu.BackendHandle, format: gpu.ArtifactFormat, kernels: std.ArrayListUnmanaged(CachedImageKernel) = .empty, buffers: ImageBuffers = .{}, shadow_alpha: ?CachedShadowAlpha = null, storage_replacements: usize = 0, pub fn init(allocator: Allocator, handle: gpu.BackendHandle, options: Options) !Processor { var result = Processor{ .allocator = allocator, .handle = handle, .format = options.artifact_format orelse try defaultFormat(handle), }; if (options.initial_storage) |limits| { result.buffers = try ImageBuffers.init(allocator, handle, limits); } return result; } pub fn deinit(self: *Processor) void { if (self.shadow_alpha) |*cached| cached.deinit(self.allocator); self.buffers.deinit(self.allocator, self.handle); for (self.kernels.items) |*cached| cached.deinit(self.handle); self.kernels.deinit(self.allocator); self.* = undefined; } pub fn storageStatus(self: *const Processor) StorageStatus { return .{ .limits = if (self.buffers.capacity) |capacity| capacity.limits else null, .capacity = self.buffers.capacity, .replacements = self.storage_replacements, }; } pub fn blurPass( self: *Processor, dst: []u32, src: []const u32, width: u32, height: u32, radius: u32, sigma: f32, axis: Axis, ) !void { const pixels = try validateImageSlices(dst, src, width, height); const taps = try blurTapCount(radius); try self.ensureStorage(.{ .dst_pixels = pixels, .src_pixels = pixels, .weight_taps = taps, }); const dst_buffer = self.buffers.dst.?.handle; const src_buffer = self.buffers.src.?.handle; const weights_buffer = try self.buffers.ensureWeightsData(self.handle, radius, sigma); try self.handle.writeBuffer(.{ .handle = src_buffer, .bytes = std.mem.sliceAsBytes(src[0..pixels]), }); try self.launchBlurPass(dst_buffer, src_buffer, weights_buffer, width, height, radius, axis); try self.buffers.readPixels(self.handle, dst_buffer, dst, pixels); } pub fn resizeBilinear( self: *Processor, dst: []u32, src: []const u32, dst_width: u32, dst_height: u32, src_width: u32, src_height: u32, ) !void { const dst_pixels = try pixelCount(dst_width, dst_height); const src_pixels = try pixelCount(src_width, src_height); if (dst.len < dst_pixels or src.len < src_pixels) return error.BufferTooSmall; try self.ensureStorage(.{ .dst_pixels = dst_pixels, .src_pixels = src_pixels, }); const dst_buffer = self.buffers.dst.?.handle; const src_buffer = self.buffers.src.?.handle; try self.handle.writeBuffer(.{ .handle = src_buffer, .bytes = std.mem.sliceAsBytes(src[0..src_pixels]), }); try self.launchResizeBilinear(dst_buffer, src_buffer, dst_width, dst_height, src_width, src_height); try self.buffers.readPixels(self.handle, dst_buffer, dst, dst_pixels); } pub fn resizeAlloc( self: *Processor, source: command.Image, width: u32, height: u32, ) !ResizedImage { try source.validate(); const pixels = try pixelCount(width, height); const dst = try self.allocator.alloc(u32, pixels); errdefer self.allocator.free(dst); try self.resizeBilinear(dst, source.pixels, width, height, source.width, source.height); return .{ .image = .{ .width = width, .height = height, .pixels = dst, }, .pixels = dst, }; } pub fn thumbnailAlloc( self: *Processor, source: command.Image, options: ThumbnailOptions, ) !Thumbnail { try source.validate(); const extent = try thumbnailExtent(source.width, source.height, options.max_width, options.max_height); if (extent.width == source.width and extent.height == source.height) { const pixels = try pixelCount(source.width, source.height); const dst = try self.allocator.dupe(u32, source.pixels[0..pixels]); errdefer self.allocator.free(dst); return .{ .image = .{ .width = source.width, .height = source.height, .pixels = dst, }, .pixels = dst, }; } return self.resizeAlloc(source, extent.width, extent.height); } pub fn shadowAlloc(self: *Processor, paint: command.Command, options: ShadowOptions) !Shadow { const plan = try planShadow(paint, options); const pixels = try self.allocator.alloc(u32, plan.pixels); errdefer self.allocator.free(pixels); if (self.useShadowAlphaCache(plan.key, pixels)) { tintShadowPixels(pixels, paint.color); return plan.shadow(pixels); } const mask = try self.allocator.alloc(u32, plan.pixels); defer self.allocator.free(mask); rasterShadowMask(mask, plan.frame, paint); if (plan.blur_radius > 0) { try self.blurTwoPass( pixels, mask, plan.width, plan.height, plan.blur_radius, plan.sigma, ); } else { @memcpy(pixels, mask); } try self.storeShadowAlpha(plan.key, pixels); tintShadowPixels(pixels, paint.color); return plan.shadow(pixels); } pub fn expandShadowsAlloc( self: *Processor, commands: []const command.Command, images: command.ImageSet, options: ShadowExpansionOptions, ) !ShadowExpansion { const shadow_count = countShadows(commands); const image_count = std.math.add(usize, images.images.len, shadow_count) catch return error.ImageCountTooLarge; if (image_count > std.math.maxInt(u32)) return error.ImageCountTooLarge; const rewritten = try self.allocator.alloc(command.Command, commands.len); errdefer self.allocator.free(rewritten); const image_entries = try self.allocator.alloc(command.Image, image_count); errdefer self.allocator.free(image_entries); if (images.images.len != 0) @memcpy(image_entries[0..images.images.len], images.images); const shadows = try self.allocator.alloc(Shadow, shadow_count); var shadow_init: usize = 0; errdefer { for (shadows[0..shadow_init]) |*shadow| shadow.deinit(self.allocator); self.allocator.free(shadows); } var generated_index: usize = 0; for (commands, 0..) |paint, index| { if (paint.kind == .shadow) { const image_index_usize = images.images.len + generated_index; const image_index: u32 = @intCast(image_index_usize); const generated = try self.shadowAlloc(paint, .{ .image_index = image_index, .sigma = options.sigma, }); shadows[generated_index] = generated; shadow_init += 1; image_entries[image_index_usize] = generated.image; rewritten[index] = generated.command; generated_index += 1; } else { rewritten[index] = paint; } } return .{ .allocator = self.allocator, .commands = rewritten, .image_entries = image_entries, .shadows = shadows, }; } fn blurTwoPass( self: *Processor, dst: []u32, src: []const u32, width: u32, height: u32, radius: u32, sigma: f32, ) !void { const pixels = try validateImageSlices(dst, src, width, height); const taps = try blurTapCount(radius); try self.ensureStorage(.{ .dst_pixels = pixels, .src_pixels = pixels, .scratch_pixels = pixels, .weight_taps = taps, }); const dst_buffer = self.buffers.dst.?.handle; const scratch_buffer = self.buffers.scratch.?.handle; const src_buffer = self.buffers.src.?.handle; const weights_buffer = try self.buffers.ensureWeightsData(self.handle, radius, sigma); try self.handle.writeBuffer(.{ .handle = src_buffer, .bytes = std.mem.sliceAsBytes(src[0..pixels]), }); try self.queueBlurPass(scratch_buffer, src_buffer, weights_buffer, width, height, radius, .horizontal); errdefer self.handle.synchronize(.{ .scope = .device }) catch {}; try self.queueBlurPass(dst_buffer, scratch_buffer, weights_buffer, width, height, radius, .vertical); try self.handle.synchronize(.{ .scope = .device }); try self.buffers.readPixels(self.handle, dst_buffer, dst, pixels); } fn ensureStorage(self: *Processor, demand: Limits) !void { if (self.buffers.capacity) |capacity| { if (capacity.admits(demand)) return; } const next_limits = if (self.buffers.capacity) |capacity| capacity.limits.merged(demand) else demand; const next = try ImageBuffers.init(self.allocator, self.handle, next_limits); const replacing = self.buffers.capacity != null; self.buffers.deinit(self.allocator, self.handle); self.buffers = next; if (replacing) self.storage_replacements += 1; } pub fn launchBlurPass( self: *Processor, dst: gpu.BufferHandle, src: gpu.BufferHandle, weights: gpu.BufferHandle, width: u32, height: u32, radius: u32, axis: Axis, ) !void { try self.queueBlurPass(dst, src, weights, width, height, radius, axis); try self.handle.synchronize(.{ .scope = .device }); } fn queueBlurPass( self: *Processor, dst: gpu.BufferHandle, src: gpu.BufferHandle, weights: gpu.BufferHandle, width: u32, height: u32, radius: u32, axis: Axis, ) !void { const pixels = try pixelCount(width, height); const taps = try blurTapCount(radius); try expectBufferSize(dst, pixels, @sizeOf(u32)); try expectBufferSize(src, pixels, @sizeOf(u32)); try expectBufferSize(weights, taps, @sizeOf(f32)); const runtime_arguments = try blurRuntimeArguments(width, height); const bindings = [_]gpu.BufferBinding{ bufferBinding(dst, .read_write), bufferBinding(src, .read_only), bufferBinding(weights, .read_only), }; try self.queueCachedKernel(.{ .blur_pass = .{ .width = width, .height = height, .radius = radius, .axis = axis, .threads = image_library.imageThreadsForExtents(width, height), } }, bindings[0..], runtime_arguments[0..], "gui/paint/image/blur-pass"); } pub fn launchResizeBilinear( self: *Processor, dst: gpu.BufferHandle, src: gpu.BufferHandle, dst_width: u32, dst_height: u32, src_width: u32, src_height: u32, ) !void { try expectBufferSize(dst, try pixelCount(dst_width, dst_height), @sizeOf(u32)); try expectBufferSize(src, try pixelCount(src_width, src_height), @sizeOf(u32)); const runtime_arguments = try resizeRuntimeArguments(dst_width, dst_height, src_width, src_height); const bindings = [_]gpu.BufferBinding{ bufferBinding(dst, .read_write), bufferBinding(src, .read_only), }; try self.launchCachedKernel(.{ .resize_bilinear = .{ .dst_width = dst_width, .dst_height = dst_height, .src_width = src_width, .src_height = src_height, .threads = image_library.imageThreadsForExtents(dst_width, dst_height), } }, bindings[0..], runtime_arguments[0..], "gui/paint/image/resize-bilinear"); } fn launchCachedKernel( self: *Processor, key: ImageKernelKey, bindings: []const gpu.BufferBinding, runtime_arguments: []const choir_abi.ScalarArgument, diagnostic_id: []const u8, ) !void { try self.queueCachedKernel(key, bindings, runtime_arguments, diagnostic_id); try self.handle.synchronize(.{ .scope = .device }); } fn queueCachedKernel( self: *Processor, key: ImageKernelKey, bindings: []const gpu.BufferBinding, runtime_arguments: []const choir_abi.ScalarArgument, diagnostic_id: []const u8, ) !void { const cached = try self.cachedKernel(key, diagnostic_id); const entry = cached.call_artifact.entry(); if (entry.element_count_argument != .none) return error.UnsupportedImageOperation; const expected_runtime_scalar_count: usize = @intCast(entry.runtime_scalar_argument_count); if (runtime_arguments.len != expected_runtime_scalar_count) return error.LaunchArgumentMismatch; const geometry = try accy.kernel.kernelCallEntryLaunchGeometry(entry, runtime_arguments); const scalars = try imageLaunchScalars(entry, cached.artifact.format, geometry, runtime_arguments); try self.handle.launch(.{ .artifact = &cached.artifact, .loaded_artifact = cached.loaded, .buffers = bindings, .scalar_arguments = scalars.slice(), .geometry = geometry, .diagnostic_id = diagnostic_id, }); } fn cachedKernel(self: *Processor, key: ImageKernelKey, diagnostic_id: []const u8) !*CachedImageKernel { for (self.kernels.items) |*cached| { if (cached.key.eql(key)) return cached; } var descriptor = (try library.selectOwned(self.allocator, .{ .image = key.query() })) orelse return error.UnsupportedImageOperation; defer descriptor.deinit(); var call_artifact = try library.createOwnedKernelCallArtifact(self.allocator, self.handle, descriptor, .{ .limits = accy.kernel.Limits.standard, .format = self.format, }); errdefer call_artifact.deinit(); var artifact = try accy.kernel.createBackendArtifactFromKernelCallEntry( self.allocator, self.handle, call_artifact.entry(), diagnostic_id, ); errdefer artifact.deinit(); const loaded = try self.handle.loadArtifact(&artifact); errdefer self.handle.destroyObject(loaded.id); try self.kernels.append(self.allocator, .{ .key = key, .call_artifact = call_artifact, .artifact = artifact, .loaded = loaded, }); return &self.kernels.items[self.kernels.items.len - 1]; } fn useShadowAlphaCache(self: *Processor, key: ShadowAlphaKey, dst: []u32) bool { if (self.shadow_alpha) |*cached| { if (cached.matches(key, dst.len)) { @memcpy(dst, cached.pixels); return true; } } return false; } fn storeShadowAlpha(self: *Processor, key: ShadowAlphaKey, pixels: []const u32) !void { if (self.shadow_alpha) |*cached| { if (cached.pixels.len == pixels.len) { cached.key = key; @memcpy(cached.pixels, pixels); return; } cached.deinit(self.allocator); self.shadow_alpha = null; } const copy = try self.allocator.dupe(u32, pixels); self.shadow_alpha = .{ .key = key, .pixels = copy, }; }};const CachedShadowAlpha = struct { key: ShadowAlphaKey, pixels: []u32, fn deinit(self: *CachedShadowAlpha, allocator: Allocator) void { allocator.free(self.pixels); self.* = undefined; } fn matches(self: *const CachedShadowAlpha, key: ShadowAlphaKey, pixel_count: usize) bool { return self.pixels.len == pixel_count and self.key.eql(key); }};const ImageKernelKey = union(enum) { blur_pass: struct { width: u32, height: u32, radius: u32, axis: Axis, threads: ImageThreads, }, resize_bilinear: struct { dst_width: u32, dst_height: u32, src_width: u32, src_height: u32, threads: ImageThreads, }, fn eql(self: ImageKernelKey, other: ImageKernelKey) bool { return switch (self) { .blur_pass => |lhs| switch (other) { .blur_pass => |rhs| lhs.width == rhs.width and lhs.height == rhs.height and lhs.radius == rhs.radius and lhs.axis == rhs.axis and threadsEql(lhs.threads, rhs.threads), else => false, }, .resize_bilinear => |lhs| switch (other) { .resize_bilinear => |rhs| lhs.dst_width == rhs.dst_width and lhs.dst_height == rhs.dst_height and lhs.src_width == rhs.src_width and lhs.src_height == rhs.src_height and threadsEql(lhs.threads, rhs.threads), else => false, }, }; } fn query(self: ImageKernelKey) library.ImageQuery { return switch (self) { .blur_pass => |key| .{ .dtype = .u32, .kind = .{ .blur_pass = .{ .radius = key.radius, .axis = key.axis } }, .width = key.width, .height = key.height, .schedule = .{ .thread_blocks = key.threads }, }, .resize_bilinear => |key| .{ .dtype = .u32, .kind = .{ .resize_bilinear = .{ .src_width = key.src_width, .src_height = key.src_height } }, .width = key.dst_width, .height = key.dst_height, .schedule = .{ .thread_blocks = key.threads }, }, }; }};const CachedImageKernel = struct { key: ImageKernelKey, call_artifact: accy.kernel.OwnedKernelCallArtifact, artifact: gpu.KernelArtifact, loaded: gpu.LoadedArtifact, fn deinit(self: *CachedImageKernel, handle: gpu.BackendHandle) void { handle.destroyObject(self.loaded.id); self.artifact.deinit(); self.call_artifact.deinit(); self.* = undefined; }};pub const ImageHostStorage = struct { pub const Limits = ImageLimits; pub const Capacity = ImageCapacity; pub const claim: alloc_phase.capacity.Declaration = .{ .source = .{ .id = "gui.paint_image_host_storage", .kind = .phase_static, .limit_source = .caller, .storage = .{ .covered = &.{ .{ .id = "caller_sized_image_readback_staging", .lifetime = .steady, .detail = "caller-sized image readback staging", }, .{ .id = "caller_sized_gaussian_weight_staging", .lifetime = .steady, .detail = "caller-sized Gaussian weight staging", }, }, .excluded = &.{ "destination source scratch and weight backend buffers and their foreign allocation", "caller-owned source destination and allocating result pixels", "shadow mask output expansion and retained alpha cache pixels", "compiled loaded and indexed image-family kernel cache artifacts", "transactional complete-epoch replacement by Processor", }, }, .capacity = .{ .inputs = &.{ alloc_phase.capacity.bindInput(ImageLimits, "dst_pixels", "dst_pixels"), alloc_phase.capacity.bindInput(ImageLimits, "weight_taps", "weight_taps"), }, .type_selectors = &.{}, .nodes = &.{ .{ .input = 0 }, .{ .input = 1 }, .{ .add = .{ .left = 0, .right = 1 } }, .{ .scale = .{ .node = 2, .coefficient = .{ .literal = 4 } } }, }, .assertions = &.{.{ .scope = .closure_total, .measure = .retained, .relation = .exact, .expression = 3, }}, }, .overload = .{ .kind = .reject_before_seal, .detail = "checked capacity derivation and allocation failure reject before host storage activation; Processor may acquire a separate larger epoch", }, .risks = .{ .transitive = .{ .status = .open, .detail = "weight generation readback and backend transfer helpers are exercised but lack a machine-checked call-graph closure certificate", }, .foreign = .{ .status = .open, .detail = "backend buffer acquisition is capacity-accounted by Processor but remains outside this host-storage claim", }, }, .obligations = &.{ .{ .key = "gui_paint_image_capacity_capacity_model", .role = .capacity_model }, .{ .key = "gui_paint_image_capacity_overload", .role = .overload }, .{ .key = "gui_paint_image_acquisition", .role = .custom }, .{ .key = "gui_paint_image_host_oom", .role = .overload }, .{ .key = "gui_paint_image_boundary", .role = .custom }, .{ .key = "gui_paint_image_atomic", .role = .custom }, .{ .key = "gui_paint_image_steady", .role = .custom }, .{ .key = "gui_paint_image_scalars", .role = .custom }, }, }, .bindings = .{ .owner = @This(), .seal = .{ .family = alloc_phase.capacity.selector(@This().activate), .premise = .{ .class = .checked_semantic_fact, .authority = .checker, }, }, .teardown = .{ .family = alloc_phase.capacity.selector(@This().deinit), .premise = .{ .class = .checked_semantic_fact, .authority = .checker, }, }, }, }; phase: alloc_phase.capacity.Phase, capacity: ImageCapacity, limits: ImageLimits, bytes: []align(@alignOf(u32)) u8, pub fn init(allocator: Allocator, limits: ImageLimits) !ImageHostStorage { const capacity = try ImageCapacity.derive(limits); const bytes = if (capacity.host_storage_bytes == 0) @as([]align(@alignOf(u32)) u8, &.{}) else try allocator.alignedAlloc( u8, .fromByteUnits(@alignOf(u32)), capacity.host_storage_bytes, ); return .{ .phase = .initialization, .capacity = capacity, .limits = limits, .bytes = bytes, }; } pub fn activate(self: *ImageHostStorage) void { std.debug.assert(self.phase == .initialization); std.debug.assert(std.meta.eql(self.capacity.limits, self.limits)); std.debug.assert(self.bytes.len == self.capacity.host_storage_bytes); self.phase = .steady; } pub fn deinit(self: *ImageHostStorage, allocator: Allocator) void { std.debug.assert(self.phase != .teardown); self.phase = .teardown; allocator.free(self.bytes); self.* = undefined; }};comptime { alloc_phase.capacity.requireAllocatorExactOwnerShape(ImageHostStorage);}const ImageBuffers = struct { capacity: ?Capacity = null, host: ?ImageHostStorage = null, dst: ?RetainedDeviceBuffer = null, src: ?RetainedDeviceBuffer = null, scratch: ?RetainedDeviceBuffer = null, weights: ?RetainedDeviceBuffer = null, weights_valid: bool = false, weights_radius: u32 = 0, weights_sigma: f32 = 0, readback: []u32 = &.{}, weight_staging: []f32 = &.{}, fn init(allocator: Allocator, handle: gpu.BackendHandle, limits: Limits) !ImageBuffers { var host_storage = try ImageHostStorage.init(allocator, limits); errdefer host_storage.deinit(allocator); const capacity = host_storage.capacity; var host = ImageHostCursor{ .bytes = host_storage.bytes }; const readback = host.take(u32, limits.dst_pixels); const weight_staging = host.take(f32, limits.weight_taps); std.debug.assert(host.offset == host_storage.bytes.len); var dst: ?RetainedDeviceBuffer = null; errdefer if (dst) |*buffer| buffer.deinit(handle); var src: ?RetainedDeviceBuffer = null; errdefer if (src) |*buffer| buffer.deinit(handle); var scratch: ?RetainedDeviceBuffer = null; errdefer if (scratch) |*buffer| buffer.deinit(handle); var weights: ?RetainedDeviceBuffer = null; errdefer if (weights) |*buffer| buffer.deinit(handle); if (limits.dst_pixels != 0) dst = try retainedDeviceBuffer(handle, u32, .u32, limits.dst_pixels); if (limits.src_pixels != 0) src = try retainedDeviceBuffer(handle, u32, .u32, limits.src_pixels); if (limits.scratch_pixels != 0) scratch = try retainedDeviceBuffer(handle, u32, .u32, limits.scratch_pixels); if (limits.weight_taps != 0) weights = try retainedDeviceBuffer(handle, f32, .f32, limits.weight_taps); host_storage.activate(); return .{ .capacity = capacity, .host = host_storage, .dst = dst, .src = src, .scratch = scratch, .weights = weights, .readback = readback, .weight_staging = weight_staging, }; } fn deinit(self: *ImageBuffers, allocator: Allocator, handle: gpu.BackendHandle) void { if (self.dst) |*buffer| buffer.deinit(handle); if (self.src) |*buffer| buffer.deinit(handle); if (self.scratch) |*buffer| buffer.deinit(handle); if (self.weights) |*buffer| buffer.deinit(handle); if (self.host) |*host| host.deinit(allocator); self.* = .{}; } fn ensureWeightsData( self: *ImageBuffers, handle: gpu.BackendHandle, radius: u32, sigma: f32, ) !gpu.BufferHandle { _ = try blurTapCount(radius); const weights = self.weights orelse return error.BufferTooSmall; if (self.weights_valid and self.weights_radius == radius and self.weights_sigma == sigma) { return weights.handle; } self.weights_valid = false; const weights_data = try image_library.gaussianWeights(self.weight_staging, radius, sigma); try handle.writeBuffer(.{ .handle = weights.handle, .bytes = std.mem.sliceAsBytes(weights_data), }); self.weights_valid = true; self.weights_radius = radius; self.weights_sigma = sigma; return weights.handle; } fn readPixels( self: *ImageBuffers, handle: gpu.BackendHandle, source: gpu.BufferHandle, dst: []u32, count: usize, ) !void { const retained_count = source.byte_size / @sizeOf(u32); if (self.readback.len < retained_count) return error.BufferTooSmall; try handle.readBuffer(.{ .handle = source, .bytes = std.mem.sliceAsBytes(self.readback[0..retained_count]), }); @memcpy(dst[0..count], self.readback[0..count]); }};const ImageHostCursor = struct { bytes: []align(@alignOf(u32)) u8, offset: usize = 0, fn take(self: *ImageHostCursor, comptime T: type, count: usize) []T { comptime std.debug.assert(@sizeOf(T) == @sizeOf(u32)); comptime std.debug.assert(@alignOf(T) <= @alignOf(u32)); const byte_count = count * @sizeOf(T); std.debug.assert(self.offset + byte_count <= self.bytes.len); const pointer: [*]T = @ptrCast(@alignCast(self.bytes.ptr + self.offset)); self.offset += byte_count; return pointer[0..count]; }};const RetainedDeviceBuffer = struct { handle: gpu.BufferHandle, element_count: usize, fn deinit(self: *RetainedDeviceBuffer, handle: gpu.BackendHandle) void { handle.destroyObject(self.handle.id); self.* = undefined; }};fn retainedDeviceBuffer( handle: gpu.BackendHandle, comptime T: type, dtype: choir_abi.DType, count: usize,) !RetainedDeviceBuffer { const next = try allocateDeviceBuffer(handle, T, dtype, count); return .{ .handle = next, .element_count = count, };}fn threadsEql(lhs: ImageThreads, rhs: ImageThreads) bool { return lhs.x == rhs.x and lhs.y == rhs.y;}fn countShadows(commands: []const command.Command) usize { var count: usize = 0; for (commands) |paint| { if (paint.kind == .shadow) count += 1; } return count;}const ShadowPlan = struct { command: command.Command, frame: ShadowFrame, pixels: usize, width: u32, height: u32, blur_radius: u32, sigma: f32, key: ShadowAlphaKey, fn shadow(self: ShadowPlan, pixels: []u32) Shadow { return .{ .command = self.command, .image = .{ .width = self.width, .height = self.height, .pixels = pixels, }, .pixels = pixels, }; }};const PreparedShadow = struct { plan: ShadowPlan, mask: []u32, fn deinitMask(self: *const PreparedShadow, allocator: Allocator) void { allocator.free(self.mask); } fn deinit(self: PreparedShadow, allocator: Allocator) void { allocator.free(self.mask); } fn shadow(self: PreparedShadow, pixels: []u32) Shadow { return self.plan.shadow(pixels); }};fn defaultFormat(handle: gpu.BackendHandle) !gpu.ArtifactFormat { const kind = handle.backendKind() orelse (try handle.queryCapabilities()).identity.backend; return switch (kind) { .cuda => .cuda_ptx, .vulkan => .vulkan_spirv, .metal => .metal_msl, .webgpu => .webgpu_wgsl, .cpu => .cpu_object, .wasm => .webassembly_module, .external => error.UnsupportedArtifactFormat, };}fn planShadow(paint: command.Command, options: ShadowOptions) !ShadowPlan { if (paint.kind != .shadow) return error.InvalidShadowCommand; const blur_radius = try shadowBlurRadius(paint.width); const sigma = try shadowSigma(blur_radius, options.sigma); const frame = try shadowFrame(paint, blur_radius); const pixels = try pixelCount(frame.width, frame.height); return .{ .command = shadowImageCommand(paint, frame, options.image_index), .frame = frame, .pixels = pixels, .width = frame.width, .height = frame.height, .blur_radius = blur_radius, .sigma = sigma, .key = ShadowAlphaKey.init(paint, frame, blur_radius, sigma), };}fn prepareShadow(allocator: Allocator, paint: command.Command, options: ShadowOptions) !PreparedShadow { const plan = try planShadow(paint, options); const mask = try allocator.alloc(u32, plan.pixels); errdefer allocator.free(mask); rasterShadowMask(mask, plan.frame, paint); return .{ .plan = plan, .mask = mask, };}const ShadowFrame = struct { rect: Rect, width: u32, height: u32,};const ShadowAlphaKey = struct { paint_rect: Rect, frame_rect: Rect, radius: f32, alpha: u8, width: u32, height: u32, blur_radius: u32, sigma: f32, fn init(paint: command.Command, frame: ShadowFrame, blur_radius: u32, sigma: f32) ShadowAlphaKey { return .{ .paint_rect = paint.rect, .frame_rect = frame.rect, .radius = paint.radius, .alpha = paint.color.a, .width = frame.width, .height = frame.height, .blur_radius = blur_radius, .sigma = sigma, }; } fn eql(self: ShadowAlphaKey, other: ShadowAlphaKey) bool { return rectEqual(self.paint_rect, other.paint_rect) and rectEqual(self.frame_rect, other.frame_rect) and self.radius == other.radius and self.alpha == other.alpha and self.width == other.width and self.height == other.height and self.blur_radius == other.blur_radius and self.sigma == other.sigma; }};fn rectEqual(left: Rect, right: Rect) bool { return left.x == right.x and left.y == right.y and left.width == right.width and left.height == right.height;}fn shadowFrame(paint: command.Command, blur_radius: u32) !ShadowFrame { if (!shadowRectFinite(paint.rect) or !shadowRectFinite(paint.clip)) return error.InvalidShadowCommand; if (paint.rect.width <= 0 or paint.rect.height <= 0) return error.InvalidShadowCommand; const margin: f32 = @floatFromInt(blur_radius); const rect = Rect{ .x = paint.rect.x - margin, .y = paint.rect.y - margin, .width = paint.rect.width + margin * 2, .height = paint.rect.height + margin * 2, }; return .{ .rect = rect, .width = try extentFromFloat(rect.width), .height = try extentFromFloat(rect.height), };}fn shadowRectFinite(rect: Rect) bool { return std.math.isFinite(rect.x) and std.math.isFinite(rect.y) and std.math.isFinite(rect.width) and std.math.isFinite(rect.height);}fn extentFromFloat(value: f32) !u32 { if (!std.math.isFinite(value) or value <= 0) return error.InvalidImage; const ceiled = @ceil(value); if (ceiled > @as(f32, @floatFromInt(std.math.maxInt(u32)))) return error.DimensionsTooLarge; return @intFromFloat(ceiled);}fn shadowBlurRadius(width: f32) !u32 { if (std.math.isNan(width)) return error.InvalidShadowCommand; if (width <= 0) return 0; if (!std.math.isFinite(width)) return error.InvalidShadowCommand; const ceiled = @ceil(width); if (ceiled > @as(f32, @floatFromInt(image_library.blur_radius_max))) return error.UnsupportedImageOperation; return @intFromFloat(ceiled);}fn shadowSigma(radius: u32, override: ?f32) !f32 { if (override) |value| { if (!(value > 0) or !std.math.isFinite(value)) return error.UnsupportedImageOperation; return value; } if (radius == 0) return 1; return @max(@as(f32, @floatFromInt(radius)) * 0.5, @as(f32, 0.5));}fn rasterShadowMask(dst: []u32, frame: ShadowFrame, paint: command.Command) void { var y: u32 = 0; while (y < frame.height) : (y += 1) { var x: u32 = 0; while (x < frame.width) : (x += 1) { var coverage: u32 = 0; inline for (.{ -0.25, 0.25 }) |dy| { inline for (.{ -0.25, 0.25 }) |dx| { const px = frame.rect.x + @as(f32, @floatFromInt(x)) + 0.5 + dx; const py = frame.rect.y + @as(f32, @floatFromInt(y)) + 0.5 + dy; if (cpu.geometry.insideRounded(paint.rect, @max(paint.radius, 0), px, py)) coverage += 1; } } const alpha = cpu.pixel.coverageAlpha(paint.color.a, coverage); dst[@as(usize, y) * frame.width + x] = cpu.pixel.packRgba(.{ .a = alpha }); } }}fn tintShadowPixels(pixels: []u32, color: Color) void { for (pixels) |*pixel_value| { const alpha: u8 = @truncate(pixel_value.* >> 24); pixel_value.* = cpu.pixel.packRgba(.{ .r = color.r, .g = color.g, .b = color.b, .a = alpha, }); }}fn shadowImageCommand(paint: command.Command, frame: ShadowFrame, image_index: u32) command.Command { return .{ .kind = .image, .rect = .{ .x = frame.rect.x, .y = frame.rect.y, .width = @floatFromInt(frame.width), .height = @floatFromInt(frame.height), }, .clip = paint.clip, .color = .{ .r = 255, .g = 255, .b = 255, .a = 255 }, .image_index = image_index, .order = paint.order, };}fn validateImageSlices(dst: []u32, src: []const u32, width: u32, height: u32) !usize { const source = command.Image{ .width = width, .height = height, .pixels = src }; try source.validate(); const pixels = try pixelCount(width, height); if (dst.len < pixels) return error.BufferTooSmall; return pixels;}fn pixelCount(width: u32, height: u32) !usize { if (width == 0 or height == 0) return error.InvalidImage; return std.math.mul(usize, @as(usize, width), @as(usize, height)) catch return error.DimensionsTooLarge;}const ImageExtent = struct { width: u32, height: u32,};fn thumbnailExtent( src_width: u32, src_height: u32, max_width: u32, max_height: u32,) !ImageExtent { if (src_width == 0 or src_height == 0 or max_width == 0 or max_height == 0) return error.InvalidImage; if (src_width <= max_width and src_height <= max_height) { return .{ .width = src_width, .height = src_height }; } const width_limited = @as(u64, max_width) * @as(u64, src_height) <= @as(u64, max_height) * @as(u64, src_width); if (width_limited) { return .{ .width = max_width, .height = scaledExtent(src_height, max_width, src_width), }; } return .{ .width = scaledExtent(src_width, max_height, src_height), .height = max_height, };}fn scaledExtent(value: u32, numerator: u32, denominator: u32) u32 { const scaled = (@as(u64, value) * @as(u64, numerator)) / @as(u64, denominator); return @intCast(@max(scaled, 1));}fn blurTapCount(radius: u32) !usize { if (radius == 0 or radius > image_library.blur_radius_max) return error.UnsupportedImageOperation; return @intCast(radius * 2 + 1);}fn blurRuntimeArguments(width: u32, height: u32) ![2]choir_abi.ScalarArgument { if (width == 0 or height == 0) return error.UnsupportedImageOperation; return .{ .{ .u32 = width }, .{ .u32 = height }, };}fn resizeRuntimeArguments(dst_width: u32, dst_height: u32, src_width: u32, src_height: u32) ![7]choir_abi.ScalarArgument { if (dst_width == 0 or dst_height == 0 or src_width == 0 or src_height == 0) return error.UnsupportedImageOperation; return .{ .{ .u32 = dst_width }, .{ .u32 = dst_height }, .{ .u32 = src_width }, .{ .f32 = image_library.resizeScale(src_width, dst_width) }, .{ .f32 = image_library.resizeScale(src_height, dst_height) }, .{ .f32 = @floatFromInt(src_width - 1) }, .{ .f32 = @floatFromInt(src_height - 1) }, };}fn expectBufferSize(handle: gpu.BufferHandle, count: usize, element_size: usize) !void { const byte_size = std.math.mul(usize, count, element_size) catch return error.BufferTooLarge; if (handle.byte_size < byte_size) return error.BufferTooSmall;}fn allocateDeviceBuffer(handle: gpu.BackendHandle, comptime T: type, dtype: choir_abi.DType, count: usize) !gpu.BufferHandle { const byte_size = std.math.mul(usize, count, @sizeOf(T)) catch return error.BufferTooLarge; return handle.allocateBuffer(.{ .byte_size = byte_size, .alignment = 256, .dtype = dtype, .element_count = std.math.cast(u64, count) orelse return error.BufferTooLarge, });}fn bufferBinding(handle: gpu.BufferHandle, access: gpu.BufferAccess) gpu.BufferBinding { return .{ .handle = handle, .access = access, .ownership = handle.ownership, .byte_size = handle.byte_size, };}fn testPixel(seed: usize) u32 { var value: u32 = @truncate(seed *% 2654435761); value ^= value >> 13; value *%= 0x5bd1e995; value ^= value >> 15; return value;}fn referenceShadowAlloc(allocator: Allocator, paint: command.Command, options: ShadowOptions) !Shadow { const prepared = try prepareShadow(allocator, paint, options); var prepared_owned = true; defer if (prepared_owned) prepared.deinit(allocator); const pixels = try allocator.alloc(u32, prepared.plan.pixels); errdefer allocator.free(pixels); if (prepared.plan.blur_radius > 0) { const scratch = try allocator.alloc(u32, prepared.plan.pixels); defer allocator.free(scratch); const weights = try image_library.gaussianWeightsAlloc(allocator, prepared.plan.blur_radius, prepared.plan.sigma); defer allocator.free(weights); image_library.referenceBlurPass(scratch, prepared.mask, weights, prepared.plan.width, prepared.plan.height, .horizontal); image_library.referenceBlurPass(pixels, scratch, weights, prepared.plan.width, prepared.plan.height, .vertical); } else { @memcpy(pixels, prepared.mask); } tintShadowPixels(pixels, paint.color); const result = prepared.shadow(pixels); prepared_owned = false; prepared.deinitMask(allocator); return result;}test "paint image capacity matches an independent host and device byte model" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(ImageHostStorage, "gui_paint_image_capacity_capacity_model"), null, null, null, null, null, null, ); } comptime { @stardustClaim( @import("alloc_phase").capacity.witness(ImageHostStorage, "gui_paint_image_capacity_overload"), null, null, null, null, null, null, ); } const cases = [_]Limits{ .{}, .{ .dst_pixels = 4096, .src_pixels = 2048 }, .{ .dst_pixels = 257, .src_pixels = 129, .scratch_pixels = 257, .weight_taps = 31 }, }; for (cases) |limits| { const capacity = try Capacity.derive(limits); const host_elements = @as(u128, limits.dst_pixels) + limits.weight_taps; const device_elements = @as(u128, limits.dst_pixels) + limits.src_pixels + limits.scratch_pixels + limits.weight_taps; const device_buffer_count = @as(usize, @intFromBool(limits.dst_pixels != 0)) + @as(usize, @intFromBool(limits.src_pixels != 0)) + @as(usize, @intFromBool(limits.scratch_pixels != 0)) + @as(usize, @intFromBool(limits.weight_taps != 0)); try std.testing.expectEqual(@as(usize, @intCast(host_elements * @sizeOf(u32))), capacity.host_storage_bytes); try std.testing.expectEqual(@as(usize, @intCast(device_elements * @sizeOf(u32))), capacity.device_storage_bytes); try std.testing.expectEqual(@as(usize, device_buffer_count), capacity.device_buffer_count); try std.testing.expectEqual( capacity.host_storage_bytes + capacity.device_storage_bytes, capacity.total_storage_bytes, ); }}test "paint image capacity rejects overflowing storage limits" { try std.testing.expectError( error.CapacityOverflow, Capacity.derive(.{ .dst_pixels = std.math.maxInt(usize), .weight_taps = 1, }), );}test "paint image host storage rejects initialization OOM and seals on retry" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(ImageHostStorage, "gui_paint_image_host_oom"), null, null, null, null, null, null, ); } var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{}); const limits = Limits{ .dst_pixels = 16, .weight_taps = 7 }; failing.fail_index = failing.alloc_index; try std.testing.expectError( error.OutOfMemory, ImageHostStorage.init(failing.allocator(), limits), ); try std.testing.expect(failing.has_induced_failure); failing.fail_index = std.math.maxInt(usize); var storage = try ImageHostStorage.init(failing.allocator(), limits); defer storage.deinit(failing.allocator()); try std.testing.expectEqual(alloc_phase.capacity.Phase.initialization, storage.phase); storage.activate(); try std.testing.expectEqual(alloc_phase.capacity.Phase.steady, storage.phase); try std.testing.expectEqual(storage.capacity.host_storage_bytes, storage.bytes.len);}test "paint image Processor initial storage exposes the exact chosen capacity" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(ImageHostStorage, "gui_paint_image_acquisition"), null, null, null, null, null, null, ); } const allocator = std.testing.allocator; var state = gpu.recording.BackendState{ .allocator = allocator, .kind = .vulkan, .format = .vulkan_spirv, }; const limits = Limits{ .dst_pixels = 64, .src_pixels = 32, .scratch_pixels = 64, .weight_taps = 7, }; var processor = try Processor.init(allocator, state.handle(), .{ .artifact_format = .vulkan_spirv, .initial_storage = limits, }); defer processor.deinit(); const status = processor.storageStatus(); try std.testing.expectEqual(limits, status.limits.?); try std.testing.expectEqual(try Capacity.derive(limits), status.capacity.?); try std.testing.expectEqual(@as(usize, 0), status.replacements); try std.testing.expectEqual(status.capacity.?.host_storage_bytes, processor.buffers.host.?.bytes.len); try std.testing.expectEqual(status.capacity.?.device_buffer_count, state.buffer_allocate_count);}test "paint image Processor replaces storage at max plus one and retains its high water mark" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(ImageHostStorage, "gui_paint_image_boundary"), null, null, null, null, null, null, ); } const allocator = std.testing.allocator; var state = gpu.recording.BackendState{ .allocator = allocator, .kind = .vulkan, .format = .vulkan_spirv, }; const limits = Limits{ .dst_pixels = 4, .src_pixels = 4 }; var processor = try Processor.init(allocator, state.handle(), .{ .artifact_format = .vulkan_spirv, .initial_storage = limits, }); defer processor.deinit(); var src = @as([4]u32, @splat(0)); var dst = @as([6]u32, @splat(0)); try processor.resizeBilinear(dst[0..4], src[0..], 2, 2, 2, 2); try std.testing.expectEqual(@as(usize, 0), processor.storageStatus().replacements); try processor.resizeBilinear(dst[0..], src[0..], 3, 2, 2, 2); const grown = processor.storageStatus(); try std.testing.expectEqual(@as(usize, 1), grown.replacements); try std.testing.expectEqual(@as(usize, 6), grown.limits.?.dst_pixels); try std.testing.expectEqual(@as(usize, 4), grown.limits.?.src_pixels); try processor.resizeBilinear(dst[0..4], src[0..], 2, 2, 2, 2); try std.testing.expectEqual(grown, processor.storageStatus());}test "paint image Processor failed storage replacement preserves the prior epoch" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(ImageHostStorage, "gui_paint_image_atomic"), null, null, null, null, null, null, ); } var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{}); var state = gpu.recording.BackendState{ .allocator = failing.allocator(), .kind = .vulkan, .format = .vulkan_spirv, }; var processor = try Processor.init(failing.allocator(), state.handle(), .{ .artifact_format = .vulkan_spirv, .initial_storage = .{ .dst_pixels = 4, .src_pixels = 4 }, }); defer processor.deinit(); const before = processor.storageStatus(); const dst_id = processor.buffers.dst.?.handle.id; var src = @as([4]u32, @splat(0)); var dst = @as([6]u32, @splat(0)); failing.fail_index = failing.alloc_index; try std.testing.expectError( error.OutOfMemory, processor.resizeBilinear(dst[0..], src[0..], 3, 2, 2, 2), ); try std.testing.expect(failing.has_induced_failure); try std.testing.expectEqual(before, processor.storageStatus()); try std.testing.expectEqual(dst_id, processor.buffers.dst.?.handle.id); failing.fail_index = std.math.maxInt(usize); try processor.resizeBilinear(dst[0..4], src[0..], 2, 2, 2, 2);}test "paint image Processor cached admitted launch makes no allocator calls" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(ImageHostStorage, "gui_paint_image_steady"), null, null, null, null, null, null, ); } var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{}); var state = gpu.recording.BackendState{ .allocator = failing.allocator(), .kind = .vulkan, .format = .vulkan_spirv, }; var processor = try Processor.init(failing.allocator(), state.handle(), .{ .artifact_format = .vulkan_spirv, .initial_storage = .{ .dst_pixels = 4, .src_pixels = 4 }, }); defer processor.deinit(); var src = @as([4]u32, @splat(0)); var dst = @as([4]u32, @splat(0)); try processor.resizeBilinear(dst[0..], src[0..], 2, 2, 2, 2); failing.fail_index = failing.alloc_index; failing.resize_fail_index = failing.resize_index; try processor.resizeBilinear(dst[0..], src[0..], 2, 2, 2, 2); try std.testing.expect(!failing.has_induced_failure);}test "paint image Processor assembles maximum launch scalars in fixed local storage" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(ImageHostStorage, "gui_paint_image_scalars"), null, null, null, null, null, null, ); } const runtime_arguments = [_]choir_abi.ScalarArgument{ .{ .u32 = 1 }, .{ .u32 = 2 }, .{ .u32 = 3 }, .{ .u32 = 4 }, .{ .u32 = 5 }, .{ .u32 = 6 }, .{ .u32 = 7 }, }; const static_arguments = @as([host_loop_launch_shape_arg_count]choir_abi.ScalarArgument, @splat(.{ .u32 = 0 })); const entry = .{ .static_arguments = static_arguments[0..] }; const geometry = choir_abi.LaunchGeometry{ .grid = .{ 2, 1, 1 }, .threadgroup = .{ 4, 1, 1 }, }; const scalars = try imageLaunchScalars(entry, .cpu_object, geometry, runtime_arguments[0..]); try std.testing.expectEqual(@as(usize, scalar_argument_count_max), scalars.count); try std.testing.expectEqualSlices( choir_abi.ScalarArgument, runtime_arguments[0..], scalars.storage[0..runtime_arguments.len], ); try std.testing.expectEqual(@as(u32, 8), scalars.storage[runtime_arguments.len].u32);}test "paint image processor records image-family shadow blur passes" { const allocator = std.testing.allocator; var state = gpu.recording.BackendState{ .allocator = allocator, .kind = .vulkan, .format = .vulkan_spirv, }; var processor = try Processor.init(allocator, state.handle(), .{ .artifact_format = .vulkan_spirv }); defer processor.deinit(); const paint = command.Command{ .kind = .shadow, .rect = .{ .x = 2, .y = 3, .width = 5, .height = 4 }, .clip = .{ .x = 0, .y = 0, .width = 16, .height = 16 }, .color = .{ .r = 20, .g = 30, .b = 40, .a = 200 }, .radius = 1, .width = 2, .order = 9, }; var shadow = try processor.shadowAlloc(paint, .{ .image_index = 3 }); defer shadow.deinit(allocator); try std.testing.expectEqual(command.Kind.image, shadow.command.kind); try std.testing.expectEqual(@as(u32, 3), shadow.command.image_index); try std.testing.expectEqual(@as(u32, 9), shadow.command.order); try std.testing.expectEqual(@as(u32, 9), shadow.image.width); try std.testing.expectEqual(@as(u32, 8), shadow.image.height); try std.testing.expectEqual(@as(usize, 2), state.launch_count); try std.testing.expectEqual(@as(usize, 2), state.load_count); try std.testing.expectEqual(@as(usize, 1), state.sync_count); try std.testing.expectEqual(@as(usize, 4), state.buffer_allocate_count); try std.testing.expectEqual(@as(usize, 2), state.write_count); try std.testing.expectEqual(@as(usize, 1), state.read_count); try std.testing.expectEqual(@as(usize, 3), state.last_launch_buffer_count); try std.testing.expectEqual(gpu.BufferAccess.read_write, state.last_buffer_access[0]); try std.testing.expectEqual(gpu.BufferAccess.read_only, state.last_buffer_access[1]); try std.testing.expectEqual(gpu.BufferAccess.read_only, state.last_buffer_access[2]); try std.testing.expectEqual(@as(usize, 2), state.last_launch_scalar_count); try std.testing.expectEqual(shadow.image.width, state.last_launch_scalar_u32_values[0]); try std.testing.expectEqual(shadow.image.height, state.last_launch_scalar_u32_values[1]);}test "paint image processor reuses image-family shadow alpha" { const allocator = std.testing.allocator; var state = gpu.recording.BackendState{ .allocator = allocator, .kind = .vulkan, .format = .vulkan_spirv, }; var processor = try Processor.init(allocator, state.handle(), .{ .artifact_format = .vulkan_spirv }); defer processor.deinit(); const paint = command.Command{ .kind = .shadow, .rect = .{ .x = 2, .y = 3, .width = 5, .height = 4 }, .clip = .{ .x = 0, .y = 0, .width = 16, .height = 16 }, .color = .{ .r = 20, .g = 30, .b = 40, .a = 200 }, .radius = 1, .width = 2, .order = 9, }; var first = try processor.shadowAlloc(paint, .{ .image_index = 3 }); defer first.deinit(allocator); try std.testing.expectEqual(@as(usize, 2), state.load_count); try std.testing.expectEqual(@as(usize, 2), state.launch_count); try std.testing.expectEqual(@as(usize, 1), state.sync_count); try std.testing.expectEqual(@as(usize, 4), state.buffer_allocate_count); try std.testing.expectEqual(@as(usize, 2), state.write_count); try std.testing.expectEqual(@as(usize, 1), state.read_count); var second = try processor.shadowAlloc(paint, .{ .image_index = 4 }); defer second.deinit(allocator); try std.testing.expectEqual(@as(usize, 2), state.load_count); try std.testing.expectEqual(@as(usize, 2), state.launch_count); try std.testing.expectEqual(@as(usize, 1), state.sync_count); try std.testing.expectEqual(@as(usize, 4), state.buffer_allocate_count); try std.testing.expectEqual(@as(usize, 2), state.write_count); try std.testing.expectEqual(@as(usize, 1), state.read_count); var changed_rgb = paint; changed_rgb.color.r = 21; var third = try processor.shadowAlloc(changed_rgb, .{ .image_index = 5 }); defer third.deinit(allocator); try std.testing.expectEqual(@as(u32, 5), third.command.image_index); try std.testing.expectEqual(@as(usize, 2), state.load_count); try std.testing.expectEqual(@as(usize, 2), state.launch_count); try std.testing.expectEqual(@as(usize, 1), state.sync_count); try std.testing.expectEqual(@as(usize, 4), state.buffer_allocate_count); try std.testing.expectEqual(@as(usize, 2), state.write_count); try std.testing.expectEqual(@as(usize, 1), state.read_count); var changed_alpha = paint; changed_alpha.color.a = 201; var fourth = try processor.shadowAlloc(changed_alpha, .{ .image_index = 6 }); defer fourth.deinit(allocator); try std.testing.expectEqual(@as(usize, 2), state.load_count); try std.testing.expectEqual(@as(usize, 4), state.launch_count); try std.testing.expectEqual(@as(usize, 2), state.sync_count); try std.testing.expectEqual(@as(usize, 4), state.buffer_allocate_count); try std.testing.expectEqual(@as(usize, 3), state.write_count); try std.testing.expectEqual(@as(usize, 2), state.read_count); var changed_width = paint; changed_width.width = 3; var fifth = try processor.shadowAlloc(changed_width, .{ .image_index = 7 }); defer fifth.deinit(allocator); try std.testing.expectEqual(@as(usize, 4), state.load_count); try std.testing.expectEqual(@as(usize, 6), state.launch_count); try std.testing.expectEqual(@as(usize, 3), state.sync_count); try std.testing.expectEqual(@as(usize, 8), state.buffer_allocate_count); try std.testing.expectEqual(@as(usize, 5), state.write_count); try std.testing.expectEqual(@as(usize, 3), state.read_count);}test "paint image processor shadow image matches reference on cpu object" { const allocator = std.testing.allocator; var state = gpu.cpu.State.init(allocator); defer state.deinit(); var processor = try Processor.init(allocator, state.handle(), .{ .artifact_format = .cpu_object }); defer processor.deinit(); const paint = command.Command{ .kind = .shadow, .rect = .{ .x = 2, .y = 3, .width = 5, .height = 4 }, .clip = .{ .x = 0, .y = 0, .width = 16, .height = 16 }, .color = .{ .r = 20, .g = 30, .b = 40, .a = 200 }, .radius = 1, .width = 2, .order = 7, }; var expected = try referenceShadowAlloc(allocator, paint, .{ .image_index = 4 }); defer expected.deinit(allocator); var actual = try processor.shadowAlloc(paint, .{ .image_index = 4 }); defer actual.deinit(allocator); try std.testing.expectEqual(expected.command.kind, actual.command.kind); try std.testing.expectEqual(expected.command.image_index, actual.command.image_index); try std.testing.expectEqual(expected.command.order, actual.command.order); try std.testing.expectEqual(expected.image.width, actual.image.width); try std.testing.expectEqual(expected.image.height, actual.image.height); try std.testing.expectEqualSlices(u32, expected.pixels, actual.pixels); const center = actual.pixels[@as(usize, actual.image.width) * 3 + 4]; const center_color = cpu.pixel.unpackRgba(center); try std.testing.expectEqual(@as(u8, 20), center_color.r); try std.testing.expectEqual(@as(u8, 30), center_color.g); try std.testing.expectEqual(@as(u8, 40), center_color.b); try std.testing.expect(center_color.a > 0); var changed_rgb = paint; changed_rgb.color.r = 90; var expected_changed = try referenceShadowAlloc(allocator, changed_rgb, .{ .image_index = 5 }); defer expected_changed.deinit(allocator); var actual_changed = try processor.shadowAlloc(changed_rgb, .{ .image_index = 5 }); defer actual_changed.deinit(allocator); try std.testing.expectEqualSlices(u32, expected_changed.pixels, actual_changed.pixels); const changed_center = actual_changed.pixels[@as(usize, actual_changed.image.width) * 3 + 4]; const changed_color = cpu.pixel.unpackRgba(changed_center); try std.testing.expectEqual(@as(u8, 90), changed_color.r); try std.testing.expectEqual(@as(u8, 30), changed_color.g); try std.testing.expectEqual(center_color.a, changed_color.a);}test "paint image processor expands shadow commands into appended images" { const allocator = std.testing.allocator; var state = gpu.cpu.State.init(allocator); defer state.deinit(); var processor = try Processor.init(allocator, state.handle(), .{ .artifact_format = .cpu_object }); defer processor.deinit(); const base_pixels = [_]u32{ cpu.pixel.packRgba(.{ .r = 255, .a = 255 }), cpu.pixel.packRgba(.{ .g = 255, .a = 255 }), cpu.pixel.packRgba(.{ .b = 255, .a = 255 }), cpu.pixel.packRgba(.{ .r = 255, .g = 255, .a = 255 }), }; const images = command.ImageSet{ .images = &.{.{ .width = 2, .height = 2, .pixels = base_pixels[0..], }} }; const commands = [_]command.Command{ .{ .kind = .fill, .rect = .{ .x = 0, .y = 0, .width = 3, .height = 3 }, .clip = .{ .x = 0, .y = 0, .width = 16, .height = 16 }, .color = .{ .r = 200, .a = 255 }, .order = 0, }, .{ .kind = .shadow, .rect = .{ .x = 3, .y = 4, .width = 5, .height = 4 }, .clip = .{ .x = 0, .y = 0, .width = 16, .height = 16 }, .color = .{ .r = 20, .g = 30, .b = 40, .a = 200 }, .radius = 1, .width = 2, .order = 2, }, .{ .kind = .image, .rect = .{ .x = 8, .y = 1, .width = 2, .height = 2 }, .clip = .{ .x = 0, .y = 0, .width = 16, .height = 16 }, .color = .{ .r = 255, .g = 255, .b = 255, .a = 255 }, .image_index = 0, .order = 1, }, }; var expansion = try processor.expandShadowsAlloc(commands[0..], images, .{}); defer expansion.deinit(); const expanded_images = expansion.imageSet(); try std.testing.expectEqual(commands.len, expansion.commands.len); try std.testing.expectEqual(@as(usize, 2), expanded_images.images.len); try std.testing.expectEqual(command.Kind.fill, expansion.commands[0].kind); try std.testing.expectEqual(command.Kind.image, expansion.commands[1].kind); try std.testing.expectEqual(command.Kind.image, expansion.commands[2].kind); try std.testing.expectEqual(@as(u32, 1), expansion.commands[1].image_index); try std.testing.expectEqual(@as(u32, 0), expansion.commands[2].image_index); try std.testing.expectEqual(@as(u32, 2), expansion.commands[1].order); try std.testing.expectEqualSlices(u32, base_pixels[0..], expanded_images.images[0].pixels); var expected = try referenceShadowAlloc(allocator, commands[1], .{ .image_index = 1 }); defer expected.deinit(allocator); try std.testing.expectEqual(expected.command.kind, expansion.commands[1].kind); try std.testing.expectEqual(expected.command.image_index, expansion.commands[1].image_index); try std.testing.expectEqual(expected.image.width, expanded_images.images[1].width); try std.testing.expectEqual(expected.image.height, expanded_images.images[1].height); try std.testing.expectEqualSlices(u32, expected.pixels, expanded_images.images[1].pixels); var target_pixels = @as([(16 * 16)]u32, @splat(0)); try cpu.renderCommandsPackedWithImages(expansion.commands, .{ .width = 16, .height = 16, .pixels = target_pixels[0..], }, .{ .a = 0 }, expanded_images); try std.testing.expect(target_pixels[4 * 16 + 4] != 0);}test "paint image processor records shadow command expansion launches" { const allocator = std.testing.allocator; var state = gpu.recording.BackendState{ .allocator = allocator, .kind = .vulkan, .format = .vulkan_spirv, }; var processor = try Processor.init(allocator, state.handle(), .{ .artifact_format = .vulkan_spirv }); defer processor.deinit(); const commands = [_]command.Command{.{ .kind = .shadow, .rect = .{ .x = 2, .y = 3, .width = 5, .height = 4 }, .clip = .{ .x = 0, .y = 0, .width = 16, .height = 16 }, .color = .{ .r = 20, .g = 30, .b = 40, .a = 200 }, .radius = 1, .width = 2, .order = 9, }}; var expansion = try processor.expandShadowsAlloc(commands[0..], .{}, .{}); defer expansion.deinit(); try std.testing.expectEqual(@as(usize, 1), expansion.commands.len); try std.testing.expectEqual(@as(usize, 1), expansion.imageSet().images.len); try std.testing.expectEqual(command.Kind.image, expansion.commands[0].kind); try std.testing.expectEqual(@as(u32, 0), expansion.commands[0].image_index); try std.testing.expectEqual(@as(usize, 2), state.launch_count); try std.testing.expectEqual(@as(usize, 2), state.load_count); try std.testing.expectEqual(@as(usize, 1), state.sync_count);}test "paint image processor expansion preserves command lists without shadows" { const allocator = std.testing.allocator; var state = gpu.recording.BackendState{ .allocator = allocator, .kind = .vulkan, .format = .vulkan_spirv, }; var processor = try Processor.init(allocator, state.handle(), .{ .artifact_format = .vulkan_spirv }); defer processor.deinit(); const pixels = [_]u32{cpu.pixel.packRgba(.{ .r = 255, .a = 255 })}; const images = command.ImageSet{ .images = &.{.{ .width = 1, .height = 1, .pixels = pixels[0..] }} }; const commands = [_]command.Command{ .{ .kind = .fill, .rect = .{ .x = 0, .y = 0, .width = 3, .height = 3 }, .clip = .{ .x = 0, .y = 0, .width = 8, .height = 8 }, .color = .{ .r = 200, .a = 255 }, }, .{ .kind = .image, .rect = .{ .x = 3, .y = 3, .width = 1, .height = 1 }, .clip = .{ .x = 0, .y = 0, .width = 8, .height = 8 }, .color = .{ .r = 255, .g = 255, .b = 255, .a = 255 }, .image_index = 0, }, }; var expansion = try processor.expandShadowsAlloc(commands[0..], images, .{}); defer expansion.deinit(); try std.testing.expectEqualSlices(command.Command, commands[0..], expansion.commands); try std.testing.expectEqual(@as(usize, 1), expansion.imageSet().images.len); try std.testing.expectEqualSlices(u32, pixels[0..], expansion.imageSet().images[0].pixels); try std.testing.expectEqual(@as(usize, 0), state.launch_count);}test "paint image processor records blur catalog launch" { const allocator = std.testing.allocator; var state = gpu.recording.BackendState{ .allocator = allocator, .kind = .vulkan, .format = .vulkan_spirv, }; var processor = try Processor.init(allocator, state.handle(), .{ .artifact_format = .vulkan_spirv }); defer processor.deinit(); const width: u32 = 33; const height: u32 = 17; const radius: u32 = 1; const pixels = try pixelCount(width, height); const taps = try blurTapCount(radius); const dst = try allocateDeviceBuffer(state.handle(), u32, .u32, pixels); defer state.handle().destroyObject(dst.id); const src = try allocateDeviceBuffer(state.handle(), u32, .u32, pixels); defer state.handle().destroyObject(src.id); const weights = try allocateDeviceBuffer(state.handle(), f32, .f32, taps); defer state.handle().destroyObject(weights.id); try processor.launchBlurPass(dst, src, weights, width, height, radius, .vertical); const threads = image_library.imageThreadsForExtents(width, height); try std.testing.expectEqual(@as(usize, 1), state.load_count); try std.testing.expectEqual(@as(usize, 1), state.launch_count); try std.testing.expectEqual(@as(usize, 1), state.sync_count); try std.testing.expectEqual(@as(usize, 3), state.last_launch_buffer_count); try std.testing.expectEqual(@as(usize, 2), state.last_launch_scalar_count); try std.testing.expectEqual(width, state.last_launch_scalar_u32_values[0]); try std.testing.expectEqual(height, state.last_launch_scalar_u32_values[1]); try std.testing.expectEqual((width + threads.x - 1) / threads.x, state.last_launch_grid[0]); try std.testing.expectEqual((height + threads.y - 1) / threads.y, state.last_launch_grid[1]); try std.testing.expectEqual(@as(u32, 1), state.last_launch_grid[2]); try std.testing.expectEqual(threads.x, state.last_launch_threadgroup[0]); try std.testing.expectEqual(threads.y, state.last_launch_threadgroup[1]); try std.testing.expectEqual(gpu.BufferAccess.read_write, state.last_buffer_access[0]); try std.testing.expectEqual(gpu.BufferAccess.read_only, state.last_buffer_access[1]); try std.testing.expectEqual(gpu.BufferAccess.read_only, state.last_buffer_access[2]);}test "paint image processor blur pass matches Accy image reference on cpu object" { const allocator = std.testing.allocator; var state = gpu.cpu.State.init(allocator); defer state.deinit(); var processor = try Processor.init(allocator, state.handle(), .{ .artifact_format = .cpu_object }); defer processor.deinit(); const width: u32 = 5; const height: u32 = 4; const pixels = try pixelCount(width, height); const radius: u32 = 1; const sigma: f32 = 1.0; const src = try allocator.alloc(u32, pixels); defer allocator.free(src); for (src, 0..) |*pixel, index| pixel.* = testPixel(index + 3); const actual = try allocator.alloc(u32, pixels); defer allocator.free(actual); @memset(actual, 0); const expected = try allocator.alloc(u32, pixels); defer allocator.free(expected); const weights = try image_library.gaussianWeightsAlloc(allocator, radius, sigma); defer allocator.free(weights); image_library.referenceBlurPass(expected, src, weights, width, height, .horizontal); try processor.blurPass(actual, src, width, height, radius, sigma, .horizontal); try std.testing.expectEqualSlices(u32, expected, actual);}test "paint image processor reuses blur pass buffers" { const allocator = std.testing.allocator; var state = gpu.recording.BackendState{ .allocator = allocator, .kind = .vulkan, .format = .vulkan_spirv, }; var processor = try Processor.init(allocator, state.handle(), .{ .artifact_format = .vulkan_spirv }); defer processor.deinit(); const width: u32 = 5; const height: u32 = 4; const pixels: usize = width * height; var src = @as([pixels]u32, @splat(0)); for (src[0..], 0..) |*pixel, index| pixel.* = testPixel(index + 101); var dst = @as([pixels]u32, @splat(0)); try processor.blurPass(dst[0..], src[0..], width, height, 1, 1.0, .horizontal); try std.testing.expectEqual(@as(usize, 3), state.buffer_allocate_count); try std.testing.expectEqual(@as(usize, 1), state.launch_count); try std.testing.expectEqual(@as(usize, 2), state.write_count); try std.testing.expectEqual(@as(usize, 1), state.read_count); try processor.blurPass(dst[0..], src[0..], width, height, 1, 1.0, .horizontal); try std.testing.expectEqual(@as(usize, 3), state.buffer_allocate_count); try std.testing.expectEqual(@as(usize, 2), state.launch_count); try std.testing.expectEqual(@as(usize, 3), state.write_count); try std.testing.expectEqual(@as(usize, 2), state.read_count); const larger_width: u32 = 6; const larger_pixels: usize = larger_width * height; var larger_src = @as([larger_pixels]u32, @splat(0)); for (larger_src[0..], 0..) |*pixel, index| pixel.* = testPixel(index + 131); var larger_dst = @as([larger_pixels]u32, @splat(0)); try processor.blurPass(larger_dst[0..], larger_src[0..], larger_width, height, 2, 1.0, .horizontal); try std.testing.expectEqual(@as(usize, 6), state.buffer_allocate_count); try std.testing.expectEqual(@as(usize, 3), state.launch_count); try std.testing.expectEqual(@as(usize, 5), state.write_count); try std.testing.expectEqual(@as(usize, 3), state.read_count);}test "paint image processor resize bilinear matches Accy image reference on cpu object" { const allocator = std.testing.allocator; var state = gpu.cpu.State.init(allocator); defer state.deinit(); var processor = try Processor.init(allocator, state.handle(), .{ .artifact_format = .cpu_object }); defer processor.deinit(); const src_width: u32 = 3; const src_height: u32 = 2; const dst_width: u32 = 5; const dst_height: u32 = 4; const src_pixels = try pixelCount(src_width, src_height); const dst_pixels = try pixelCount(dst_width, dst_height); const src = try allocator.alloc(u32, src_pixels); defer allocator.free(src); for (src, 0..) |*pixel, index| pixel.* = testPixel(index + 11); const actual = try allocator.alloc(u32, dst_pixels); defer allocator.free(actual); @memset(actual, 0); const expected = try allocator.alloc(u32, dst_pixels); defer allocator.free(expected); image_library.referenceResizeBilinear(expected, src, dst_width, dst_height, src_width, src_height); try processor.resizeBilinear(actual, src, dst_width, dst_height, src_width, src_height); try std.testing.expectEqualSlices(u32, expected, actual);}test "paint image processor records owned resize image launch" { const allocator = std.testing.allocator; var state = gpu.recording.BackendState{ .allocator = allocator, .kind = .vulkan, .format = .vulkan_spirv, }; var processor = try Processor.init(allocator, state.handle(), .{ .artifact_format = .vulkan_spirv }); defer processor.deinit(); const src_width: u32 = 3; const src_height: u32 = 2; const dst_width: u32 = 5; const dst_height: u32 = 4; var src = @as([(src_width * src_height)]u32, @splat(0)); for (src[0..], 0..) |*pixel, index| pixel.* = testPixel(index + 31); var resized = try processor.resizeAlloc(.{ .width = src_width, .height = src_height, .pixels = src[0..], }, dst_width, dst_height); defer resized.deinit(allocator); try std.testing.expectEqual(dst_width, resized.image.width); try std.testing.expectEqual(dst_height, resized.image.height); try std.testing.expectEqual(@as(usize, dst_width * dst_height), resized.image.pixels.len); try std.testing.expectEqual(resized.pixels.ptr, resized.image.pixels.ptr); try std.testing.expectEqual(@as(usize, 1), state.launch_count); try std.testing.expectEqual(@as(usize, 1), state.load_count); try std.testing.expectEqual(@as(usize, 1), state.sync_count); try std.testing.expectEqual(@as(usize, 1), state.write_count); try std.testing.expectEqual(@as(usize, 1), state.read_count); try std.testing.expectEqual(@as(usize, 2), state.last_launch_buffer_count); try std.testing.expectEqual(@as(usize, 7), state.last_launch_scalar_count); try std.testing.expectEqual(dst_width, state.last_launch_scalar_u32_values[0]); try std.testing.expectEqual(dst_height, state.last_launch_scalar_u32_values[1]); try std.testing.expectEqual(src_width, state.last_launch_scalar_u32_values[2]); try std.testing.expectApproxEqAbs(image_library.resizeScale(src_width, dst_width), state.last_launch_scalar_f32_values[3], 0.00001); try std.testing.expectApproxEqAbs(image_library.resizeScale(src_height, dst_height), state.last_launch_scalar_f32_values[4], 0.00001);}test "paint image processor reuses image-family resize kernels" { const allocator = std.testing.allocator; var state = gpu.recording.BackendState{ .allocator = allocator, .kind = .vulkan, .format = .vulkan_spirv, }; var processor = try Processor.init(allocator, state.handle(), .{ .artifact_format = .vulkan_spirv }); defer processor.deinit(); const src_width: u32 = 3; const src_height: u32 = 2; const dst_width: u32 = 5; const dst_height: u32 = 4; var src = @as([(src_width * src_height)]u32, @splat(0)); for (src[0..], 0..) |*pixel, index| pixel.* = testPixel(index + 41); var first = try processor.resizeAlloc(.{ .width = src_width, .height = src_height, .pixels = src[0..], }, dst_width, dst_height); defer first.deinit(allocator); try std.testing.expectEqual(@as(usize, 1), state.load_count); try std.testing.expectEqual(@as(usize, 1), state.launch_count); try std.testing.expectEqual(@as(usize, 2), state.buffer_allocate_count); var second = try processor.resizeAlloc(.{ .width = src_width, .height = src_height, .pixels = src[0..], }, dst_width, dst_height); defer second.deinit(allocator); try std.testing.expectEqual(@as(usize, 1), state.load_count); try std.testing.expectEqual(@as(usize, 2), state.launch_count); try std.testing.expectEqual(@as(usize, 2), state.buffer_allocate_count); var third = try processor.resizeAlloc(.{ .width = src_width, .height = src_height, .pixels = src[0..], }, dst_width + 1, dst_height); defer third.deinit(allocator); try std.testing.expectEqual(@as(usize, 2), state.load_count); try std.testing.expectEqual(@as(usize, 3), state.launch_count); try std.testing.expectEqual(@as(usize, 4), state.buffer_allocate_count);}test "paint image processor reads retained larger resize buffers into active result" { const allocator = std.testing.allocator; var state = gpu.recording.BackendState{ .allocator = allocator, .kind = .vulkan, .format = .vulkan_spirv, }; var processor = try Processor.init(allocator, state.handle(), .{ .artifact_format = .vulkan_spirv }); defer processor.deinit(); const src_width: u32 = 3; const src_height: u32 = 2; var src = @as([(src_width * src_height)]u32, @splat(0)); for (src[0..], 0..) |*pixel, index| pixel.* = testPixel(index + 151); var larger = try processor.resizeAlloc(.{ .width = src_width, .height = src_height, .pixels = src[0..], }, 5, 4); defer larger.deinit(allocator); try std.testing.expectEqual(@as(usize, 20), larger.pixels.len); try std.testing.expectEqual(@as(usize, 80), state.last_read_byte_count); var smaller = try processor.resizeAlloc(.{ .width = src_width, .height = src_height, .pixels = src[0..], }, 2, 2); defer smaller.deinit(allocator); try std.testing.expectEqual(@as(usize, 4), smaller.pixels.len); try std.testing.expectEqual(@as(usize, 2), state.buffer_allocate_count); try std.testing.expectEqual(@as(usize, 2), state.read_count); try std.testing.expectEqual(@as(usize, 80), state.last_read_byte_count);}test "paint image processor owned resize image matches Accy image reference on cpu object" { const allocator = std.testing.allocator; var state = gpu.cpu.State.init(allocator); defer state.deinit(); var processor = try Processor.init(allocator, state.handle(), .{ .artifact_format = .cpu_object }); defer processor.deinit(); const src_width: u32 = 4; const src_height: u32 = 3; const dst_width: u32 = 2; const dst_height: u32 = 5; const src_pixels = try pixelCount(src_width, src_height); const dst_pixels = try pixelCount(dst_width, dst_height); const src = try allocator.alloc(u32, src_pixels); defer allocator.free(src); for (src, 0..) |*pixel, index| pixel.* = testPixel(index + 47); const expected = try allocator.alloc(u32, dst_pixels); defer allocator.free(expected); image_library.referenceResizeBilinear(expected, src, dst_width, dst_height, src_width, src_height); var resized = try processor.resizeAlloc(.{ .width = src_width, .height = src_height, .pixels = src, }, dst_width, dst_height); defer resized.deinit(allocator); try std.testing.expectEqual(dst_width, resized.image.width); try std.testing.expectEqual(dst_height, resized.image.height); try std.testing.expectEqualSlices(u32, expected, resized.pixels); try std.testing.expectEqualSlices(u32, expected, resized.image.pixels);}test "paint image thumbnail extent fits max box without upscaling" { try std.testing.expectEqual(ImageExtent{ .width = 100, .height = 50 }, try thumbnailExtent(400, 200, 100, 100)); try std.testing.expectEqual(ImageExtent{ .width = 50, .height = 100 }, try thumbnailExtent(200, 400, 100, 100)); try std.testing.expectEqual(ImageExtent{ .width = 100, .height = 50 }, try thumbnailExtent(100, 50, 500, 500)); try std.testing.expectEqual(ImageExtent{ .width = 4, .height = 1 }, try thumbnailExtent(7, 3, 4, 4)); try std.testing.expectError(error.InvalidImage, thumbnailExtent(1, 1, 0, 4));}test "paint image processor records thumbnail resize launch" { const allocator = std.testing.allocator; var state = gpu.recording.BackendState{ .allocator = allocator, .kind = .vulkan, .format = .vulkan_spirv, }; var processor = try Processor.init(allocator, state.handle(), .{ .artifact_format = .vulkan_spirv }); defer processor.deinit(); const src_width: u32 = 4; const src_height: u32 = 2; const dst_width: u32 = 2; const dst_height: u32 = 1; var src = @as([(src_width * src_height)]u32, @splat(0)); for (src[0..], 0..) |*pixel, index| pixel.* = testPixel(index + 71); var thumbnail = try processor.thumbnailAlloc(.{ .width = src_width, .height = src_height, .pixels = src[0..], }, .{ .max_width = 2, .max_height = 2, }); defer thumbnail.deinit(allocator); try std.testing.expectEqual(dst_width, thumbnail.image.width); try std.testing.expectEqual(dst_height, thumbnail.image.height); try std.testing.expectEqual(@as(usize, dst_width * dst_height), thumbnail.pixels.len); try std.testing.expectEqual(@as(usize, 1), state.launch_count); try std.testing.expectEqual(@as(usize, 1), state.load_count); try std.testing.expectEqual(@as(usize, 1), state.sync_count); try std.testing.expectEqual(@as(usize, 1), state.write_count); try std.testing.expectEqual(@as(usize, 1), state.read_count); try std.testing.expectEqual(@as(usize, 7), state.last_launch_scalar_count); try std.testing.expectEqual(dst_width, state.last_launch_scalar_u32_values[0]); try std.testing.expectEqual(dst_height, state.last_launch_scalar_u32_values[1]); try std.testing.expectEqual(src_width, state.last_launch_scalar_u32_values[2]); try std.testing.expectApproxEqAbs(image_library.resizeScale(src_width, dst_width), state.last_launch_scalar_f32_values[3], 0.00001); try std.testing.expectApproxEqAbs(image_library.resizeScale(src_height, dst_height), state.last_launch_scalar_f32_values[4], 0.00001);}test "paint image processor thumbnail copies already fitting images without launch" { const allocator = std.testing.allocator; var state = gpu.recording.BackendState{ .allocator = allocator, .kind = .vulkan, .format = .vulkan_spirv, }; var processor = try Processor.init(allocator, state.handle(), .{ .artifact_format = .vulkan_spirv }); defer processor.deinit(); const src_width: u32 = 2; const src_height: u32 = 2; var src = @as([(src_width * src_height)]u32, @splat(0)); for (src[0..], 0..) |*pixel, index| pixel.* = testPixel(index + 83); var thumbnail = try processor.thumbnailAlloc(.{ .width = src_width, .height = src_height, .pixels = src[0..], }, .{ .max_width = 8, .max_height = 8, }); defer thumbnail.deinit(allocator); try std.testing.expectEqual(src_width, thumbnail.image.width); try std.testing.expectEqual(src_height, thumbnail.image.height); try std.testing.expectEqualSlices(u32, src[0..], thumbnail.pixels); try std.testing.expectEqualSlices(u32, src[0..], thumbnail.image.pixels); try std.testing.expect(@intFromPtr(src[0..].ptr) != @intFromPtr(thumbnail.pixels.ptr)); try std.testing.expectEqual(@as(usize, 0), state.launch_count); try std.testing.expectEqual(@as(usize, 0), state.write_count); try std.testing.expectEqual(@as(usize, 0), state.read_count);}test "paint image processor thumbnail image matches Accy image reference on cpu object" { const allocator = std.testing.allocator; var state = gpu.cpu.State.init(allocator); defer state.deinit(); var processor = try Processor.init(allocator, state.handle(), .{ .artifact_format = .cpu_object }); defer processor.deinit(); const src_width: u32 = 6; const src_height: u32 = 4; const dst_width: u32 = 3; const dst_height: u32 = 2; const src_pixels = try pixelCount(src_width, src_height); const dst_pixels = try pixelCount(dst_width, dst_height); const src = try allocator.alloc(u32, src_pixels); defer allocator.free(src); for (src, 0..) |*pixel, index| pixel.* = testPixel(index + 97); const expected = try allocator.alloc(u32, dst_pixels); defer allocator.free(expected); image_library.referenceResizeBilinear(expected, src, dst_width, dst_height, src_width, src_height); var thumbnail = try processor.thumbnailAlloc(.{ .width = src_width, .height = src_height, .pixels = src, }, .{ .max_width = 3, .max_height = 3, }); defer thumbnail.deinit(allocator); try std.testing.expectEqual(dst_width, thumbnail.image.width); try std.testing.expectEqual(dst_height, thumbnail.image.height); try std.testing.expectEqualSlices(u32, expected, thumbnail.pixels); try std.testing.expectEqualSlices(u32, expected, thumbnail.image.pixels);}Source: lib/gui/src/paint/root.zig:8
zig
pub const image = @import("image.zig");Complete caller list for paint.ImageProcessor.deinit
21 direct callers.
lib.gui.src.paint.image.test_paint_image_Processor_cached_admitted_launch_makes_no_allocator_calls[function] — test source atlib/gui/src/paint/image.zig:1553in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_Processor_failed_storage_replacement_preserves_the_prior_epoch[function] — test source atlib/gui/src/paint/image.zig:1513in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_Processor_initial_storage_exposes_the_exact_chosen_capacity[function] — test source atlib/gui/src/paint/image.zig:1437in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_Processor_replaces_storage_at_max_plus_one_and_retains_its_high_water_mark[function] — test source atlib/gui/src/paint/image.zig:1475in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_blur_pass_matches_Accy_image_reference_on_cpu_object[function] — test source atlib/gui/src/paint/image.zig:1972in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_expands_shadow_commands_into_appended_images[function] — test source atlib/gui/src/paint/image.zig:1787in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_expansion_preserves_command_lists_without_shadows[function] — test source atlib/gui/src/paint/image.zig:1894in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_owned_resize_image_matches_Accy_image_reference_on_cpu_object[function] — test source atlib/gui/src/paint/image.zig:2197in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_reads_retained_larger_resize_buffers_into_active_result[function] — test source atlib/gui/src/paint/image.zig:2162in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_records_blur_catalog_launch[function] — test source atlib/gui/src/paint/image.zig:1930in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_records_image-family_shadow_blur_passes[function] — test source atlib/gui/src/paint/image.zig:1624in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_records_owned_resize_image_launch[function] — test source atlib/gui/src/paint/image.zig:2074in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_records_shadow_command_expansion_launches[function] — test source atlib/gui/src/paint/image.zig:1863in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_records_thumbnail_resize_launch[function] — test source atlib/gui/src/paint/image.zig:2238in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_resize_bilinear_matches_Accy_image_reference_on_cpu_object[function] — test source atlib/gui/src/paint/image.zig:2044in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_reuses_blur_pass_buffers[function] — test source atlib/gui/src/paint/image.zig:2003in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_reuses_image-family_resize_kernels[function] — test source atlib/gui/src/paint/image.zig:2115in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_reuses_image-family_shadow_alpha[function] — test source atlib/gui/src/paint/image.zig:1666in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_shadow_image_matches_reference_on_cpu_object[function] — test source atlib/gui/src/paint/image.zig:1738in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_thumbnail_copies_already_fitting_images_without_launch[function] — test source atlib/gui/src/paint/image.zig:2280in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_thumbnail_image_matches_Accy_image_reference_on_cpu_object[function] — test source atlib/gui/src/paint/image.zig:2314in nearest public ownertiny.gui.paint.image
Complete caller list for paint.ImageProcessor.init
21 direct callers.
lib.gui.src.paint.image.test_paint_image_Processor_cached_admitted_launch_makes_no_allocator_calls[function] — test source atlib/gui/src/paint/image.zig:1553in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_Processor_failed_storage_replacement_preserves_the_prior_epoch[function] — test source atlib/gui/src/paint/image.zig:1513in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_Processor_initial_storage_exposes_the_exact_chosen_capacity[function] — test source atlib/gui/src/paint/image.zig:1437in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_Processor_replaces_storage_at_max_plus_one_and_retains_its_high_water_mark[function] — test source atlib/gui/src/paint/image.zig:1475in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_blur_pass_matches_Accy_image_reference_on_cpu_object[function] — test source atlib/gui/src/paint/image.zig:1972in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_expands_shadow_commands_into_appended_images[function] — test source atlib/gui/src/paint/image.zig:1787in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_expansion_preserves_command_lists_without_shadows[function] — test source atlib/gui/src/paint/image.zig:1894in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_owned_resize_image_matches_Accy_image_reference_on_cpu_object[function] — test source atlib/gui/src/paint/image.zig:2197in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_reads_retained_larger_resize_buffers_into_active_result[function] — test source atlib/gui/src/paint/image.zig:2162in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_records_blur_catalog_launch[function] — test source atlib/gui/src/paint/image.zig:1930in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_records_image-family_shadow_blur_passes[function] — test source atlib/gui/src/paint/image.zig:1624in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_records_owned_resize_image_launch[function] — test source atlib/gui/src/paint/image.zig:2074in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_records_shadow_command_expansion_launches[function] — test source atlib/gui/src/paint/image.zig:1863in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_records_thumbnail_resize_launch[function] — test source atlib/gui/src/paint/image.zig:2238in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_resize_bilinear_matches_Accy_image_reference_on_cpu_object[function] — test source atlib/gui/src/paint/image.zig:2044in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_reuses_blur_pass_buffers[function] — test source atlib/gui/src/paint/image.zig:2003in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_reuses_image-family_resize_kernels[function] — test source atlib/gui/src/paint/image.zig:2115in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_reuses_image-family_shadow_alpha[function] — test source atlib/gui/src/paint/image.zig:1666in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_shadow_image_matches_reference_on_cpu_object[function] — test source atlib/gui/src/paint/image.zig:1738in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_thumbnail_copies_already_fitting_images_without_launch[function] — test source atlib/gui/src/paint/image.zig:2280in nearest public ownertiny.gui.paint.imagelib.gui.src.paint.image.test_paint_image_processor_thumbnail_image_matches_Accy_image_reference_on_cpu_object[function] — test source atlib/gui/src/paint/image.zig:2314in nearest public ownertiny.gui.paint.image
Audit
| Definitions | 38 |
|---|---|
| Public names | 79 |
| Members | 39 |
| Version | 26.7.0 |
| Revision | daab053ee433 |