The GPU path in C
daegun hands you glyph data rather than drawing, so you can upload it yourself. Backends for Metal, Vulkan, D3D11 and D3D12 are there too if you would rather daegun drove the device, and every call below exists four times over with a daegun_metal_,daegun_vulkan_, daegun_d3d11_ or daegun_d3d12_ prefix.
A frame, end to end
Build a batch once, upload it once, and per frame turn the glyphs you can see into instances. Errors are left out here for length; every call answers daegun_status.
#include "daegun.h"
/* Once. The batch holds the curves, the geometry is that batch on the device. */
daegun_batch *batch = NULL;
daegun_batch_new(&batch);
daegun_glyph_slot slot;
daegun_font_gpu_glyph(font, batch, gid, NULL, 0, &slot);
daegun_metal_renderer *gpu = NULL;
daegun_metal_renderer_new(&gpu);
daegun_metal_geometry *geometry = NULL;
daegun_metal_geometry_new(gpu, batch, &geometry);
/* Per frame. One instance per placement of a glyph. */
const float offset[2] = { 8.0f, 8.0f };
const float em[2] = { 48.0f, 48.0f };
const float white[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
daegun_glyph_instance inst;
daegun_glyph_slot_instance(&slot, offset, 48.0f, em, white, &inst);
daegun_subpixel_params sp;
daegun_subpixel_params_from_layout(DAEGUN_LAYOUT_GRAYSCALE, &sp);
daegun_metal_target *target = NULL;
daegun_metal_target_new(gpu, 256, 256, &target);
daegun_metal_draw(gpu, target, geometry, &inst, 1, &sp, DAEGUN_MODE_SUBPIXEL);
size_t n = 0;
const uint8_t *pixels = daegun_metal_read_pixels(gpu, target, &n); /* BGRA, borrowed */A slot carries no size, so one upload serves every size you draw it at. UseDAEGUN_MODE_SUBPIXEL with a grayscale layout for ordinary source-over blending;DAEGUN_MODE_GRAYSCALE sets no blend state and replaces what it draws over, which with overlapping glyph quads means each one punches a hole through the last.
More than one font, and layering
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.
Several geometries into one target work too. A target clears before every draw, so passNULL to _target_set_clear and the draw keeps what the target already holds instead. Clear on the first draw, NULL on the rest, and they layer rather than erase each other.
const uint8_t background[4] = { 12, 12, 14, 255 };
daegun_metal_target_set_clear(target, background);
daegun_metal_draw(gpu, target, body, instances, count, &sp, DAEGUN_MODE_SUBPIXEL);
daegun_metal_target_set_clear(target, NULL); /* keep what is there */
daegun_metal_draw(gpu, target, title, heading, 4, &sp, DAEGUN_MODE_SUBPIXEL);Drawing into a surface you own
_read_pixels copies the target off the device and blocks until it can. On a discrete GPU the readback dominates the frame outright: 489 us of a 513 us frame at 1024x1024, measured. An app that presents through the CPU pays more for the trip home than for the work. Two calls remove it.
Adopt the device first. A swapchain image belongs to the device its swapchain was created on, so a renderer that picked its own can never touch your backbuffer._renderer_new picks its own; _renderer_from_device takes yours. daegun never destroys what it is handed: it takes a reference where the platform refcounts and borrows where it does not, so keep the device alive past the renderer.
/* Metal: the device is yours, and so is the CAMetalLayer it came from. */
daegun_metal_renderer *gpu = NULL;
daegun_metal_renderer_from_device(device, &gpu);
/* Per frame: wrap the drawable, choose a background, draw. daegun presents it. */
daegun_metal_target *target = NULL;
daegun_metal_target_from_drawable(gpu, drawable, width, height, &target);
const uint8_t background[4] = { 12, 12, 14, 255 };
daegun_metal_target_set_clear(target, background);
daegun_metal_draw(gpu, target, geometry, instances, count, &sp, DAEGUN_MODE_SUBPIXEL);
daegun_metal_target_free(target);A borrowed target stages nothing, which is the point of it: _read_pixels answers NULL with the reason in daegun_last_error, and _target_pixels comes back empty. Keep _target_new for offscreen work where you do want the bytes.
What each backend takes, and who presents
| Backend | Adopt with | Borrow with | Presented by |
|---|---|---|---|
| Metal | void *device | _target_from_drawable, or _target_from_texture | daegun |
| Vulkan | instance, physical device, device, queue family | _target_from_image, taking a uint64_t | you |
| D3D11 | device and its immediate context | _target_from_texture | you |
| D3D12 | device and a direct command queue | _target_from_texture | 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. Do not present the drawable yourself as well. Vulkan presentation is queue-level and semaphore-ordered, and D3D presentation belongs to the swapchain, so there you own the synchronization and daegun would only be guessing at it.
Two more things belong to you rather than to daegun. Vulkan leaves the image inVK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, and D3D12 leaves the resource inD3D12_RESOURCE_STATE_RENDER_TARGET, so transitioning either one to present is yours to arrange.
/* Vulkan: an image handle by value, and the format named explicitly. */
daegun_vulkan_renderer *gpu = NULL;
daegun_vulkan_renderer_from_device(instance, physical, device, queue_family,
/* dual_src_blend */ 1, &gpu);
daegun_vulkan_target *target = NULL;
daegun_vulkan_target_from_image(gpu, (uint64_t)image, width, height,
DAEGUN_SURFACE_BGRA8, &target);
/* Direct3D: a COM pointer, and the format named the same way. */
daegun_d3d11_renderer *gpu11 = NULL;
daegun_d3d11_renderer_from_device(d3d_device, d3d_context, &gpu11);
daegun_d3d11_target *back = NULL;
daegun_d3d11_target_from_texture(gpu11, backbuffer, width, height,
DAEGUN_SURFACE_BGRA8, &back);dual_src_blend is what you enabled at device creation, not what the hardware supports: daegun cannot tell them apart, and without it there is no subpixel pipeline.
Byte order
_target_new gives you RGBA8. _target_with_format takesDAEGUN_SURFACE_RGBA8 or DAEGUN_SURFACE_BGRA8, and so does every borrowing call except Metal's, which reads the format off the texture and answersDAEGUN_RANGE if it is neither. Most swapchains are BGRA, and CAMetalLayer refuses RGBA8 outright, so matching the surface here beats swizzling every frame on the way out.
When a device is not there
_renderer_new answers DAEGUN_UNSUPPORTED rather than failing when the platform has no such device, which is an answer and not an error. Vulkan is opened by name at run time, so a machine without the loader answers the same way. Ask_renderer_supports_subpixel before choosing a mode: subpixel output needs dual-source blending, and not every device and driver exposes it.
Every call here is listed in the C reference, grouped by backend.