# Programs

Complete programs for what happens around the operations: moving pixels in and out, masks, files, lifetime, memory, typography and putting an image on screen.

# Reuse temporary GPU memory

Requirements queries return plans without allocating GPU storage. Merge plans for sequential operations, reserve once, and pass the same Workspace in each operation’s options. Records keep it alive until submission retirement.

auto plan = box_blur_requirements(image.size(), {.radius = 8}).workspace;
plan.merge(gaussian_blur_requirements(image.size(), {.radius = 12, .sigma = 4}).workspace);
auto workspace = ctx.create_workspace(plan);
auto result = ctx.create_image({320, 200});

cmd.box_blur(image, result, {.radius = 8, .workspace = workspace});
cmd.gaussian_blur(result, {.radius = 12, .sigma = 4, .workspace = workspace});

Reuse temporary GPU memory

# Present an editor viewport

Draw a pan/zoom/rotate view of the canvas for an editor: nearest pixels at 100% and above, area-averaged pyramid levels when zoomed out, a transparency checkerboard, the pasteboard color, a Quick Mask style selection overlay and a pixel grid above 500%. Presenter and Display accept the same ViewportOptions.

#include <wgpupixel_webgpu.h>

using namespace wgpupixel;

int main() {
    auto ctx = Context::create();
    auto canvas = ctx.create_image({4000, 3000});
    auto shape = ctx.create_image({4000, 3000});
    auto selection = ctx.create_mask({4000, 3000});
    auto cmd = ctx.create_commands();
    cmd.fill(canvas, {.color = {0, 0, 0, 0}}); // Transparent: the checkerboard shows.
    cmd.fill(canvas, {.color = {0.2f, 0.05f, 0.01f, 0.5f}, .region = Rect{500, 400, 2000, 1500}});
    cmd.circle(shape, {.color = {1, 1, 1, 1}, .background = {0, 0, 0, 0}, .softness = 40});
    cmd.extract_mask(shape, selection, {.mode = MaskMode::alpha}); // A soft round selection.
    ctx.submit_and_wait(cmd);
    ctx.destroy(shape);

    // A 1280 x 720 texture your UI samples; the Display keeps its view alive.
    auto display = webgpu::Display::create(ctx, 1280, 720);
    // Fit the canvas (zoom 24%), centered; view maps canvas pixels to display pixels.
    const float zoom = 0.24f;
    webgpu::ViewportOptions view{
        .view = Affine::translate((1280 - 4000 * zoom) / 2, (720 - 3000 * zoom) / 2) *
                Affine::scale(zoom),
        .overlay = &selection, // Tints unselected areas red at 50%, like Quick Mask.
    };
    display.reserve(canvas.size(), view); // Explicit cache reservation; draw never grows it.
    display.draw(canvas, view);
    display.wait();

    // Zoom to 800% around canvas point (1000, 800), rotated 15 degrees, with a pixel grid.
    view.view = Affine::translate(640, 360) * Affine::rotate(15) * Affine::scale(8) *
                Affine::translate(-1000, -800);
    view.overlay = nullptr;
    view.pixel_grid = true;
    display.reserve(canvas.size(), view); // Explicit cache reservation; draw never grows it.
    display.draw(canvas, view);
    display.wait(); // Wait before editing the canvas again.
    display.close();
    ctx.destroy(selection);
    ctx.destroy(canvas);
}

# Budget a GPU cache

Count requested GPU bytes by category, evict a cached image before reserving its replacement, and enforce a hard allocation budget.

#include <wgpupixel.h>
#include <iostream>

using namespace wgpupixel;

int main() {
    auto ctx = Context::create();
    constexpr std::uint64_t budget = 256ull * 1024 * 1024;
    ctx.set_memory_limit(budget);
    auto cached = ctx.create_image({3840, 2160});
    ctx.reset_peak();

    constexpr ImageSize next_size{4096, 2160};
    constexpr std::uint64_t next_bytes = 4096ull * 2160 * 16;
    const auto limits = ctx.limits();
    if (next_size.width > limits.max_image_dimension ||
        next_size.height > limits.max_image_dimension ||
        next_bytes / 16 > limits.max_image_pixels) return 1;
    const auto usage = ctx.memory();
    bool evicted = usage.total > budget || next_bytes > budget - usage.total;
    if (evicted) ctx.destroy(cached); // Wait for any submissions using it first.
    auto replacement = ctx.create_image(next_size);
    if (!evicted) ctx.destroy(cached);
    std::cout << "GPU bytes: " << ctx.memory().total
              << "; peak: " << ctx.memory().peak << '\n';
    ctx.destroy(replacement);
}

