Concepts
Embedding #
Use this path when your program already owns the window, the event loop, or the GPU. For example, a game engine can draw a Knots UI into its own render passes.
Import the modules #
exe.root_module.addImport("ui", knots.module("ui"));
exe.root_module.addImport("input", knots.module("input"));
exe.root_module.addImport("render", knots.module("render"));
// Only when you draw with the bundled GPU backend:
exe.root_module.addImport("renderer", knots.module("renderer"));
// Only when you also use the Knots window:
exe.root_module.addImport("window", knots.module("window"));
The host loop #
This loop uses the Knots window and the bundled GPU backend, but owns every pass:
var context_ui = try ui.Context.init(gpa, .{});
defer context_ui.deinit();
while (window.isOpen()) {
window.pollEvents(io);
defer window.finishInputFrame();
var frame = try context_ui.beginFrame(.{
.input = try window.collectInput(),
.now_ms = now_ms,
.delta_ns = delta_ns,
.logical_extent = window.getSize(),
.physical_extent = window.getFramebufferSize(),
.content_scale = window.getContentScale(),
});
defer frame.deinit();
try buildUi(&frame);
const output = try context_ui.endFrame(&frame);
// Apply the effects, then draw. See below.
}
ui.Context.Config has two fields: ui (the theme, fonts,
and state lifetimes) and arena_reset_mode.
The input data is borrowed until endFrame or abortFrame. The
output is borrowed until the next beginFrame. Copy the data that you need
for longer.
Apply the effects #
Apply each effect one time, before the next frame:
window.setCursorShape(output.cursor_shape);
if (output.clipboard_write) |text| _ = try window.setClipboardText(gpa, text);
if (output.close) break;
if (output.redraw) scheduleAnotherFrame();
-
text_inputis true when a text field has focus. Show the on-screen keyboard or start text input on platforms that need it. -
capture_pointerandcapture_keyboardare true when the UI uses the pointer or the keyboard. Do not give that input to your game or scene. -
accessibilityis the snapshot for assistive technology. Read Accessibility.
Draw with Painter #
Painter draws a packet with the bundled GPU backend. It works in two
stages:
-
prepareuploads the packet data for one upload slot. It returns aPreparedvalue. -
encoderecords the draw commands into a render pass that you started. You can encode the samePreparedvalue into more than one compatible pass.
const painter = try renderer.Painter.create(gpa, render_context, gpu_frame.uploadSlotCount());
defer painter.destroyAfterWait();
// Each frame:
var submission = try gpu_frame.begin();
const prepared = try painter.prepare(&output.packet, &.{
.width = extent.width,
.height = extent.height,
.content_scale = window.getContentScale(),
.upload_slot = submission.upload_slot,
.frame_context = submission,
.linear_target = false,
});
var pass = try submission.beginRenderPass(.{
.label = "ui",
.color_attachment = .{ .clear_color = .{ 0, 0, 0, 1 } },
});
try painter.encode(&prepared, &pass);
pass.end();
try submission.submit();
Set linear_target = true when the target texture stores linear color, not
sRGB. The
embedded
example is a complete program. It also shows how to create the device, the surface,
and the render context.
Backdrop effects #
encode skips backdrop effects, because a backdrop must read the
pixels behind it. To draw backdrops, set backdrops = true in the prepare
options, and draw into a target that the painter can sample. Then use
encodeSegment with a Painter.Cursor. It stops at each
backdrop group. End the pass, capture the backdrop, and continue in a new pass that
loads the target. renderer.Renderer does this for you.
Ownership and lifetimes #
- Packet data, input slices, image bytes, and callback data are borrowed. Use them before the next frame, or copy them for async work.
- Keep textures and callback resources alive until the GPU completes the work.
-
Complete the previous GPU work for an upload slot before you use the slot again in
prepare. -
Call
Painter.destroyAfterWaitonly after all GPU work is complete. - The painter synchronizes glyph uploads and releases old resources by upload slot. You do not need to acknowledge glyph uploads.
Let Knots own the surface #
If you own the window but not the GPU passes, use renderer.Renderer. It
owns the surface and draws a packet with one call:
const knots_renderer = try renderer.Renderer.create(gpa, render_context, window_handle, width, height, .{});
defer knots_renderer.destroy();
_ = knots_renderer.render(&output.packet, content_scale);
Call resize when the window size changes.
Write a custom renderer #
A renderer for another graphics API reads render.Packet directly. It does
not need the renderer module. It must:
- Draw the vertices, glyph instances, clips, images, and backdrops of the packet.
-
Upload the glyph atlas. Upload only the changed rows when the atlas
base_revisionmatches the revision that it has. Otherwise, upload the full atlas. - Check the packet extensions, and reject commands that it does not support.
Vulkan shader sources #
A custom Vulkan renderer can use the Knots shaders. The build script exports their source paths:
const Knots = @import("knots");
const vertex = Knots.vulkanUIShaderSource(knots, .ui_primitives_vertex);
const fragment = Knots.vulkanUIShaderSource(knots, .ui_primitives_fragment);
// Also: .ui_primitives_instance_vertex, .slug_vertex, .slug_fragment
The shaders are Zig files. Compile them for the spirv32-vulkan target.