# 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.
# 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);
}
# 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"));
}
# Load and save images
Native file I/O decodes into linear premultiplied float32 pixels and converts tagged colors. Integer PNG/TIFF inputs retain their 8- or 16-bit precision. Untagged integers use the piecewise sRGB EOTF, including 8-bit grayscale TIFF, which previously used gamma 2.2. Embedded RGB/grayscale ICC profiles and supported linear or gamma transfer tags take precedence. Untagged RGB uses sRGB/Rec.709 primaries. 16-bit ICC conversions produce linear float32 directly, avoiding an intermediate integer sRGB LUT and retaining out-of-sRGB-gamut RGB for matrix/shaper profiles. Integer input can still use profiles with tables/LUTs, whose own quantization and domain limits apply. Straight 8-bit ICC input continues to convert through 8-bit sRGB.
Half/float EXR and linear float TIFF preserve HDR and negative RGB through float32
uploads. Untagged float TIFF is assumed linear. Float ICC input is accepted only
with an XYZ matrix/shaper profile whose TRCs are analytic parametric curves and
which has no AToB/DToB input LUT tags, including profiles containing both matrix and
LUT tags. Table TRCs and LUT profiles are rejected with io_failed, even for samples
within [0,1], because they can clip HDR/negatives and quantize float samples.
Nonlinear parametric ICC conversion follows LittleCMS's curve definitions: sRGB
extends its linear toe below zero, while pure gamma clamps negatives to zero.
Color conversion can round float samples; it does not promise bitwise identity.
ICC input and output samples, including alpha and premultiplied results, must be
finite with magnitude at most 1e18; otherwise loading fails with io_failed.
EXR uses premultiplied alpha; TIFF follows ExtraSamples. This includes 8-bit associated-alpha TIFF: encoded RGB is now unpremultiplied before the EOTF/ICC conversion and premultiplied again in linear light. Linear associated float RGB is converted directly, preserving color at zero alpha, with or without ICC. Unreadable optional TIFF EXIF leaves alpha association unspecified, treated as straight. Supported file layouts remain those of the pinned FFmpeg decoders. 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;
}
}
# 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;
}
}
# 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.
}
# 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);
}
# 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);
}
# 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.
}
# 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);
}
# 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;
}
}
# 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);
}
# 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);
}
# 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);
}
# 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});
![]()