# Transfer pixels

Upload straight-alpha sRGB RGBA8 bytes, edit in linear float32, and download RGBA8. Submit and wait before reading; transfer buffers can be reused.

// Upload RGBA8 pixels, process in float32, and download RGBA8 pixels.

#include <wgpupixel.h>
#include <iostream>
#include <vector>

using namespace wgpupixel;

int main() {
    // sRGB RGBA8: an opaque white image at 1920 × 1080.
    std::vector<std::uint8_t> pixels(1920 * 1080 * 4, 255);
    std::vector<std::uint8_t> result(pixels.size());

    auto ctx = Context::create();
    auto image = ctx.create_image({1920, 1080});
    auto upload = ctx.create_upload_buffer(image);
    auto readback = ctx.create_readback_buffer(image);
    std::cout << "Transfer capacities: " << upload.capacity_pixels() << ", "
              << readback.capacity_pixels() << " pixels, "
              << upload.bytes_per_pixel() << " bytes per uploaded pixel, "
              << readback.bytes_per_pixel() << " bytes per downloaded pixel\n";

    ctx.write(upload, pixels); // Copy CPU bytes into reusable transfer storage.

    auto cmd = ctx.create_commands();
    cmd.upload(upload, image); // GPU conversion: sRGB -> linear, then premultiply alpha.
    cmd.brightness(image, {.amount = -0.25f});
    cmd.download(image, readback); // Unpremultiply, encode sRGB, and quantize to RGBA8.

    auto done = ctx.submit(cmd);
    ctx.wait(done);
    ctx.read(readback, result); // Completed download; result must have the exact byte count.

    std::cout << "First RGBA pixel: " << unsigned(result[0]) << ", " << unsigned(result[1]) << ", "
              << unsigned(result[2]) << ", " << unsigned(result[3]) << '\n';

    ctx.destroy(upload);
    ctx.destroy(readback);
    ctx.destroy(image);
}

# 16-bit, float and partial transfers

Each transfer buffer has a CPU format: rgba8 and rgba16 are straight-alpha sRGB; rgba32_float is linear premultiplied storage, lossless and HDR-safe. An optional region moves one rectangle as tightly packed rows, for tiles and eyedroppers.

#include <wgpupixel.h>
#include <array>
#include <iostream>
#include <vector>

using namespace wgpupixel;

int main() {
    auto ctx = Context::create();
    auto canvas = ctx.create_image({1920, 1080});

    // 16-bit straight-alpha sRGB, as decoded from a 16-bit PNG or TIFF: 8 bytes per pixel.
    std::vector<std::uint16_t> photo(1920 * 1080 * 4, 65535);
    auto upload = ctx.create_upload_buffer(canvas, {.format = TransferFormat::rgba16});
    ctx.write(upload, {reinterpret_cast<const std::uint8_t*>(photo.data()), photo.size() * 2});

    // A tile update needs a buffer for the tile only; rows of the rectangle are packed.
    const Rect tile{512, 256, 256, 256};
    std::vector<std::uint8_t> tile_pixels(256 * 256 * 4, 128); // RGBA8 by default.
    auto tile_upload = ctx.create_upload_buffer(canvas, {.capacity_pixels = 256 * 256});
    ctx.write(tile_upload, tile_pixels);

    // Eyedropper: one linear premultiplied float pixel; values above 1 are preserved.
    auto probe = ctx.create_readback_buffer(
        canvas, {.format = TransferFormat::rgba32_float, .capacity_pixels = 1});

    ctx.run_and_wait([&](Commands& cmd) {
        cmd.upload(upload, canvas);
        cmd.upload(tile_upload, canvas, {.region = tile});
        cmd.exposure(canvas, {.stops = 1});
        cmd.download(canvas, probe, {.region = Rect{600, 300, 1, 1}});
    });
    std::array<float, 4> sample{};
    ctx.read(probe, {reinterpret_cast<std::uint8_t*>(sample.data()), sizeof(sample)});
    std::cout << "Linear premultiplied RGBA: " << sample[0] << ", " << sample[1] << ", "
              << sample[2] << ", " << sample[3] << '\n'; // About 0.217, 0.217, 0.217, 0.502.

    ctx.destroy(probe);
    ctx.destroy(tile_upload);
    ctx.destroy(upload);
    ctx.destroy(canvas);
}

