Something went wrong. Try again.
Windows, a GPU renderer and text for Zig programs that draw their own interface.
Something went wrong. Try again.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Draws a frame of instanced quads into an offscreen image, and hands the//! image to a `Target` to put on screen.//!//! Everything drawn is a rectangle — a background, a glyph out of the//! atlas, a line under text, a cursor, an image, or a line, disc or//! triangle cut out of one — so there is one pipeline, and one vertex shader//! that makes a quad from its instance. A frame is one draw call, or one more for each//! image in it, since each image is a texture of its own. The atlases are//! uploaded whole when they change, which after the first screenful is//! rarely; an image, once for each picture it shows.//!//! One frame is in flight at a time. A window redraws only when something//! changed, and paced by the compositor's frame callbacks; a second frame//! in flight would buy nothing but latency.
const std = @import("std");const vk = @import("vk");const shaders = @import("shaders");
const Device = @import("Device.zig");const Atlas = @import("../text/Atlas.zig");const images = @import("image.zig");const testing_ = @import("../testing.zig");
const log = std.log.scoped(.renderer);
const Renderer = @This();
/// What the renderer draws in, and so what a target's own image must be/// for a frame to be drawn straight into it.pub const format: vk.Format = .b8g8r8a8_unorm;
device: *Device,render_pass: vk.RenderPass,/// The same, for drawing straight into a target's own image: left in/// GENERAL, for a consumer outside this device.render_pass_direct: vk.RenderPass,set_layout: vk.DescriptorSetLayout,pipeline_layout: vk.PipelineLayout,pipeline: vk.Pipeline,sampler: vk.Sampler,descriptor_pool: vk.DescriptorPool,descriptor_set: vk.DescriptorSet,command_pool: vk.CommandPool,command_buffer: vk.CommandBuffer,fence: vk.Fence,
target: Device.Image = .{},framebuffer: vk.Framebuffer = .null_handle,
atlases: [2]AtlasTexture = .{ .{}, .{} },staging: Device.Buffer = .{},instances: Device.Buffer = .{},
/// Images: the layout of the set each binds, which the drawing of it uses/// in place of `blank`'s, and what it is sampled with — filtered, since an/// image is scaled to its place.image_layout: vk.DescriptorSetLayout,image_sampler: vk.Sampler,/// Their sets come from these, each made when the last was full.image_pools: std.ArrayList(vk.DescriptorPool) = .empty,textures: std.AutoHashMapUnmanaged(images.Key, Texture) = .empty,/// Bound for everything that is not an image: one clear pixel.blank: ?Texture = null,image_staging: Device.Buffer = .{},/// Counts frames, to say when a texture was last drawn.frame: u64 = 0,
/// One rectangle to draw. Matches the vertex inputs of `shaders/cell.zig`.pub const Instance = extern struct { /// Top-left corner, target pixels. x: f32, y: f32, width: f32, height: f32, /// Top-left corner in the atlas, texels. For a line, where it starts, /// and for a triangle, its first corner, in target pixels. u: f32 = 0, v: f32 = 0, /// Straight RGBA. color: [4]u8, kind: Kind, /// For an image, the size of the part drawn, in its pixels; it is /// scaled to the instance's. For a line, where it ends, and for a /// triangle, its second corner, in target pixels. uw: f32 = 0, uh: f32 = 0, /// For a triangle, its third corner, in target pixels. ex: f32 = 0, ey: f32 = 0,
/// What the rectangle is. The low eight bits say which; a line keeps /// its width above them, in sixteenths of a pixel, and a triangle which /// of its edges are antialiased, since there is no other room in the /// instance for either. pub const Kind = enum(u32) { solid = 0, alpha = 1, color = 2, image = 3, /// A line of any angle, its ends at `u`, `v` and `uw`, `uh`; the /// rectangle is only where it may draw. Edges are antialiased. line = 4, /// A filled circle, as big as fits in the rectangle and centered /// in it, its edge antialiased. disc = 5, /// A filled triangle, its corners at `u`, `v`, `uw`, `uh` and `ex`, /// `ey`; the rectangle is only where it may draw. Each edge is /// antialiased, or hard: an edge inside a polygon cut into /// triangles is hard on both sides, so that the two share its /// pixels between them exactly rather than each covering half and /// leaving a seam. triangle = 6, _,
/// Which kind, without what a line or a triangle keeps above the /// low eight bits. pub fn base(k: Kind) Kind { return @fromBackingInt(@backingInt(k) & 0xff); }
test base { try std.testing.expectEqual(Kind.line, Kind.lineOf(2).base()); try std.testing.expectEqual(Kind.triangle, Kind.triangleOf(0b111).base()); try std.testing.expectEqual(Kind.solid, Kind.solid.base()); }
/// A line `w` pixels wide, rounded to a sixteenth of a pixel and /// never less than one. pub fn lineOf(w: f32) Kind { const sixteenths: u32 = @intFromFloat(std.math.clamp(@round(w * 16), 1, 0xffffff)); return @fromBackingInt(@backingInt(Kind.line) | sixteenths << 8); }
test lineOf { try std.testing.expectEqual(1.5, Kind.lineOf(1.5).lineWidth()); // Too thin to see is as thin as it can be, not nothing. try std.testing.expectEqual(1.0 / 16.0, Kind.lineOf(0).lineWidth()); }
/// A line's width in pixels. pub fn lineWidth(k: Kind) f32 { return @as(f32, @floatFromInt(@backingInt(k) >> 8)) / 16; }
test lineWidth { try std.testing.expectEqual(3, Kind.lineOf(3).lineWidth()); try std.testing.expectEqual(0.25, Kind.lineOf(0.25).lineWidth()); }
/// A triangle whose edges are antialiased where `smooth` has a bit /// set: the first for the edge from its first corner to its /// second, the second from its second to its third, the third /// from its third back to its first. pub fn triangleOf(smooth: u3) Kind { return @fromBackingInt(@backingInt(Kind.triangle) | @as(u32, smooth) << 8); }
test triangleOf { // Its first and third edges antialiased; the second, from the // second corner to the third, hard. const k: Kind = .triangleOf(0b101); try std.testing.expectEqual(Kind.triangle, k.base()); try std.testing.expectEqual(0b101, k.smoothEdges()); }
/// Which of a triangle's edges are antialiased, as `triangleOf` /// has them. pub fn smoothEdges(k: Kind) u3 { return @truncate(@backingInt(k) >> 8); }
test smoothEdges { try std.testing.expectEqual(0b111, Kind.triangleOf(0b111).smoothEdges()); try std.testing.expectEqual(0, Kind.triangleOf(0).smoothEdges()); } };
/// A line from `x0`, `y0` to `x1`, `y1`, `w` wide. pub fn line(x0: f32, y0: f32, x1: f32, y1: f32, w: f32, color: [4]u8) Instance { const pad = w / 2 + 1; const left = @min(x0, x1) - pad; const top = @min(y0, y1) - pad; return .{ .x = left, .y = top, .width = @max(x0, x1) + pad - left, .height = @max(y0, y1) + pad - top, .u = x0, .v = y0, .uw = x1, .uh = y1, .color = color, .kind = .lineOf(w), }; }
test line { const l = Instance.line(0, 0, 10, 0, 2, .{ 0, 0, 0, 255 }); // The rectangle reaches past the ends by half the width and a pixel // for the antialiasing. try std.testing.expectEqual(-2, l.x); try std.testing.expectEqual(14, l.width); try std.testing.expectEqual(10, l.uw); try std.testing.expectEqual(2, l.kind.lineWidth()); }
/// A disc of radius `r` centered on `cx`, `cy`. pub fn disc(cx: f32, cy: f32, r: f32, color: [4]u8) Instance { return .{ .x = cx - r, .y = cy - r, .width = 2 * r, .height = 2 * r, .color = color, .kind = .disc }; }
test disc { const d = Instance.disc(5, 5, 3, .{ 0, 0, 0, 255 }); try std.testing.expectEqual(2, d.x); try std.testing.expectEqual(6, d.width); try std.testing.expectEqual(Kind.disc, d.kind); }
/// A triangle with corners at `x0`, `y0`, `x1`, `y1` and `x2`, `y2`, /// in either winding, its edges antialiased as `smooth` says /// (`Kind.triangleOf`). pub fn triangle(x0: f32, y0: f32, x1: f32, y1: f32, x2: f32, y2: f32, smooth: u3, color: [4]u8) Instance { // An antialiased edge reaches half a pixel past where it is. const left = @min(x0, x1, x2) - 1; const top = @min(y0, y1, y2) - 1; return .{ .x = left, .y = top, .width = @max(x0, x1, x2) + 1 - left, .height = @max(y0, y1, y2) + 1 - top, .u = x0, .v = y0, .uw = x1, .uh = y1, .ex = x2, .ey = y2, .color = color, .kind = .triangleOf(smooth), }; }
test triangle { const t = Instance.triangle(0, 0, 10, 0, 0, 10, 0b111, .{ 0, 0, 0, 255 }); try std.testing.expectEqual(-1, t.x); try std.testing.expectEqual(12, t.width); try std.testing.expectEqual(Kind.triangle, t.kind.base()); try std.testing.expectEqual(0b111, t.kind.smoothEdges()); }};
/// How much of the pixel centered on `px`, `py` a line from `x0`, `y0` to/// `x1`, `y1`, `w` wide, covers, as the shader works it out: by the/// distance from the pixel's center to the segment.pub fn lineCoverage(px: f32, py: f32, x0: f32, y0: f32, x1: f32, y1: f32, w: f32) f32 { const dx = x1 - x0; const dy = y1 - y0; const len2 = dx * dx + dy * dy; const t = if (len2 > 0) std.math.clamp(((px - x0) * dx + (py - y0) * dy) / len2, 0, 1) else 0; const ex = px - (x0 + t * dx); const ey = py - (y0 + t * dy); return std.math.clamp(w / 2 + 0.5 - @sqrt(ex * ex + ey * ey), 0, 1);}
test lineCoverage { // A horizontal line two pixels wide along y = 0: all of a pixel on it, // half of one a pixel off it, none of one further away. try std.testing.expectEqual(1, lineCoverage(5, 0, 0, 0, 10, 0, 2)); try std.testing.expectEqual(0.5, lineCoverage(5, 1, 0, 0, 10, 0, 2)); try std.testing.expectEqual(0, lineCoverage(5, 3, 0, 0, 10, 0, 2));}
/// How much of the pixel centered on `px`, `py` a disc centered on `cx`,/// `cy` of radius `r` covers.pub fn discCoverage(px: f32, py: f32, cx: f32, cy: f32, r: f32) f32 { const ex = px - cx; const ey = py - cy; return std.math.clamp(r + 0.5 - @sqrt(ex * ex + ey * ey), 0, 1);}
test discCoverage { try std.testing.expectEqual(1, discCoverage(0, 0, 0, 0, 2)); // Centered on the edge: half in, half out. try std.testing.expectEqual(0.5, discCoverage(2, 0, 0, 0, 2)); try std.testing.expectEqual(0, discCoverage(5, 0, 0, 0, 2));}
/// How much of the pixel centered on `px`, `py` a triangle with corners/// at `x0`, `y0`, `x1`, `y1` and `x2`, `y2` covers, its edges antialiased/// as `smooth` says (`Instance.Kind.triangleOf`): the least of what each/// edge leaves of it. An antialiased edge leaves what its distance from/// the pixel's center says, as a line's does; a hard one leaves all or/// nothing, by which side of it the center is.pub fn triangleCoverage(px: f32, py: f32, x0: f32, y0: f32, x1: f32, y1: f32, x2: f32, y2: f32, smooth: u3) f32 { // Twice the area, signed by the winding, so that inside is the same // side of every edge whichever way round the corners go. const area = (x1 - x0) * (y2 - y0) - (y1 - y0) * (x2 - x0); if (area == 0) return 0; const sign: f32 = if (area > 0) 1 else -1; return @min( edgeCoverage(px, py, x0, y0, x1, y1, sign, smooth & 1 != 0), edgeCoverage(px, py, x1, y1, x2, y2, sign, smooth & 2 != 0), edgeCoverage(px, py, x2, y2, x0, y0, sign, smooth & 4 != 0), );}
test triangleCoverage { // Corners at (0, 0), (10, 0) and (0, 10); a pixel a quarter inside its // first edge, along y = 0. try std.testing.expectEqual(1, triangleCoverage(2, 2, 0, 0, 10, 0, 0, 10, 0b111)); try std.testing.expectEqual(0, triangleCoverage(20, 20, 0, 0, 10, 0, 0, 10, 0b111)); // Antialiased, the edge covers what its distance says; hard, all of // the pixel, since its center is inside. try std.testing.expectEqual(0.75, triangleCoverage(5, 0.25, 0, 0, 10, 0, 0, 10, 0b001)); try std.testing.expectEqual(1, triangleCoverage(5, 0.25, 0, 0, 10, 0, 0, 10, 0b000));}
/// What the edge from `x0`, `y0` to `x1`, `y1` of a triangle wound as/// `sign` says leaves of the pixel centered on `px`, `py`.fn edgeCoverage(px: f32, py: f32, x0: f32, y0: f32, x1: f32, y1: f32, sign: f32, smooth: bool) f32 { const dx = (x1 - x0) * sign; const dy = (y1 - y0) * sign; if (smooth) { // Positive inside. const cross = dx * (py - y0) - dy * (px - x0); return std.math.clamp(cross / @sqrt(dx * dx + dy * dy) + 0.5, 0, 1); } // Worked out from the same end of the edge in both the triangles that // share it, whichever way each has it running, so that the two come // to exactly opposite answers. From each's own first corner, rounding // can put a center just inside both, and it is drawn twice. const forward = x0 < x1 or (x0 == x1 and y0 < y1); const ox, const oy, const ex, const ey = if (forward) .{ x0, y0, x1, y1 } else .{ x1, y1, x0, y0 }; const raw = (ex - ox) * (py - oy) - (ey - oy) * (px - ox); const cross = if (forward) raw * sign else -raw * sign; // A center exactly on a hard edge belongs to one side of it only: the // triangle that has the edge running one way, and not the one that // has it running the other. if (cross == 0) return if (dy > 0 or (dy == 0 and dx < 0)) 1 else 0; return if (cross > 0) 1 else 0;}
comptime { std.debug.assert(@sizeOf(Instance) == 48);}
/// Instances drawn together: from `first` to where the next run starts, or/// the end. `image` is the texture an image instance among them is drawn/// from; null, they draw from the atlases.pub const Run = struct { first: u32, image: ?images.Key = null,};
/// An image on the GPU.const Texture = struct { image: Device.Image, set: vk.DescriptorSet, pool: vk.DescriptorPool, /// Which of its pictures it holds; a new one for each change of its pixels. generation: u64 = 0, /// The frame it was last drawn in. used: u64 = 0,};
/// A texture not drawn for this many frames goes, and is uploaded again/// if it is wanted after all; and so does every one not drawn in the/// frame just gone, while they take more than `texture_budget` bytes.const texture_age = 600;const texture_budget = 512 * 1024 * 1024;
const AtlasTexture = struct { image: Device.Image = .{}, /// The atlas's `modified` and `resized` counts when it was last /// uploaded. modified: u64 = std.math.maxInt(u64), resized: u64 = std.math.maxInt(u64),};
/// Puts a finished frame somewhere.////// `acquire` comes first, and says where the frame goes: straight into the/// target's own image, through a framebuffer it makes with the render pass/// it is given; or, when it returns none, into the renderer's offscreen/// image, which `record` then copies out. `finish` follows the submit.pub const Target = struct { context: *anyopaque, acquire: *const fn (context: *anyopaque, width: u32, height: u32, render_pass: vk.RenderPass) anyerror!Acquired, /// Called only when there was no framebuffer: with the render pass /// ended and the offscreen image in TRANSFER_SRC_OPTIMAL. record: *const fn (context: *anyopaque, cmd: vk.CommandBuffer, image: vk.Image, width: u32, height: u32) anyerror!void, /// Called after the submit, with the fence that signals its end. finish: *const fn (context: *anyopaque, fence: vk.Fence) anyerror!void,};
/// What a target's `acquire` says about the frame: where it is drawn, and/// how its submit is synchronized with the target's consumer.pub const Acquired = struct { direct: ?Direct = null, sync: Sync = .{},};
/// A target's own image, to draw into. It belongs to a consumer outside/// this device — a compositor — between frames, so the renderer takes it/// from the foreign queue family before drawing and gives it back after.pub const Direct = struct { image: vk.Image, framebuffer: vk.Framebuffer,};
/// What a frame's submit waits for before it writes the target, and/// signals when it is done, for a target whose consumer is outside this/// device.pub const Sync = struct { /// A binary semaphore to wait on before writing the target. wait: ?vk.Semaphore = null, /// A semaphore to signal, and for a timeline the value to signal it to. signal: ?vk.Semaphore = null, signal_value: ?u64 = null,};
/// The one dependency both render passes have. Identical in each, because/// pipelines are compatible only with render passes whose dependencies/// match, and one pipeline draws in both: the copy out reads what was drawn,/// and the release to the compositor follows it.const after_drawing: vk.SubpassDependency = .{ .src_subpass = 0, .dst_subpass = vk.SUBPASS_EXTERNAL, .src_stage_mask = .{ .color_attachment_output = true }, .dst_stage_mask = .{ .transfer = true, .bottom_of_pipe = true }, .src_access_mask = .{ .color_attachment_write = true }, .dst_access_mask = .{ .transfer_read = true },};
/// The render passes, pipeline, samplers and command buffer a window's/// frames are drawn with, on `device`. Nothing is drawn into, and no image/// made, until the first `draw`.pub fn init(device: *Device) !Renderer { const d = device.device;
const render_pass = try d.createRenderPass(&.{ .attachment_count = 1, .p_attachments = &.{.{ .format = format, .samples = .{ .@"1" = true }, .load_op = .clear, .store_op = .store, .stencil_load_op = .dont_care, .stencil_store_op = .dont_care, .initial_layout = .undefined, .final_layout = .transfer_src_optimal, }}, .subpass_count = 1, .p_subpasses = &.{.{ .pipeline_bind_point = .graphics, .color_attachment_count = 1, .p_color_attachments = &.{.{ .attachment = 0, .layout = .color_attachment_optimal }}, }}, .dependency_count = 1, .p_dependencies = &.{after_drawing}, }, null); errdefer d.destroyRenderPass(render_pass, null);
// Compatible with the one above — the same attachment, format and // samples — so the one pipeline draws in either. const render_pass_direct = try d.createRenderPass(&.{ .attachment_count = 1, .p_attachments = &.{.{ .format = format, .samples = .{ .@"1" = true }, .load_op = .clear, .store_op = .store, .stencil_load_op = .dont_care, .stencil_store_op = .dont_care, .initial_layout = .undefined, .final_layout = .general, }}, .subpass_count = 1, .p_subpasses = &.{.{ .pipeline_bind_point = .graphics, .color_attachment_count = 1, .p_color_attachments = &.{.{ .attachment = 0, .layout = .color_attachment_optimal }}, }}, .dependency_count = 1, .p_dependencies = &.{after_drawing}, }, null); errdefer d.destroyRenderPass(render_pass_direct, null);
const bindings = [_]vk.DescriptorSetLayoutBinding{ .{ .binding = 0, .descriptor_type = .combined_image_sampler, .descriptor_count = 1, .stage_flags = .{ .fragment = true } }, .{ .binding = 1, .descriptor_type = .combined_image_sampler, .descriptor_count = 1, .stage_flags = .{ .fragment = true } }, }; const set_layout = try d.createDescriptorSetLayout(&.{ .binding_count = bindings.len, .p_bindings = &bindings }, null); errdefer d.destroyDescriptorSetLayout(set_layout, null);
const image_binding = [_]vk.DescriptorSetLayoutBinding{ .{ .binding = 0, .descriptor_type = .combined_image_sampler, .descriptor_count = 1, .stage_flags = .{ .fragment = true } }, }; const image_layout = try d.createDescriptorSetLayout(&.{ .binding_count = image_binding.len, .p_bindings = &image_binding }, null); errdefer d.destroyDescriptorSetLayout(image_layout, null);
const pipeline_layout = try d.createPipelineLayout(&.{ .set_layout_count = 2, .p_set_layouts = &.{ set_layout, image_layout }, .push_constant_range_count = 1, .p_push_constant_ranges = &.{.{ .stage_flags = .{ .vertex = true }, .offset = 0, .size = 8 }}, }, null); errdefer d.destroyPipelineLayout(pipeline_layout, null);
const pipeline = try createPipeline(d, render_pass, pipeline_layout); errdefer d.destroyPipeline(pipeline, null);
const sampler = try d.createSampler(&.{ .mag_filter = .nearest, .min_filter = .nearest, .mipmap_mode = .nearest, .address_mode_u = .clamp_to_edge, .address_mode_v = .clamp_to_edge, .address_mode_w = .clamp_to_edge, .mip_lod_bias = 0, .anisotropy_enable = .false, .max_anisotropy = 1, .compare_enable = .false, .compare_op = .always, .min_lod = 0, .max_lod = 0, .border_color = .float_transparent_black, .unnormalized_coordinates = .false, }, null); errdefer d.destroySampler(sampler, null); const image_sampler = try d.createSampler(&.{ .mag_filter = .linear, .min_filter = .linear, .mipmap_mode = .nearest, .address_mode_u = .clamp_to_edge, .address_mode_v = .clamp_to_edge, .address_mode_w = .clamp_to_edge, .mip_lod_bias = 0, .anisotropy_enable = .false, .max_anisotropy = 1, .compare_enable = .false, .compare_op = .always, .min_lod = 0, .max_lod = 0, .border_color = .float_transparent_black, .unnormalized_coordinates = .false, }, null); errdefer d.destroySampler(image_sampler, null);
const descriptor_pool = try d.createDescriptorPool(&.{ .max_sets = 1, .pool_size_count = 1, .p_pool_sizes = &.{.{ .type = .combined_image_sampler, .descriptor_count = 2 }}, }, null); errdefer d.destroyDescriptorPool(descriptor_pool, null); var descriptor_set: vk.DescriptorSet = undefined; try d.allocateDescriptorSets(&.{ .descriptor_pool = descriptor_pool, .descriptor_set_count = 1, .p_set_layouts = &.{set_layout}, }, @ptrCast(&descriptor_set));
const command_pool = try d.createCommandPool(&.{ .flags = .{ .reset_command_buffer = true }, .queue_family_index = device.queue_family, }, null); errdefer d.destroyCommandPool(command_pool, null); var command_buffer: vk.CommandBuffer = undefined; try d.allocateCommandBuffers(&.{ .command_pool = command_pool, .level = .primary, .command_buffer_count = 1, }, @ptrCast(&command_buffer));
const fence = try d.createFence(&.{ .flags = .{ .signaled = true } }, null);
return .{ .device = device, .render_pass = render_pass, .render_pass_direct = render_pass_direct, .set_layout = set_layout, .pipeline_layout = pipeline_layout, .pipeline = pipeline, .sampler = sampler, .descriptor_pool = descriptor_pool, .descriptor_set = descriptor_set, .command_pool = command_pool, .command_buffer = command_buffer, .fence = fence, .image_layout = image_layout, .image_sampler = image_sampler, };}
test init { try testing_.live(); const device = try Device.init(std.testing.allocator, .{ .application_name = "aerie doctest" }); defer device.deinit(); var renderer = try Renderer.init(device); defer renderer.deinit();}
/// Waits for the frame in flight, if there is one, then destroys/// everything the renderer made: its images, textures and buffers among/// them. The device outlives it.pub fn deinit(r: *Renderer) void { const dev = r.device; const d = dev.device; _ = d.waitForFences(&.{r.fence}, .true, std.math.maxInt(u64)) catch {}; for (&r.atlases) |*a| a.image.destroy(dev); var it = r.textures.valueIterator(); while (it.next()) |t| t.image.destroy(dev); r.textures.deinit(dev.gpa); if (r.blank) |*b| b.image.destroy(dev); for (r.image_pools.items) |pool| d.destroyDescriptorPool(pool, null); r.image_pools.deinit(dev.gpa); r.image_staging.destroy(dev); d.destroySampler(r.image_sampler, null); d.destroyDescriptorSetLayout(r.image_layout, null); r.staging.destroy(dev); r.instances.destroy(dev); if (r.framebuffer != .null_handle) d.destroyFramebuffer(r.framebuffer, null); r.target.destroy(dev); d.destroyFence(r.fence, null); d.destroyCommandPool(r.command_pool, null); d.destroyDescriptorPool(r.descriptor_pool, null); d.destroySampler(r.sampler, null); d.destroyPipeline(r.pipeline, null); d.destroyPipelineLayout(r.pipeline_layout, null); d.destroyDescriptorSetLayout(r.set_layout, null); d.destroyRenderPass(r.render_pass, null); d.destroyRenderPass(r.render_pass_direct, null); r.* = undefined;}
test deinit { try testing_.live(); const device = try Device.init(std.testing.allocator, .{ .application_name = "aerie doctest" }); defer device.deinit(); var renderer = try Renderer.init(device); renderer.deinit();}
fn createPipeline(d: vk.DeviceProxy, render_pass: vk.RenderPass, layout: vk.PipelineLayout) !vk.Pipeline { const cell = try d.createShaderModule(&.{ .code_size = shaders.cell.len, .p_code = @ptrCast(&shaders.cell), }, null); defer d.destroyShaderModule(cell, null);
const stages = [_]vk.PipelineShaderStageCreateInfo{ .{ .stage = .{ .vertex = true }, .module = cell, .p_name = "vertex" }, .{ .stage = .{ .fragment = true }, .module = cell, .p_name = "fragment" }, }; const attributes = [_]vk.VertexInputAttributeDescription{ .{ .location = 0, .binding = 0, .format = .r32g32_sfloat, .offset = @offsetOf(Instance, "x") }, .{ .location = 1, .binding = 0, .format = .r32g32_sfloat, .offset = @offsetOf(Instance, "width") }, .{ .location = 2, .binding = 0, .format = .r32g32_sfloat, .offset = @offsetOf(Instance, "u") }, .{ .location = 3, .binding = 0, .format = .r8g8b8a8_unorm, .offset = @offsetOf(Instance, "color") }, .{ .location = 4, .binding = 0, .format = .r32_uint, .offset = @offsetOf(Instance, "kind") }, .{ .location = 5, .binding = 0, .format = .r32g32_sfloat, .offset = @offsetOf(Instance, "uw") }, .{ .location = 6, .binding = 0, .format = .r32g32_sfloat, .offset = @offsetOf(Instance, "ex") }, }; const dynamic = [_]vk.DynamicState{ .viewport, .scissor }; var pipeline: vk.Pipeline = undefined; _ = try d.createGraphicsPipelines(.null_handle, &.{.{ .stage_count = stages.len, .p_stages = &stages, .p_vertex_input_state = &.{ .vertex_binding_description_count = 1, .p_vertex_binding_descriptions = &.{.{ .binding = 0, .stride = @sizeOf(Instance), .input_rate = .instance }}, .vertex_attribute_description_count = attributes.len, .p_vertex_attribute_descriptions = &attributes, }, .p_input_assembly_state = &.{ .topology = .triangle_list, .primitive_restart_enable = .false }, .p_viewport_state = &.{ .viewport_count = 1, .scissor_count = 1 }, .p_rasterization_state = &.{ .depth_clamp_enable = .false, .rasterizer_discard_enable = .false, .polygon_mode = .fill, .cull_mode = .{}, .front_face = .clockwise, .depth_bias_enable = .false, .depth_bias_constant_factor = 0, .depth_bias_clamp = 0, .depth_bias_slope_factor = 0, .line_width = 1, }, .p_multisample_state = &.{ .rasterization_samples = .{ .@"1" = true }, .sample_shading_enable = .false, .min_sample_shading = 1, .alpha_to_coverage_enable = .false, .alpha_to_one_enable = .false, }, .p_color_blend_state = &.{ .logic_op_enable = .false, .logic_op = .copy, .attachment_count = 1, .p_attachments = &.{.{ .blend_enable = .true, .src_color_blend_factor = .one, .dst_color_blend_factor = .one_minus_src_alpha, .color_blend_op = .add, .src_alpha_blend_factor = .one, .dst_alpha_blend_factor = .one_minus_src_alpha, .alpha_blend_op = .add, .color_write_mask = .{ .r = true, .g = true, .b = true, .a = true }, }}, .blend_constants = .{ 0, 0, 0, 0 }, }, .p_dynamic_state = &.{ .dynamic_state_count = dynamic.len, .p_dynamic_states = &dynamic }, .layout = layout, .render_pass = render_pass, .subpass = 0, .base_pipeline_index = -1, }}, null, @ptrCast(&pipeline)); return pipeline;}
/// Makes the offscreen image `width` by `height`, if it is not already.fn ensureTarget(r: *Renderer, width: u32, height: u32) !void { if (r.target.image != .null_handle and r.target.width == width and r.target.height == height) return; const dev = r.device; if (r.framebuffer != .null_handle) dev.device.destroyFramebuffer(r.framebuffer, null); r.framebuffer = .null_handle; r.target.destroy(dev); r.target = try dev.image(width, height, format, .{ .color_attachment = true, .transfer_src = true }); r.framebuffer = try dev.device.createFramebuffer(&.{ .render_pass = r.render_pass, .attachment_count = 1, .p_attachments = &.{r.target.view}, .width = width, .height = height, .layers = 1, }, null);}
fn ensureBuffer(r: *Renderer, buffer: *Device.Buffer, size: usize, usage: vk.BufferUsageFlags) !void { if (buffer.size >= size) return; buffer.destroy(r.device); // Grown by half again, so that a screen getting busier does not // reallocate every frame. buffer.* = try r.device.hostBuffer(@max(size, buffer.size + buffer.size / 2, 64 * 1024), usage);}
/// Records the upload of an atlas that changed since the last frame. A/// resized atlas gets a new image, and the descriptor set points at it.fn recordAtlas(r: *Renderer, cmd: vk.CommandBuffer, index: usize, atlas: *const Atlas, staging_offset: *usize) !bool { const tex = &r.atlases[index]; if (tex.modified == atlas.modified and tex.resized == atlas.resized) return false; const dev = r.device; const d = dev.device;
var fresh = false; if (tex.image.image == .null_handle or tex.resized != atlas.resized) { tex.image.destroy(dev); tex.image = try dev.image(atlas.size, atlas.size, switch (atlas.format) { .alpha8 => .r8_unorm, .rgba => .r8g8b8a8_unorm, }, .{ .sampled = true, .transfer_dst = true }); fresh = true; }
@memcpy(r.staging.mapped.?[staging_offset.*..][0..atlas.data.len], atlas.data); const barrier_in = vk.ImageMemoryBarrier{ .src_access_mask = .{}, .dst_access_mask = .{ .transfer_write = true }, .old_layout = .undefined, .new_layout = .transfer_dst_optimal, .src_queue_family_index = vk.QUEUE_FAMILY_IGNORED, .dst_queue_family_index = vk.QUEUE_FAMILY_IGNORED, .image = tex.image.image, .subresource_range = Device.color_range, }; d.cmdPipelineBarrier(cmd, .{ .top_of_pipe = true }, .{ .transfer = true }, .{}, null, null, &.{barrier_in}); d.cmdCopyBufferToImage(cmd, r.staging.buffer, tex.image.image, .transfer_dst_optimal, &.{.{ .buffer_offset = staging_offset.*, .buffer_row_length = 0, .buffer_image_height = 0, .image_subresource = .{ .aspect_mask = .{ .color = true }, .mip_level = 0, .base_array_layer = 0, .layer_count = 1 }, .image_offset = .{ .x = 0, .y = 0, .z = 0 }, .image_extent = .{ .width = atlas.size, .height = atlas.size, .depth = 1 }, }}); var barrier_out = barrier_in; barrier_out.src_access_mask = .{ .transfer_write = true }; barrier_out.dst_access_mask = .{ .shader_read = true }; barrier_out.old_layout = .transfer_dst_optimal; barrier_out.new_layout = .shader_read_only_optimal; d.cmdPipelineBarrier(cmd, .{ .transfer = true }, .{ .fragment_shader = true }, .{}, null, null, &.{barrier_out});
staging_offset.* += std.mem.alignForward(usize, atlas.data.len, 16); tex.modified = atlas.modified; tex.resized = atlas.resized; return fresh;}
/// Whether the image `key` is held as of `generation`: for an application/// deciding which images' pixels it has to upload.pub fn holds(r: *const Renderer, key: images.Key, generation: u64) bool { const t = r.textures.get(key) orelse return false; return t.generation == generation;}
test holds { try testing_.live(); const device = try Device.init(std.testing.allocator, .{ .application_name = "aerie doctest" }); defer device.deinit(); var renderer = try Renderer.init(device); defer renderer.deinit(); // Nothing is held until a frame has uploaded it. try std.testing.expect(!renderer.holds(.{ .owner = 1, .id = 7 }, 0));}
/// `holds`, behind the interface that whatever decides on uploads takes,/// so that it need not know about the renderer.pub fn held(r: *const Renderer) images.Held { return .{ .context = r, .has = struct { fn has(context: *const anyopaque, key: images.Key, generation: u64) bool { const self: *const Renderer = @ptrCast(@alignCast(context)); return self.holds(key, generation); } }.has };}
test held { try testing_.live(); const device = try Device.init(std.testing.allocator, .{ .application_name = "aerie doctest" }); defer device.deinit(); var renderer = try Renderer.init(device); defer renderer.deinit(); const h = renderer.held(); try std.testing.expect(!h.has(h.context, .{ .owner = 1, .id = 7 }, 0));}
/// A descriptor set for a texture, from whichever pool has room.fn imageSet(r: *Renderer) !struct { vk.DescriptorSet, vk.DescriptorPool } { const d = r.device.device; for (r.image_pools.items) |pool| { var set: vk.DescriptorSet = undefined; d.allocateDescriptorSets(&.{ .descriptor_pool = pool, .descriptor_set_count = 1, .p_set_layouts = &.{r.image_layout}, }, @ptrCast(&set)) catch |err| switch (err) { error.OutOfPoolMemory, error.FragmentedPool => continue, else => return err, }; return .{ set, pool }; } const per_pool = 64; const pool = try d.createDescriptorPool(&.{ .flags = .{ .free_descriptor_set = true }, .max_sets = per_pool, .pool_size_count = 1, .p_pool_sizes = &.{.{ .type = .combined_image_sampler, .descriptor_count = per_pool }}, }, null); errdefer d.destroyDescriptorPool(pool, null); try r.image_pools.append(r.device.gpa, pool); var set: vk.DescriptorSet = undefined; try d.allocateDescriptorSets(&.{ .descriptor_pool = pool, .descriptor_set_count = 1, .p_set_layouts = &.{r.image_layout}, }, @ptrCast(&set)); return .{ set, pool };}
/// A `width` by `height` texture, with its set pointing at it.fn newTexture(r: *Renderer, width: u32, height: u32) !Texture { const dev = r.device; var img = try dev.image(width, height, .r8g8b8a8_unorm, .{ .sampled = true, .transfer_dst = true }); errdefer img.destroy(dev); const set, const pool = try r.imageSet(); const info = [_]vk.DescriptorImageInfo{.{ .sampler = r.image_sampler, .image_view = img.view, .image_layout = .shader_read_only_optimal }}; dev.device.updateDescriptorSets(&.{.{ .dst_set = set, .dst_binding = 0, .dst_array_element = 0, .descriptor_count = 1, .descriptor_type = .combined_image_sampler, .p_image_info = &info, .p_buffer_info = undefined, .p_texel_buffer_view = undefined, }}, null); return .{ .image = img, .set = set, .pool = pool };}
fn destroyTexture(r: *Renderer, t: *Texture) void { const dev = r.device; t.image.destroy(dev); dev.device.freeDescriptorSets(t.pool, &.{t.set}) catch {};}
/// Records copying `bytes` of staging, from `offset`, into all of `img`,/// leaving it ready to sample.fn recordImageCopy(r: *Renderer, cmd: vk.CommandBuffer, buffer: vk.Buffer, offset: usize, img: *const Device.Image) void { const d = r.device.device; const barrier_in = vk.ImageMemoryBarrier{ .src_access_mask = .{}, .dst_access_mask = .{ .transfer_write = true }, .old_layout = .undefined, .new_layout = .transfer_dst_optimal, .src_queue_family_index = vk.QUEUE_FAMILY_IGNORED, .dst_queue_family_index = vk.QUEUE_FAMILY_IGNORED, .image = img.image, .subresource_range = Device.color_range, }; d.cmdPipelineBarrier(cmd, .{ .top_of_pipe = true }, .{ .transfer = true }, .{}, null, null, &.{barrier_in}); d.cmdCopyBufferToImage(cmd, buffer, img.image, .transfer_dst_optimal, &.{.{ .buffer_offset = offset, .buffer_row_length = 0, .buffer_image_height = 0, .image_subresource = .{ .aspect_mask = .{ .color = true }, .mip_level = 0, .base_array_layer = 0, .layer_count = 1 }, .image_offset = .{ .x = 0, .y = 0, .z = 0 }, .image_extent = .{ .width = img.width, .height = img.height, .depth = 1 }, }}); var barrier_out = barrier_in; barrier_out.src_access_mask = .{ .transfer_write = true }; barrier_out.dst_access_mask = .{ .shader_read = true }; barrier_out.old_layout = .transfer_dst_optimal; barrier_out.new_layout = .shader_read_only_optimal; d.cmdPipelineBarrier(cmd, .{ .transfer = true }, .{ .fragment_shader = true }, .{}, null, null, &.{barrier_out});}
/// Records the uploads of the images in `uploads`, each into its texture,/// made or remade at its size as needed. One too big for the device is/// left out, and not drawn.fn recordUploads(r: *Renderer, cmd: vk.CommandBuffer, uploads: []const images.Upload) !void { const dev = r.device; const gpa = dev.gpa; const limit = dev.properties.limits.max_image_dimension_2d;
var total: usize = 0; if (r.blank == null) total += 16; for (uploads) |u| total += std.mem.alignForward(usize, @as(usize, u.width) * u.height * 4, 16); if (total == 0) { // Nothing to copy: a big buffer left from a big image goes. if (r.image_staging.size > 16 * 1024 * 1024) r.image_staging.destroy(dev); return; } try r.ensureBuffer(&r.image_staging, total, .{ .transfer_src = true }); const staging = r.image_staging.mapped.?; var offset: usize = 0;
if (r.blank == null) { var b = try r.newTexture(1, 1); errdefer r.destroyTexture(&b); @memset(staging[0..4], 0); r.recordImageCopy(cmd, r.image_staging.buffer, 0, &b.image); r.blank = b; offset += 16; }
for (uploads) |*u| { if (u.width == 0 or u.height == 0) continue; if (u.width > limit or u.height > limit) { log.warn("an image of {d}x{d} is more than the {d} a side the GPU can hold", .{ u.width, u.height, limit }); continue; } const gop = try r.textures.getOrPut(gpa, u.key); const fits = gop.found_existing and gop.value_ptr.image.width == u.width and gop.value_ptr.image.height == u.height; if (!fits) { // Sent again at another size: a texture of the new one. if (gop.found_existing) r.destroyTexture(gop.value_ptr); gop.value_ptr.* = r.newTexture(u.width, u.height) catch |err| { r.textures.removeByPtr(gop.key_ptr); return err; }; } const len = @as(usize, u.width) * u.height * 4; u.toRgba(staging[offset..][0..len]); r.recordImageCopy(cmd, r.image_staging.buffer, offset, &gop.value_ptr.image); gop.value_ptr.generation = u.generation; gop.value_ptr.used = r.frame; offset += std.mem.alignForward(usize, len, 16); }}
/// Lets go of the textures nothing has drawn for a while, or, while they/// take too much, nothing drew last frame. With the GPU idle.fn evictTextures(r: *Renderer) void { var bytes: usize = 0; var it = r.textures.valueIterator(); while (it.next()) |t| bytes += @as(usize, t.image.width) * t.image.height * 4; var again = true; while (again) { again = false; var kit = r.textures.iterator(); while (kit.next()) |kv| { const t = kv.value_ptr; const stale = t.used + texture_age < r.frame; const spare = bytes > texture_budget and t.used < r.frame; if (!stale and !spare) continue; bytes -= @as(usize, t.image.width) * t.image.height * 4; r.destroyTexture(t); r.textures.removeByPtr(kv.key_ptr); // The iterator is no good once something is removed. again = true; break; } }}
/// Draws `instances` over `background` into a `width` by `height` image/// and hands it to `target`.////// Waits first for the last frame's commands, since there is one frame in/// flight at a time. `runs` says which image each stretch of `instances`/// draws from; instances before the first run, or all of them with none,/// draw from the atlases. `uploads` are the pixels of images the renderer/// does not hold (see `holds`), and `alpha` and `color` are uploaded when/// they changed since the last frame. The frame is drawn straight into the/// target's own image when its `acquire` offers one, and otherwise into/// the renderer's, which the target's `record` copies out. `background` is/// RGBA from 0 to 1, written as it is: a translucent one is premultiplied,/// as the compositor reads it.pub fn draw( r: *Renderer, width: u32, height: u32, background: [4]f32, instances: []const Instance, runs: []const Run, uploads: []const images.Upload, alpha: *const Atlas, color: *const Atlas, target: Target,) !void { const dev = r.device; const d = dev.device; _ = try d.waitForFences(&.{r.fence}, .true, std.math.maxInt(u64));
// The last frame is done with its textures: those this one draws are // kept, and some others may go. r.frame += 1; for (runs) |run| if (run.image) |key| if (r.textures.getPtr(key)) |t| { t.used = r.frame; }; for (uploads) |u| if (r.textures.getPtr(u.key)) |t| { t.used = r.frame; }; r.evictTextures();
const acquired = try target.acquire(target.context, width, height, r.render_pass_direct); // The fence is reset only by a submit that will signal it, since the // next frame and `deinit` wait on it with no timeout. A frame that fails // once its target has been acquired still submits, with nothing in it, // to signal the fence and to consume the semaphore it was told to wait // on, which cannot be waited on again until something has. var submitted = false; errdefer if (!submitted) r.abandon(acquired.sync); if (acquired.direct == null) try r.ensureTarget(width, height); const instance_bytes = std.mem.sliceAsBytes(instances); try r.ensureBuffer(&r.instances, @max(instance_bytes.len, @sizeOf(Instance)), .{ .vertex_buffer = true }); @memcpy(r.instances.mapped.?[0..instance_bytes.len], instance_bytes);
const cmd = r.command_buffer; try d.resetCommandBuffer(cmd, .{}); try d.beginCommandBuffer(cmd, &.{ .flags = .{ .one_time_submit = true } });
// Atlases first: whatever changed since the last frame. const atlas_bytes = std.mem.alignForward(usize, alpha.data.len, 16) + color.data.len; try r.ensureBuffer(&r.staging, atlas_bytes, .{ .transfer_src = true }); var offset: usize = 0; const fresh_alpha = try r.recordAtlas(cmd, 0, alpha, &offset); const fresh_color = try r.recordAtlas(cmd, 1, color, &offset); if (fresh_alpha or fresh_color) { const infos = [_]vk.DescriptorImageInfo{ .{ .sampler = r.sampler, .image_view = r.atlases[0].image.view, .image_layout = .shader_read_only_optimal }, .{ .sampler = r.sampler, .image_view = r.atlases[1].image.view, .image_layout = .shader_read_only_optimal }, }; const writes = [_]vk.WriteDescriptorSet{ .{ .dst_set = r.descriptor_set, .dst_binding = 0, .dst_array_element = 0, .descriptor_count = 1, .descriptor_type = .combined_image_sampler, .p_image_info = infos[0..1], .p_buffer_info = undefined, .p_texel_buffer_view = undefined }, .{ .dst_set = r.descriptor_set, .dst_binding = 1, .dst_array_element = 0, .descriptor_count = 1, .descriptor_type = .combined_image_sampler, .p_image_info = infos[1..2], .p_buffer_info = undefined, .p_texel_buffer_view = undefined }, }; d.updateDescriptorSets(&writes, null); } try r.recordUploads(cmd, uploads);
const ours = dev.queue_family; if (acquired.direct) |direct| { // Taken from the compositor; its old contents are not wanted. const take = vk.ImageMemoryBarrier{ .src_access_mask = .{}, .dst_access_mask = .{ .color_attachment_write = true }, .old_layout = .undefined, .new_layout = .color_attachment_optimal, .src_queue_family_index = vk.QUEUE_FAMILY_FOREIGN_EXT, .dst_queue_family_index = ours, .image = direct.image, .subresource_range = Device.color_range, }; d.cmdPipelineBarrier(cmd, .{ .top_of_pipe = true }, .{ .color_attachment_output = true }, .{}, null, null, &.{take}); } d.cmdBeginRenderPass(cmd, &.{ .render_pass = if (acquired.direct != null) r.render_pass_direct else r.render_pass, .framebuffer = if (acquired.direct) |direct| direct.framebuffer else r.framebuffer, .render_area = .{ .offset = .{ .x = 0, .y = 0 }, .extent = .{ .width = width, .height = height } }, .clear_value_count = 1, .p_clear_values = &.{.{ .color = .{ .float_32 = background } }}, }, .@"inline"); d.cmdBindPipeline(cmd, .graphics, r.pipeline); d.cmdSetViewport(cmd, 0, &.{.{ .x = 0, .y = 0, .width = @floatFromInt(width), .height = @floatFromInt(height), .min_depth = 0, .max_depth = 1 }}); d.cmdSetScissor(cmd, 0, &.{.{ .offset = .{ .x = 0, .y = 0 }, .extent = .{ .width = width, .height = height } }}); d.cmdBindDescriptorSets(cmd, .graphics, r.pipeline_layout, 0, &.{ r.descriptor_set, r.blank.?.set }, null); const viewport = [2]f32{ @floatFromInt(width), @floatFromInt(height) }; d.cmdPushConstants(cmd, r.pipeline_layout, .{ .vertex = true }, 0, @sizeOf(@TypeOf(viewport)), &viewport); d.cmdBindVertexBuffers(cmd, 0, &.{r.instances.buffer}, &.{0}); // Instances before the first run, if any, draw from the atlases. const lead: u32 = if (runs.len > 0) @min(runs[0].first, @as(u32, @intCast(instances.len))) else @intCast(instances.len); if (lead > 0) d.cmdDraw(cmd, 6, lead, 0, 0); var bound: ?vk.DescriptorSet = r.blank.?.set; for (runs, 0..) |run, i| { const end: u32 = if (i + 1 < runs.len) runs[i + 1].first else @intCast(instances.len); if (end <= run.first) continue; const set = if (run.image) |key| (r.textures.get(key) orelse continue).set else r.blank.?.set; if (bound != set) { d.cmdBindDescriptorSets(cmd, .graphics, r.pipeline_layout, 1, &.{set}, null); bound = set; } d.cmdDraw(cmd, 6, end - run.first, 0, run.first); } d.cmdEndRenderPass(cmd);
if (acquired.direct) |direct| { // Given back. const give = vk.ImageMemoryBarrier{ .src_access_mask = .{ .color_attachment_write = true }, .dst_access_mask = .{}, .old_layout = .general, .new_layout = .general, .src_queue_family_index = ours, .dst_queue_family_index = vk.QUEUE_FAMILY_FOREIGN_EXT, .image = direct.image, .subresource_range = Device.color_range, }; d.cmdPipelineBarrier(cmd, .{ .color_attachment_output = true }, .{ .bottom_of_pipe = true }, .{}, null, null, &.{give}); } else try target.record(target.context, cmd, r.target.image, width, height); try d.endCommandBuffer(cmd);
const sync = acquired.sync; const wait_stage: vk.PipelineStageFlags = .{ .transfer = true, .color_attachment_output = true }; const signal_values = [1]u64{sync.signal_value orelse 0}; const timeline: vk.TimelineSemaphoreSubmitInfo = .{ .signal_semaphore_value_count = 1, .p_signal_semaphore_values = &signal_values, }; try d.resetFences(&.{r.fence}); try d.queueSubmit(dev.queue, &.{.{ .p_next = if (sync.signal_value != null) &timeline else null, .wait_semaphore_count = @intFromBool(sync.wait != null), .p_wait_semaphores = if (sync.wait) |*w| @ptrCast(w) else null, .p_wait_dst_stage_mask = @ptrCast(&wait_stage), .command_buffer_count = 1, .p_command_buffers = &.{cmd}, .signal_semaphore_count = @intFromBool(sync.signal != null), .p_signal_semaphores = if (sync.signal) |*sg| @ptrCast(sg) else null, }}, r.fence); submitted = true; try target.finish(target.context, r.fence);}
/// What a frame that failed after its target was acquired submits instead:/// a wait on the target's semaphore, if it gave one, and the fence. Nothing/// is signaled, so a timeline the target named stays where it was.fn abandon(r: *Renderer, sync: Sync) void { const d = r.device.device; const wait_stage: vk.PipelineStageFlags = .{ .all_commands = true }; d.resetFences(&.{r.fence}) catch return; d.queueSubmit(r.device.queue, &.{.{ .wait_semaphore_count = @intFromBool(sync.wait != null), .p_wait_semaphores = if (sync.wait) |*w| @ptrCast(w) else null, .p_wait_dst_stage_mask = @ptrCast(&wait_stage), }}, r.fence) catch {};}
test draw { try testing_.live(); const gpa = std.testing.allocator; const device = try Device.init(gpa, .{ .application_name = "aerie doctest" }); defer device.deinit(); var renderer = try Renderer.init(device); defer renderer.deinit(); var alpha = try Atlas.init(gpa, 64, .alpha8); defer alpha.deinit(gpa); var color = try Atlas.init(gpa, 64, .rgba); defer color.deinit(gpa);
// A target that keeps the frame where the CPU can read it, rather // than putting it on screen. const Readback = struct { device: *Device, buffer: Device.Buffer,
fn acquire(_: *anyopaque, _: u32, _: u32, _: vk.RenderPass) anyerror!Acquired { return .{}; }
fn record(context: *anyopaque, cmd: vk.CommandBuffer, image: vk.Image, width: u32, height: u32) anyerror!void { const self: *@This() = @ptrCast(@alignCast(context)); const d = self.device.device; d.cmdCopyImageToBuffer(cmd, image, .transfer_src_optimal, self.buffer.buffer, &.{.{ .buffer_offset = 0, .buffer_row_length = 0, .buffer_image_height = 0, .image_subresource = .{ .aspect_mask = .{ .color = true }, .mip_level = 0, .base_array_layer = 0, .layer_count = 1 }, .image_offset = .{ .x = 0, .y = 0, .z = 0 }, .image_extent = .{ .width = width, .height = height, .depth = 1 }, }}); d.cmdPipelineBarrier(cmd, .{ .transfer = true }, .{ .host = true }, .{}, null, &.{.{ .src_access_mask = .{ .transfer_write = true }, .dst_access_mask = .{ .host_read = true }, .src_queue_family_index = vk.QUEUE_FAMILY_IGNORED, .dst_queue_family_index = vk.QUEUE_FAMILY_IGNORED, .buffer = self.buffer.buffer, .offset = 0, .size = vk.WHOLE_SIZE, }}, null); }
fn finish(context: *anyopaque, fence: vk.Fence) anyerror!void { const self: *@This() = @ptrCast(@alignCast(context)); _ = try self.device.device.waitForFences(&.{fence}, .true, std.math.maxInt(u64)); } }; var readback: Readback = .{ .device = device, .buffer = try device.hostBuffer(8 * 8 * 4, .{ .transfer_dst = true }) }; defer readback.buffer.destroy(device);
// The left half red, over white. const instances = [_]Instance{.{ .x = 0, .y = 0, .width = 4, .height = 8, .color = .{ 255, 0, 0, 255 }, .kind = .solid }}; try renderer.draw(8, 8, .{ 1, 1, 1, 1 }, &instances, &.{}, &.{}, &alpha, &color, .{ .context = &readback, .acquire = Readback.acquire, .record = Readback.record, .finish = Readback.finish, });
// B, G, R, A in memory. const pixels = readback.buffer.mapped.?; try std.testing.expectEqualSlices(u8, &.{ 0, 0, 255, 255 }, pixels[0..4]); try std.testing.expectEqualSlices(u8, &.{ 255, 255, 255, 255 }, pixels[7 * 4 ..][0..4]);}
test "a frame that fails after acquiring its target leaves the fence to be signaled" { try testing_.live(); const gpa = std.testing.allocator; const device = try Device.init(gpa, .{ .application_name = "aerie test" }); defer device.deinit(); var renderer = try Renderer.init(device); defer renderer.deinit(); var alpha = try Atlas.init(gpa, 64, .alpha8); defer alpha.deinit(gpa); var color = try Atlas.init(gpa, 64, .rgba); defer color.deinit(gpa);
const Failing = struct { fn acquire(_: *anyopaque, _: u32, _: u32, _: vk.RenderPass) anyerror!Acquired { return .{}; } fn record(_: *anyopaque, _: vk.CommandBuffer, _: vk.Image, _: u32, _: u32) anyerror!void { return error.Refused; } fn finish(_: *anyopaque, _: vk.Fence) anyerror!void { unreachable; } }; var nothing: u8 = 0; const target: Target = .{ .context = ¬hing, .acquire = Failing.acquire, .record = Failing.record, .finish = Failing.finish }; try std.testing.expectError(error.Refused, renderer.draw(8, 8, .{ 1, 1, 1, 1 }, &.{}, &.{}, &.{}, &alpha, &color, target));
// The next frame, and `deinit`, wait on it with no timeout. const waited = try device.device.waitForFences(&.{renderer.fence}, .true, std.time.ns_per_s); try std.testing.expectEqual(vk.Result.success, waited);}