Skip to documentation
SLOP

tiny.gui.paint.image

Reference tiny.gui paint image

Defined in paint.

API (39)

Actions

Public operations.

Types and contracts

Public types and contracts.

Values and defaults

Public values and defaults.

No direct callersNo direct callspaintimage
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Called byCallsNo direct callspaint.PaintImageHostStorageinitpaint.ImageProcessorCapacityderive
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callsprivate sourcelib.gui.src.paint.image.ImageBuffersinittest sourcelib.gui.src.paint.imagetest: paint image host storage reject...paint.PaintImageHostStorageactivate
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callsprivate sourcelib.gui.src.paint.image.ImageBuffersinittest sourcelib.gui.src.paint.imagetest: paint image host storage reject...paint.PaintImageHostStoragedeinit
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsprivate sourcelib.gui.src.paint.image.ImageBuffersinittest sourcelib.gui.src.paint.imagetest: paint image host storage reject...paint.image.Capacityderivepaint.PaintImageHostStorageinit
Static calls · unresolved targets: 1 · external targets: 1.
Called byCallstest sourcelib.gui.src.paint.imagetest: paint image processor blur pass...test sourcelib.gui.src.paint.imagetest: paint image processor reuses bl...private sourcelib.gui.src.paint.image.ImageBuffersensureWeightsDataprivate sourcelib.gui.src.paint.image.ImageBuffersreadPixelsprivate sourcelib.gui.src.paint.image.ProcessorensureStoragepaint.ImageProcessorlaunchBlurPassprivate sourcelib.gui.src.paint.imageblurTapCountprivate sourcelib.gui.src.paint.imagevalidateImageSlicespaint.ImageProcessorblurPass
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallstest sourcelib.gui.src.paint.imagetest: paint image Processor cached ad...test sourcelib.gui.src.paint.imagetest: paint image Processor failed st...test sourcelib.gui.src.paint.imagetest: paint image Processor initial s...test sourcelib.gui.src.paint.imagetest: paint image Processor replaces ...test sourcelib.gui.src.paint.imagetest: paint image processor blur pass...+16 moreprivate sourcelib.gui.src.paint.image.ImageBuffersdeinitpaint.ImageProcessordeinit
Static calls · unresolved targets: 0 · external targets: 2.
Called byCallstest sourcelib.gui.src.paint.imagetest: paint image processor expands s...test sourcelib.gui.src.paint.imagetest: paint image processor expansion...test sourcelib.gui.src.paint.imagetest: paint image processor records s...paint.ImageProcessorshadowAllocprivate sourcelib.gui.src.paint.imagecountShadowspaint.ImageProcessorexpandShadowsAlloc
Static calls · unresolved targets: 2 · external targets: 1.
Called byCallstest sourcelib.gui.src.paint.imagetest: paint image Processor cached ad...test sourcelib.gui.src.paint.imagetest: paint image Processor failed st...test sourcelib.gui.src.paint.imagetest: paint image Processor initial s...test sourcelib.gui.src.paint.imagetest: paint image Processor replaces ...test sourcelib.gui.src.paint.imagetest: paint image processor blur pass...+16 moreprivate sourcelib.gui.src.paint.image.ImageBuffersinitprivate sourcelib.gui.src.paint.imagedefaultFormatpaint.ImageProcessorinit
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallspaint.ImageProcessorblurPasstest sourcelib.gui.src.paint.imagetest: paint image processor records b...private sourcelib.gui.src.paint.image.ProcessorqueueBlurPasspaint.ImageProcessorlaunchBlurPass
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallspaint.ImageProcessorresizeBilinearprivate sourcelib.gui.src.paint.image.ProcessorlaunchCachedKernelprivate sourcelib.gui.src.paint.imagebufferBindingprivate sourcelib.gui.src.paint.imageexpectBufferSizeprivate sourcelib.gui.src.paint.imagepixelCountprivate sourcelib.gui.src.paint.imageresizeRuntimeArgumentspaint.ImageProcessorlaunchResizeBilinear
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallspaint.ImageProcessorthumbnailAlloctest sourcelib.gui.src.paint.imagetest: paint image processor owned res...test sourcelib.gui.src.paint.imagetest: paint image processor reads ret...test sourcelib.gui.src.paint.imagetest: paint image processor records o...test sourcelib.gui.src.paint.imagetest: paint image processor reuses im...paint.ImageProcessorresizeBilinearprivate sourcelib.gui.src.paint.imagepixelCountprivate sourcelib.machine.src.world.sourcevalidatepaint.ImageProcessorresizeAlloc
Static calls · unresolved targets: 2 · external targets: 0.
Called byCallspaint.ImageProcessorresizeAlloctest sourcelib.gui.src.paint.imagetest: paint image Processor cached ad...test sourcelib.gui.src.paint.imagetest: paint image Processor failed st...test sourcelib.gui.src.paint.imagetest: paint image Processor replaces ...test sourcelib.gui.src.paint.imagetest: paint image processor resize bi...private sourcelib.gui.src.paint.image.ImageBuffersreadPixelsprivate sourcelib.gui.src.paint.image.ProcessorensureStoragepaint.ImageProcessorlaunchResizeBilinearprivate sourcelib.gui.src.paint.imagepixelCountpaint.ImageProcessorresizeBilinear
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallspaint.ImageProcessorexpandShadowsAlloctest sourcelib.gui.src.paint.imagetest: paint image processor records i...test sourcelib.gui.src.paint.imagetest: paint image processor reuses im...test sourcelib.gui.src.paint.imagetest: paint image processor shadow im...private sourcelib.gui.src.paint.image.ProcessorblurTwoPassprivate sourcelib.gui.src.paint.image.ProcessorstoreShadowAlphaprivate sourcelib.gui.src.paint.image.ProcessoruseShadowAlphaCacheprivate sourcelib.gui.src.paint.imageplanShadowprivate sourcelib.gui.src.paint.imagerasterShadowMaskprivate sourcelib.gui.src.paint.imagetintShadowPixelspaint.ImageProcessorshadowAlloc
Static calls · unresolved targets: 2 · external targets: 1.
Called byCallsNo direct callstest sourcelib.gui.src.paint.imagetest: paint image Processor failed st...test sourcelib.gui.src.paint.imagetest: paint image Processor initial s...test sourcelib.gui.src.paint.imagetest: paint image Processor replaces ...paint.ImageProcessorstorageStatus
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallstest sourcelib.gui.src.paint.imagetest: paint image processor records t...test sourcelib.gui.src.paint.imagetest: paint image processor thumbnail...test sourcelib.gui.src.paint.imagetest: paint image processor thumbnail...paint.ImageProcessorresizeAllocprivate sourcelib.gui.src.paint.imagepixelCountprivate sourcelib.gui.src.paint.imagethumbnailExtentprivate sourcelib.machine.src.world.sourcevalidatepaint.ImageProcessorthumbnailAlloc
Static calls · unresolved targets: 2 · external targets: 0.
Called byCallsNo direct callersprivate sourcelib.gui.src.paint.imagetintShadowPixelspaint.ImageShadowExpansionrefreshReusableCommands
Static calls · unresolved targets: 0 · external targets: 0.

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.

Complete caller list for paint.ImageProcessor.init

21 direct callers.

Audit

Definitions38
Public names79
Members39
Version26.7.0
Revisiondaab053ee433