# Coverage masks

An A8 mask stores one linear coverage byte per pixel. Upload, fill, copy, invert, extract and download coverage; the same mask can limit most pixel operations.

#include <wgpupixel.h>

#include <array>
#include <cstdint>
#include <iostream>

using namespace wgpupixel;

int main() {
    auto ctx = Context::create();
    auto image = ctx.create_image({3, 1});
    auto mask = ctx.create_mask({3, 1});
    auto inverse = ctx.create_mask({3, 1});
    auto upload = ctx.create_upload_buffer(mask);
    auto readback = ctx.create_readback_buffer(image);
    auto mask_readback = ctx.create_readback_buffer(mask);
    // Three linear coverage bytes: unchanged, about half, and full effect.
    const std::array<std::uint8_t, 3> coverage{0, 128, 255};
    ctx.write(upload, coverage);

    auto cmd = ctx.create_commands(5);
    cmd.upload(upload, mask);
    cmd.fill(image, {.color = {0, 0, 0.25f, 0.5f}}); // Linear premultiplied half-alpha blue.
    cmd.fill(image, {.color = {0.5f, 0, 0, 0.5f}, .mask = &mask});
    cmd.brightness(image, {.amount = 0.1f, .mask = &mask});
    cmd.download(image, readback);
    ctx.submit_and_wait(cmd);

    std::array<std::uint8_t, 12> output{};
    ctx.read(readback, output); // Exported image pixels are straight sRGB RGBA8.
    for (std::size_t i = 0; i < coverage.size(); ++i) {
        std::cout << "coverage " << unsigned(coverage[i]) << ":";
        for (std::size_t c = 0; c < 4; ++c) {
            std::cout << ' ' << unsigned(output[i * 4 + c]);
        }
        std::cout << '\n';
    }

    // Mask-only operations use the same command and transfer API, one byte per pixel.
    cmd.copy(mask, inverse);
    cmd.invert(inverse); // 0 -> 255; 128 -> 127; 255 -> 0.
    cmd.download(inverse, mask_readback);
    ctx.submit_and_wait(cmd);
    std::array<std::uint8_t, 3> inverted{};
    ctx.read(mask_readback, inverted);
    std::cout << "Inverted coverage:";
    for (auto value : inverted) std::cout << ' ' << unsigned(value);
    std::cout << '\n';

    cmd.fill(mask, {.coverage = 0.5f}); // Uniform coverage, quantized to bytes.
    cmd.extract_mask(image, inverse, {.mode = MaskMode::alpha}); // Or linear luminance.
    // Masked out-of-place operations preserve destination pixels where coverage is zero.
    auto result = ctx.create_image({3, 1});
    cmd.fill(result, {.color = {0, 0, 0, 0}}); // The destination must already contain valid pixels.
    cmd.copy(image, result, {.mask = &inverse});
    ctx.submit_and_wait(cmd);

    mask.set_size({1, 1}); // Logical dimensions only: no resize or clear, capacity stays 3.
    std::cout << mask.size().width << 'x' << mask.size().height << ", capacity "
              << mask.capacity_pixels() << ", bytes/pixel " << mask_readback.bytes_per_pixel() << '\n';

    ctx.destroy(result);
    ctx.destroy(mask_readback);
    ctx.destroy(readback);
    ctx.destroy(upload);
    ctx.destroy(inverse);
    ctx.destroy(mask);
    ctx.destroy(image);
}

# Load and save images

Native file I/O decodes into linear premultiplied pixels and converts tagged colors. Saving writes an 8-bit sRGB PNG. Run with an input filename and output filename.

// Load a file, apply grayscale on the GPU, and save an sRGB PNG.

#include <wgpupixel_io.h>
#include <iostream>

int main(int argc, char** argv) {
    if (argc != 3) {
        std::cerr << "Usage: example-files input output.png\n";
        return 1;
    }
    try {
        using namespace wgpupixel;
        auto ctx = Context::create();
        // Decode, apply orientation/color conversion, and return a caller-owned Image.
        auto image = io::load(ctx, argv[1]);
        auto cmd = ctx.create_commands();
        cmd.grayscale(image);
        ctx.submit_and_wait(cmd); // Finish editing before the synchronous save.
        io::save(ctx, image, argv[2]); // RGBA8 sRGB PNG; overwrites the output path.
        ctx.destroy(image);
    } catch (const wgpupixel::Error& error) {
        std::cerr << error.operation() << ": " << error.what() << '\n';
        return 1;
    }
}

