The GPU path
daegun hands you data instead of drawing, because owning a device needs unsafe and the engine does not.
use daegun::GpuBatch;
let mut batch = GpuBatch::new();
let slot = font.gpu_glyph(&mut batch, gid, &[])?;
// batch.curves(), batch.bands(), batch.band_curves(), batch.hulls() go to the GPU
// slot.instance(offset, scale, em_pixels, tint) makes one GlyphInstanceShader source for GLSL, HLSL and MSL ships with the crate. Backends for Metal, Vulkan, D3D11 and D3D12 are there too if you would rather daegun drove the device.
More than one font
One batch holds as many faces as you like. A slot is keyed by font, glyph and axis position, so two faces never collide, and a mixed-font page uploads as one geometry and draws in one pass.
Several geometries into one target work too. A target clears before every draw, so give the clear as None and the draw keeps what the target already holds instead. Clear on the first draw, None on the rest, and they layer rather than erase each other.
Drawing into a surface you own
A backend can render straight into your swapchain, with no readback and no copy. This matters more than it sounds: reading the pixels back stalls the pipeline, measured here at 1.6–2.2 ms against 0.06 ms for the draw itself, so an app that presents through the CPU pays more for the trip home than for the work.
Two calls get you there, and both are unsafe because both take raw handles.
use daegun::gpu::{metal::Renderer, Mode};
// Your device, not daegun's. daegun draws on it and never destroys it.
let renderer = unsafe { Renderer::from_device(device) }?;
// Per frame, straight into the drawable.
let mut target = unsafe { renderer.target_from_drawable(drawable, width, height) }?;
target.set_clear(Some(background));
renderer.draw(&mut target, &geometry, &instances, ¶ms, Mode::Subpixel)?;Adoption is the part that makes the rest reachable. A swapchain image belongs to the device its swapchain was created on, so a renderer that picked its own device can never touch your backbuffer. from_device takes yours instead. daegun never destroys what it is handed: it retains where the platform refcounts and borrows where it does not, so the device has to outlive the renderer.
A borrowed target stages nothing, so read_pixels answers BadTarget andpixels comes back empty. That is the point of it. Keep target for offscreen work where you do want the bytes back.
| Backend | You hand it | Presented by |
|---|---|---|
| Metal | CAMetalDrawable, or a bare MTLTexture | daegun |
| Vulkan | A VkImage, through target_from_image | you |
| D3D11 | An ID3D11Texture2D backbuffer | you |
| D3D12 | An ID3D12Resource backbuffer | you |
Metal presents and the others do not, which is deliberate rather than unfinished.presentDrawable: is a command-buffer call, so daegun can order it against its own draw for free and nothing waits on anything. Vulkan presentation is queue-level and semaphore-ordered, and D3D presentation belongs to the swapchain. In both cases you own the synchronization and daegun would only be guessing at it.
Byte order
target gives you RGBA8. target_with_format takesFormat::Rgba8Unorm or Format::Bgra8Unorm, and a borrowed surface takes the format of what you handed over. Most swapchains are BGRA, and CAMetalLayer refuses RGBA8 outright, so matching the surface here beats swizzling every frame on the way out.
Where these live
Under daegun::gpu, one module per backend – metal,vulkan, d3d11 and d3d12 – each gated to the platform that has it, and documented in full in the reference alongside everything else. All four are in the C header too, under a daegun_metal_, daegun_vulkan_, daegun_d3d11_ ordaegun_d3d12_ prefix: _renderer_from_device to adopt,_target_from_texture to borrow one, and _target_from_drawable on Metal for the swapchain path. Vulkan names its borrow _target_from_image, since that is what it takes. The GPU path in C walks the same ground from that side.