# Own resources explicitly

Image copies alias one allocation. Storage is released when its last owner lets go; recordings and pending submissions retain what they use. After waiting, destroy can release storage explicitly and invalidate all aliases.

// Create handles, move ownership, copy aliases, and release resources.

#include <wgpupixel.h>
#include <utility>

using namespace wgpupixel;

int main() {
    // Default handles are empty. Factories attach them to a context.
    Context context;
    context = Context::create(); // Device now; each pipeline compiles on its first submission.
    context.prepare(); // Optional: compile every pipeline up front. Idempotent.
    auto ctx = std::move(context); // Context is movable, not copyable.
    Image image;
    UploadBuffer upload;
    ReadbackBuffer readback;
    Commands recording;
    Submission pending;
    image = ctx.create_image({1920, 1080});
    upload = ctx.create_upload_buffer(image);
    readback = ctx.create_readback_buffer(image);
    recording = ctx.create_commands(1); // Capacity for one recorded operation.

    // Moving commands transfers the recording; copying an Image only aliases it.
    auto cmd = std::move(recording);
    recording = std::move(cmd);
    // UploadBuffer and ReadbackBuffer copies also alias their original storage.
    auto alias = image;
    recording.fill(alias, {.color = {0.2f, 0.5f, 0.8f, 1.0f}});
    pending = ctx.submit(recording);
    auto ticket = pending; // Submission copies refer to the same completion.
    ctx.wait(ticket);

    ctx.destroy(upload);
    ctx.destroy(readback);
    ctx.destroy(alias); // Invalidates image too; never destroy it twice.
    // Commands release their own bookkeeping when leaving scope.
}

# Reuse an allocation

set_size changes logical dimensions within existing capacity; it does not resample or clear pixels. Revisions track writes, not immutable snapshots.

// Reserve 4K storage once and reuse it for 1080p operations.

#include <wgpupixel.h>
#include <iostream>

using namespace wgpupixel;

int main() {
    auto ctx = Context::create();
    auto work = ctx.create_image({3840, 2160});
    auto cmd = ctx.create_commands(2); // One slot for fill, one for brightness.

    // set_size changes logical dimensions, preserving the allocation and its contents.
    work.set_size({1920, 1080});
    std::cout << work.size().width << " x " << work.size().height
              << ", capacity: " << work.capacity_pixels() << " pixels\n";

    const float adjustments[] = {0.0f, 0.1f, 0.2f};
    for (float amount : adjustments) {
        // Initialize the logical area before applying an in-place adjustment.
        cmd.fill(work, {.color = {0.1f, 0.4f, 0.7f, 1.0f}});
        cmd.brightness(work, {.amount = amount});

        auto done = ctx.submit(cmd);
        std::cout << "Submitted revision: " << work.revision() << '\n';
        ctx.wait(done); // Reuse the same command storage on the next iteration.
    }

    ctx.destroy(work);
}

# Handle an error

Invalid arguments throw structured errors before recording that operation. Inspect code, operation, parameter and message; fix the call before continuing.

// Inspect a validation error, correct its argument, and continue recording.

#include <wgpupixel.h>
#include <iostream>

using namespace wgpupixel;

void describe(const Error& error) {
    std::cout << static_cast<unsigned>(error.code()) << ' '
              << error.operation() << '.' << error.parameter() << ": " << error.what() << '\n';
}

int main() {
    auto ctx = Context::create();
    auto image = ctx.create_image({1920, 1080});
    auto cmd = ctx.create_commands();
    cmd.fill(image, {.color = {0.2f, 0.5f, 0.8f, 1.0f}});

    try {
        cmd.gaussian_blur(image, {.radius = 18, .sigma = 6.0f}); // Required workspace was omitted.
    } catch (const Error& error) {
        if (error.code() != ErrorCode::capacity) throw;
        describe(error);
    }

    // Failed validation added no commands. Correct the argument and continue.
    auto workspace = ctx.create_workspace(gaussian_blur_requirements(image.size(), {.radius = 18, .sigma = 6.0f}).workspace);
    cmd.gaussian_blur(image, {.radius = 18, .sigma = 6.0f, .workspace = workspace});
    ctx.submit_and_wait(cmd);
    ctx.destroy(workspace);
    ctx.destroy(image);

    // An application adapter can also construct the same error type.
    describe(Error(ErrorCode::invalid_argument, "adapter", "opacity", "Expected a finite value"));
}

# Draw text on an image

Load a font and composite glyph coverage on the GPU. The raster origin preserves the layout position. Run with a font filename; link wgpupixel::text.

#include <wgpupixel_text.h>
#include <array>
#include <fstream>
#include <iostream>
#include <iterator>

using namespace wgpupixel;
int main(int argc, char** argv) {
    if (argc != 2) {
        std::cerr << "Usage: example_text /path/to/font.ttf\n";
        return 1;
    }
    try {
        text::FontCollection fonts;
        const auto families = fonts.add_file(argv[1]);
        text::TextStyle style;
        style.family = families.at(0);
        style.size_px = 48;
        auto layout = fonts.layout("Hello, pixels", style);
        const auto raster = layout.rasterize(text::RasterFormat::a8);
        if (raster.pixels.empty()) {
            return 0;
        }

        auto ctx = Context::create();
        auto mask = ctx.create_mask({raster.width, raster.height});
        auto layer = ctx.create_image({raster.width, raster.height});
        auto canvas = ctx.create_image({640, 240});
        auto upload = ctx.create_upload_buffer(mask);
        ctx.write(upload, raster.pixels);
        auto commands = ctx.create_commands(5);
        commands.upload(upload, mask);
        commands.fill(layer, {.color = {0, 0, 0, 0}});
        // Color is linear premultiplied; glyph coverage multiplies all four channels.
        commands.fill(layer, {.color = {0.4f, 0.15f, 0.05f, 0.5f}, .mask = &mask});
        commands.fill(canvas, {.color = {0.08f, 0.08f, 0.08f, 1}});
        // Raster origin preserves bearings and baseline relative to layout position.
        commands.blend(layer, canvas, {.position = {24 + raster.origin_x, 24 + raster.origin_y}});
        ctx.submit_and_wait(commands);
        auto readback = ctx.create_readback_buffer(canvas);
        auto download = ctx.create_commands(1);
        download.download(canvas, readback);
        ctx.submit_and_wait(download);
        std::vector<std::uint8_t> pixels(std::size_t(canvas.size().width) * canvas.size().height * 4);
        ctx.read(readback, pixels); // Straight-alpha sRGB RGBA8, ready for an encoder.
        std::cout << families.at(0) << ": " << layout.metrics().line_count << " lines, "
                  << pixels.size() << " composited RGBA bytes\n";
        ctx.destroy(readback);
        ctx.destroy(upload);
        ctx.destroy(mask);
        ctx.destroy(layer);
        ctx.destroy(canvas);
    } catch (const Error& error) {
        std::cerr << error.what() << '\n';
        return 1;
    }
}

# Style and edit text

Wrapping, UTF-8 style ranges, hit testing, carets and selections. Ranges replace the complete style. Run with a font filename.

#include <wgpupixel_text.h>
#include <array>
#include <fstream>
#include <iostream>
#include <iterator>

using namespace wgpupixel;
int main(int argc, char** argv) {
    if (argc != 2) {
        std::cerr << "Usage: example_text_layout /path/to/font.ttf\n";
        return 1;
    }
    try {
        text::FontCollection fonts;
        const auto families = fonts.add_file(argv[1]);
        text::TextStyle style;
        style.family = families.at(0);
        style.size_px = 48;
        text::ParagraphStyle paragraph;
        paragraph.width_px = 480;
        paragraph.wrap = text::Wrap::word_char;
        paragraph.alignment = text::Alignment::left;
        paragraph.direction = text::Direction::auto_detect;
        // Ranges replace the whole style and use UTF-8 byte offsets.
        auto accent = style;
        accent.weight = 700;
        accent.color = {0.8f, 0.2f, 0.05f, 1};
        const std::array ranges{text::StyleRange{0, 8, accent}};
        auto layout = fonts.layout("External fonts\nEditable UTF-8 text", style, paragraph, ranges);
        const auto metrics = layout.metrics();
        const auto hit = layout.hit_test(30, 20);
        const auto caret = layout.caret(hit.byte_index);
        const auto selection = layout.selection(0, 8);
        // hit.trailing counts Unicode characters within the grapheme, not bytes.
        std::cout << "Hit byte " << hit.byte_index << ", leading caret x " << caret.strong.x
                  << ", selection rectangles " << selection.size() << '\n';
        // RGBA preserves style colors; use A8 when only glyph coverage is needed.
        const auto colored = layout.rasterize(text::RasterFormat::rgba8);
        std::cout << metrics.line_count << " lines, " << colored.width << " x " << colored.height << ", RGBA stride "
                  << colored.stride << " bytes\n";
    } catch (const Error& error) {
        std::cerr << error.what() << '\n';
        return 1;
    }
}

# Load a font from memory

Fonts are copied into a private collection. List the registered families without modifying system fonts. Run with a font filename.

#include <wgpupixel_text.h>
#include <array>
#include <fstream>
#include <iostream>
#include <iterator>

using namespace wgpupixel;
int main(int argc, char** argv) {
    if (argc != 2) {
        std::cerr << "Usage: example_font_memory /path/to/font.ttf\n";
        return 1;
    }
    try {
        std::ifstream file(argv[1], std::ios::binary);
        const std::vector<std::uint8_t> bytes(std::istreambuf_iterator<char>{file}, {});
        text::FontCollection memory_fonts;
        const auto registered = memory_fonts.add_bytes(bytes);
        std::cout << registered.at(0) << ": " << memory_fonts.families().size()
                  << " memory font families\n";
    } catch (const Error& error) {
        std::cerr << error.what() << '\n';
        return 1;
    }
}

# Present into your texture

Use the context’s borrowed device and queue. Keep target textures alive until the draw completes. The renderer receives a GPU texture, without a pixel download.

// Draw a 1080p image into a texture for a UI renderer.

#include <wgpupixel_webgpu.h>
#include <stdexcept>
#include <utility>

using namespace wgpupixel;

struct Target {
    WGPUTexture texture = nullptr;
    WGPUTextureView view = nullptr;
    ~Target() {
        if (view) wgpuTextureViewRelease(view);
        if (texture) wgpuTextureRelease(texture);
    }
};

int main() {
    auto ctx = Context::create();
    auto gpu = webgpu::native_context(ctx); // Borrowed instance, adapter, device, queue.
    auto image = ctx.create_image({1920, 1080});
    auto cmd = ctx.create_commands();

    // A UI renderer on this device can sample the result as a texture.
    Target target;
    WGPUTextureDescriptor desc = WGPU_TEXTURE_DESCRIPTOR_INIT;
    desc.size = {1920, 1080, 1};
    desc.format = WGPUTextureFormat_RGBA8Unorm;
    desc.usage = WGPUTextureUsage_RenderAttachment | WGPUTextureUsage_TextureBinding;
    target.texture = wgpuDeviceCreateTexture(gpu.device, &desc);
    if (!target.texture) throw std::runtime_error("Could not create display texture");
    target.view = wgpuTextureCreateView(target.texture, nullptr);

    // Create the presenter once for this target format and reuse it across frames.
    webgpu::Presenter presenter;
    presenter = webgpu::Presenter::create(ctx, desc.format);
    auto display = std::move(presenter); // Presenter is movable, not copyable.
    cmd.fill(image, {.color = {0.2f, 0.5f, 0.8f, 1.0f}});
    auto edited = ctx.submit(cmd);
    // The shared queue orders edits before drawing. Keep the target alive until wait.
    ctx.wait(display.draw(image, target.view, 1920, 1080));
    ctx.wait(edited);

    ctx.destroy(image);
    // target.view contains the displayed pixels for the application's renderer.
}

# Give the UI a display texture

Display owns a presentation texture. Register its borrowed view in a UI renderer on the same device; unregister it before close. Wait before editing its input image again.

#include <wgpupixel_webgpu.h>
using namespace wgpupixel;
int main() {
    auto ctx = Context::create();
    auto image = ctx.create_image({320, 200});
    auto cmd = ctx.create_commands();
    cmd.fill(image, {.color = {0.05f, 0.2f, 0.5f, 1}});
    ctx.submit_and_wait(cmd);
    auto display = webgpu::Display::create(ctx, 320, 200);
    const auto presented = display.draw(image);
    if (!ctx.is_complete(presented)) ctx.wait(presented);
    auto view = display.view(); // Borrow this in your UI, using this same device.
    (void)view;
    // Unregister the view from your UI before releasing it.
    display.close();
    ctx.destroy(image);
}