diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 15:57:22 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | c6a5aaf5c4a020eaefe24943ecb6cf9d4eb26836 (patch) | |
| tree | dab75cd767a3079e71277cef786f95763bcad638 /src/gui/gui.zig | |
| parent | 7f179dc4d02cb726194dd73ea316230683f26494 (diff) | |
| download | pardes-c6a5aaf5c4a020eaefe24943ecb6cf9d4eb26836.tar.gz pardes-c6a5aaf5c4a020eaefe24943ecb6cf9d4eb26836.zip | |
Post chain: Shadertoy passes compiled by glslc; Crt, Ripple and Glitch rewritten as Shadertoy files
Stage 9 of docs/render-pipeline.md. src/gui/Post.zig runs the chain over
the finished frame: each pass reads the one before, ping-pong textures
between, the last writes the window; an empty chain is the direct path.
The prefix (shaders/post/prefix.glsl) is ghostty's uniform block, name
for name and offset for offset (tested against a copy of its Uniforms, the
selection colours by their GLSL names), at SDL GPU's bindings, y down.
A user's file (Shader <path>) is compiled by glslc with compileGlsl's own
flags on a thread of its own, taken in at the next loop step; errors name
the file's own lines; a failed compile keeps the last good pipeline;
glslc missing is said once. The bundled passes compile with the build,
through the same prefix.
Crt, Ripple and Glitch are Shadertoy files, rewritten: no barrel (input is
identity, tested), everything in linear light and dithered, keyed to iTime.
scene_effects, crt.zig and crt.frag.glsl are gone; the core keeps no clock
for the chain. ShaderAnimation off|on|always drives redraw level A: the
chain alone over the retained frame (34 us CPU a redraw, measured).
Shared files touched: pardes.zig (nextWake loses the scene line,
disableSceneEffects empties the chain, two tests), builtins.zig (one switch
arm), ninep/ctl.zig (two switch arms), macos.zig (flags from the chain),
gui.zig. Not touched: Messages.zig, mouse.zig, detached/*, host_io, tty.
Diffstat (limited to 'src/gui/gui.zig')
| -rw-r--r-- | src/gui/gui.zig | 334 |
1 files changed, 169 insertions, 165 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig index 2cde5edf..ab1875b7 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -12,7 +12,7 @@ const look = @import("../look.zig"); const message = pardes.Messages.Message; const file_watch = @import("../file_watch.zig"); const deck = @import("deck.zig"); -const crt = @import("crt.zig"); +const Post = @import("Post.zig"); const pet = @import("pet.zig"); const fonts = @import("../fonts.zig"); // the Font builtin's half of the seam const selection_pipe = @import("../selection_pipe.zig"); @@ -43,8 +43,6 @@ const overlay_vert_spv = @embedFile("overlay.vert.spv"); const overlay_frag_spv = @embedFile("overlay.frag.spv"); const image_vert_spv = @embedFile("image.vert.spv"); const image_frag_spv = @embedFile("image.frag.spv"); -const crt_vert_spv = @embedFile("crt.vert.spv"); -const crt_frag_spv = @embedFile("crt.frag.spv"); const bg_default = [3]u8{ 18, 18, 18 }; const fg_default = [3]u8{ 204, 204, 204 }; @@ -202,18 +200,17 @@ const touch_click_flash_max_frames: u8 = 14; const touch_click_flash_vertices: usize = 1400; const touch_scroll_tick: f32 = 0.02; -test "a persistent scene presents a new panel's frame zero before advancing it" { +test "a new panel's frame zero is presented before it advances" { const core = try pardes.Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer core.deinit(); var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); defer arena.deinit(); const frame = pardes.animation.frame_ns; - core.settings.scene_effects.crt = true; core.advance(100); - const scene_frame = try core.render(arena.allocator()); - core.acknowledgePanelPresentation(scene_frame.panelTracks()); - try std.testing.expectEqual(@as(?u64, 100 + frame), core.nextWake()); + const idle = try core.render(arena.allocator()); + core.acknowledgePanelPresentation(idle.panelTracks()); + try std.testing.expectEqual(@as(?u64, null), core.nextWake()); core.settings.panel_transition = .slide; core.update(.{ .command = "Newcol" }); @@ -235,10 +232,9 @@ test "a persistent scene presents a new panel's frame zero before advancing it" for (second.panelTracks()) |track| try std.testing.expectEqual(@as(u16, 1), track.frame); } -// Per pane: background, rule, two grip strokes, two square rails, and the -// rule under each of its notice chips. -const pane_chrome_vertices: usize = 6 * (6 + pardes.Pane.Notices.max); -const max_overlay_vertices: usize = 24 + pet.max_vertices + pardes.MAX_COLS * 6 + pardes.MAX_PANES * pane_chrome_vertices + touch_click_flash_vertices + max_touch_points * (1100 + max_touch_trail_points * overlay_circle_vertices); +// The triangle overlay holds only the pet and the touch view: the chrome is +// decor. +const max_overlay_vertices: usize = pet.max_vertices + touch_click_flash_vertices + max_touch_points * (1100 + max_touch_trail_points * overlay_circle_vertices); const font_px_step: f32 = 2.0; const font_px_min: f32 = 8.0; @@ -1765,7 +1761,6 @@ const Gui = struct { decor_pipeline: *c.SDL_GPUGraphicsPipeline, overlay_pipeline: *c.SDL_GPUGraphicsPipeline, image_pipeline: *c.SDL_GPUGraphicsPipeline, - crt_pipeline: *c.SDL_GPUGraphicsPipeline, atlas_tex: *c.SDL_GPUTexture, atlas_sampler: *c.SDL_GPUSampler, linear_sampler: *c.SDL_GPUSampler, @@ -1838,7 +1833,8 @@ const Gui = struct { scene_tex_h: u32 = 0, scene_failures: u8 = 0, scene_target_failed: bool = false, - presented_scene: crt.Frame = .{}, + /// The post chain, run over the frame in scene_tex. + post: Post = .{}, pointer_present: bool = false, arrow_cursor: ?*c.SDL_Cursor = null, link_cursor: ?*c.SDL_Cursor = null, @@ -1977,7 +1973,9 @@ fn windowCells(g: *const Gui) GridCells { }; } -fn windowPointToPixels(geometry: WindowGeometry, x: f32, y: f32) crt.Point { +const Point = struct { x: f32, y: f32 }; + +fn windowPointToPixels(geometry: WindowGeometry, x: f32, y: f32) Point { return .{ .x = x * geometry.pixel_w / geometry.window_w, .y = y * geometry.pixel_h / geometry.window_h, @@ -1991,7 +1989,7 @@ test "high-density window coordinates map once to render pixels" { .pixel_w = 1120, .pixel_h = 720, }; - try std.testing.expectEqual(crt.Point{ .x = 700, .y = 240 }, windowPointToPixels(geometry, 350, 120)); + try std.testing.expectEqual(Point{ .x = 700, .y = 240 }, windowPointToPixels(geometry, 350, 120)); } fn fixedCellLayout(g: *const Gui) CellLayout { @@ -2397,7 +2395,6 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u const decor_pipeline = try makePipeline(device, swapchain_format, decor_frag_spv, 0, backgroundLayerBlend()); const overlay_pipeline = try makeOverlayPipeline(device, swapchain_format); const image_pipeline = try makeImagePipeline(device, swapchain_format); - const crt_pipeline = try makeCrtPipeline(device, swapchain_format); const overlay_buffer_size: u32 = @intCast(max_overlay_vertices * @sizeOf(OverlayVertex)); var ovb_info = c.SDL_GPUBufferCreateInfo{ .usage = c.SDL_GPU_BUFFERUSAGE_VERTEX, .size = overlay_buffer_size, .props = 0 }; @@ -2427,7 +2424,6 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u .decor_pipeline = decor_pipeline, .overlay_pipeline = overlay_pipeline, .image_pipeline = image_pipeline, - .crt_pipeline = crt_pipeline, .atlas_tex = atlas_tex, .atlas_sampler = atlas_sampler, .linear_sampler = linear_sampler, @@ -2464,7 +2460,6 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u c.SDL_ReleaseGPUGraphicsPipeline(device, g.pipeline); c.SDL_ReleaseGPUGraphicsPipeline(device, g.decor_pipeline); c.SDL_ReleaseGPUGraphicsPipeline(device, g.overlay_pipeline); - c.SDL_ReleaseGPUGraphicsPipeline(device, g.crt_pipeline); c.SDL_ReleaseGPUTexture(device, g.atlas_tex); c.SDL_ReleaseGPUSampler(device, g.atlas_sampler); c.SDL_ReleaseGPUSampler(device, g.linear_sampler); @@ -2490,6 +2485,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u g.prepared_images.deinit(gpa); g.cover.deinit(gpa); g.decor.deinit(gpa); + g.post.deinit(gpa, device); } defer c.SDL_ReleaseGPUTransferBuffer(device, g.image_vxfer); defer c.SDL_ReleaseGPUBuffer(device, g.image_vbuf); @@ -2796,7 +2792,6 @@ fn paintAttached(g: *Gui, gpa: std.mem.Allocator, client: *detached_client.Clien gpa, null, &surface, - .{}, false, ) catch |err| blk: { log.err("render: {t}", .{err}); @@ -3065,23 +3060,14 @@ const StdinFeed = struct { f.fill = len - seq_start; break; }; - if (parseFinger(body[0..end])) |finger_in| { - var finger = finger_in; + if (parseFinger(body[0..end])) |finger| { if (g) |gp| { var wpw: c_int = 0; var wph: c_int = 0; _ = c.SDL_GetWindowSizeInPixels(gp.window, &wpw, &wph); const win_w: f32 = @floatFromInt(@max(wpw, 1)); const win_h: f32 = @floatFromInt(@max(wph, 1)); - if (mapScenePoint(gp, finger.x * win_w, finger.y * win_h, win_w, win_h)) |m| { - finger.x = m.x / win_w; - finger.y = m.y / win_h; - handleFinger(&gp.touch, in, finger, win_w, win_h, @floatFromInt(gp.cell_w), @floatFromInt(gp.cell_h), @floatFromInt(gp.tagline_width)); - } else { - finger.kind = .cancel; - handleFinger(&gp.touch, in, finger, win_w, win_h, @floatFromInt(gp.cell_w), @floatFromInt(gp.cell_h), @floatFromInt(gp.tagline_width)); - in.post(.pointer_leave); - } + handleFinger(&gp.touch, in, finger, win_w, win_h, @floatFromInt(gp.cell_w), @floatFromInt(gp.cell_h), @floatFromInt(gp.tagline_width)); } else if (in.core) |core| { handleFinger(&grid_touch, in, finger, @floatFromInt(core.screen_w), @floatFromInt(core.screen_h), 1, 1, 1); } @@ -3288,6 +3274,20 @@ const Input = struct { }; fn dispatch(g: *Gui, in: *Input, sev: *const c.SDL_Event) void { + // What the post chain sees of the pointer and the focus. + switch (sev.type) { + c.SDL_EVENT_MOUSE_MOTION => { + const at = windowPointToPixels(windowGeometry(g.window), sev.motion.x, sev.motion.y); + g.post.mouse(at.x, at.y, null); + }, + c.SDL_EVENT_MOUSE_BUTTON_DOWN, c.SDL_EVENT_MOUSE_BUTTON_UP => { + const at = windowPointToPixels(windowGeometry(g.window), sev.button.x, sev.button.y); + g.post.mouse(at.x, at.y, sev.button.down); + }, + c.SDL_EVENT_WINDOW_FOCUS_GAINED => g.post.focus(true), + c.SDL_EVENT_WINDOW_FOCUS_LOST => g.post.focus(false), + else => {}, + } switch (sev.type) { c.SDL_EVENT_QUIT, c.SDL_EVENT_WINDOW_CLOSE_REQUESTED => in.close(), c.SDL_EVENT_WINDOW_MOUSE_LEAVE => { @@ -3460,7 +3460,7 @@ fn dispatch(g: *Gui, in: *Input, sev: *const c.SDL_Event) void { }, c.SDL_EVENT_FINGER_DOWN, c.SDL_EVENT_FINGER_MOTION, c.SDL_EVENT_FINGER_UP, c.SDL_EVENT_FINGER_CANCELED => { const f = sev.tfinger; - var finger: Finger = .{ + const finger: Finger = .{ .kind = switch (sev.type) { c.SDL_EVENT_FINGER_DOWN => .down, c.SDL_EVENT_FINGER_MOTION => .motion, @@ -3479,15 +3479,7 @@ fn dispatch(g: *Gui, in: *Input, sev: *const c.SDL_Event) void { const win_h: f32 = @floatFromInt(@max(ph, 1)); const fcw: f32 = @floatFromInt(g.cell_w); const fch: f32 = @floatFromInt(g.cell_h); - if (mapScenePoint(g, finger.x * win_w, finger.y * win_h, win_w, win_h)) |m| { - finger.x = m.x / win_w; - finger.y = m.y / win_h; - handleFinger(&g.touch, in, finger, win_w, win_h, fcw, fch, @floatFromInt(g.tagline_width)); - } else { - finger.kind = .cancel; - handleFinger(&g.touch, in, finger, win_w, win_h, fcw, fch, @floatFromInt(g.tagline_width)); - in.post(.pointer_leave); - } + handleFinger(&g.touch, in, finger, win_w, win_h, fcw, fch, @floatFromInt(g.tagline_width)); }, c.SDL_EVENT_PINCH_BEGIN, c.SDL_EVENT_PINCH_UPDATE => in.post(.{ .pinch = sev.pinch.scale }), c.SDL_EVENT_GAMEPAD_ADDED => { @@ -3747,14 +3739,30 @@ test "tagline pointer mapping retains physical grips and carries logical text co try std.testing.expectEqual(pardes.TagKind.workspace, topbar.tag_hit.?.kind); } -fn mapScenePoint(g: *const Gui, x: f32, y: f32, w: f32, h: f32) ?crt.Point { - return crt.mapScene(x, y, w, h, g.presented_scene.effects, g.presented_scene.time_seconds); +test "a click at the window's corner lands in the corner cell with a post chain on" { + const core = try pardes.Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 24 }); + defer core.deinit(); + try std.testing.expect(core.settings.post.set(.crt, "", true)); + var g: Gui = undefined; + g.cell_w = 10; + g.cell_h = 20; + g.tagline_width = 6; + g.tagline_height = 12; + const geometry: WindowGeometry = .{ .window_w = 400, .window_h = 240, .pixel_w = 800, .pixel_h = 480 }; + const last = mouseCellWithGeometry(&g, core, 399.9, 239.9, geometry).?; + try std.testing.expectEqual(@as(u16, 79), last.col); + try std.testing.expectEqual(@as(u16, 23), last.row); + const first = mouseCellWithGeometry(&g, core, 0, 0, geometry).?; + try std.testing.expectEqual(@as(u16, 0), first.col); + try std.testing.expectEqual(@as(u16, 0), first.row); } +/// Where a window point lands: the cell drawn under it. Input is identity: +/// no post pass moves a pixel (the bundled Crt has no barrel), so a click +/// is exactly where it looks. fn mouseCellWithGeometry(g: *const Gui, core: ?*const pardes.Pardes, x: f32, y: f32, geometry: WindowGeometry) ?MouseCell { const physical = windowPointToPixels(geometry, x, y); - const mapped = mapScenePoint(g, physical.x, physical.y, geometry.pixel_w, geometry.pixel_h) orelse return null; - return gridCellAtPixels(g, core, mapped.x, mapped.y); + return gridCellAtPixels(g, core, physical.x, physical.y); } fn mouseCell(g: *const Gui, core: ?*const pardes.Pardes, x: f32, y: f32) ?MouseCell { @@ -4064,6 +4072,7 @@ fn waitInput(ctx: ?*anyopaque, timeout_ms: u32) void { // shell's queue raises for a terminal's output, a completion, a file // system request. What the shell moves or times itself polls every // frame: a smooth scroll, a gamepad's sticks, the test stdin feed, + // a post chain that moves on its own (ShaderAnimation), // a retired shell waiting to be killed, a present to retry. A // minimized window sleeps through the core's animation too: nothing // shows it, and it is caught up at once when the window is back. @@ -4072,7 +4081,8 @@ fn waitInput(ctx: ?*anyopaque, timeout_ms: u32) void { } else for (s.ptys) |slot| { if (slot) |pt| if (pt.kill_at != 0) break true; } else false; - const polls = g.scroll_lag != 0 or g.scroll_delta != 0 or g.gamepad != null or s.test_mode or core.needs_frame or retiring; + const polls = g.scroll_lag != 0 or g.scroll_delta != 0 or g.gamepad != null or s.test_mode or core.needs_frame or retiring or + g.post.animating(core.settings.shader_animation); const minimized = !s.test_mode and c.SDL_GetWindowFlags(g.window) & (c.SDL_WINDOW_MINIMIZED | c.SDL_WINDOW_OCCLUDED) != 0; // A latency trace is fed on stdin, whose poll below paces the loop: // waiting here too would make every tick cost two frames. @@ -4157,6 +4167,10 @@ fn pollFrame(ctx: ?*anyopaque) void { const geom = windowCells(g); if (updateCoreResize(core, geom.cols, geom.rows, g.cell_w, g.cell_h, g.tagline_width, g.tagline_height)) resetScroll(g); stepScroll(g, core, s.gpa); + g.post.sync(s.gpa, s.io, g.device, g.swapchain_format, core); + // Between the core's frames, a chain that moves on its own redraws alone. + if (g.post.animating(core.settings.shader_animation) and !core.needs_frame and s.presented and + c.SDL_GetTicksNS() -| g.post.last_ns >= pardes.animation.frame_ns) redrawPost(g, s.gpa); } fn finishWindowOpacity(core: *pardes.Pardes, applied: *u8, failure: ?[]const u8) void { @@ -4216,15 +4230,13 @@ fn present(ctx: ?*anyopaque, surface: *const pardes.Surface) void { frame_started_ns = monoNs(); const frame = @constCast(surface); s.surface = frame; - const scene_requested = core.settings.scene_effects.crt or - core.settings.scene_effects.ripple or core.settings.scene_effects.glitch; + const scene_requested = g.post.ready(); if (!scene_requested) g.scene_failures = 0; s.presented = renderFrame( g, s.gpa, core, frame, - core.settings.scene_effects, core.settings.debug, ) catch |err| blk: { log.err("render: {t}", .{err}); @@ -5190,7 +5202,6 @@ fn renderFrame( gpa: std.mem.Allocator, core: ?*const pardes.Pardes, surface: *pardes.Surface, - scene_effects: pardes.animation.SceneEffect, debug_on: bool, ) !bool { if (surface.pointer_shape != g.pointer_shape) { @@ -5205,48 +5216,19 @@ fn renderFrame( defer if (!command_consumed) { _ = c.SDL_SubmitGPUCommandBuffer(cmd); }; - var sw: u32 = 0; - var sh: u32 = 0; - var target: *c.SDL_GPUTexture = undefined; - if (g.capture) { - sw = @as(u32, surface.cols) * g.cell_w + g.capture_pad; - sh = @as(u32, surface.rows) * g.cell_h + g.capture_pad; - if (sw == 0 or sh == 0) { - command_consumed = true; - _ = c.SDL_SubmitGPUCommandBuffer(cmd); - return false; - } - try ensureCaptureTexture(g, sw, sh); - target = g.capture_tex.?; - } else if (g.soft_present) { - var pw: c_int = 0; - var ph: c_int = 0; - if (!c.SDL_GetWindowSizeInPixels(g.window, &pw, &ph) or pw <= 0 or ph <= 0) { - command_consumed = true; - _ = c.SDL_SubmitGPUCommandBuffer(cmd); - return false; - } - sw = @intCast(pw); - sh = @intCast(ph); - try ensureCaptureTexture(g, sw, sh); - target = g.capture_tex.?; - } else { - var swap_tex: ?*c.SDL_GPUTexture = null; - if (!c.SDL_AcquireGPUSwapchainTexture(cmd, g.window, &swap_tex, &sw, &sh)) { - command_consumed = true; - _ = c.SDL_SubmitGPUCommandBuffer(cmd); - return false; - } - target = swap_tex orelse { // swapchain busy: skip the frame - command_consumed = true; - _ = c.SDL_SubmitGPUCommandBuffer(cmd); - return false; - }; - } + const frame_target = try acquireTarget(g, cmd, @as(u32, surface.cols) * g.cell_w + g.capture_pad, @as(u32, surface.rows) * g.cell_h + g.capture_pad) orelse { + command_consumed = true; + _ = c.SDL_SubmitGPUCommandBuffer(cmd); + return false; + }; + const target = frame_target.texture; + const sw = frame_target.w; + const sh = frame_target.h; const win_w: f32 = @floatFromInt(sw); const win_h: f32 = @floatFromInt(sh); - var scene_on = scene_effects.crt or scene_effects.ripple or scene_effects.glitch; + // The post chain runs over a local frame, from the scene texture. + var scene_on = core != null and g.post.ready(); if (scene_on) ensureSceneTexture(g, sw, sh) catch { g.scene_target_failed = true; scene_on = false; @@ -5559,64 +5541,87 @@ fn renderFrame( drawOverlayGpu(g, cmd, rp, overlay_count); c.SDL_EndGPURenderPass(rp); - var rendered_scene: crt.Frame = .{}; if (scene_on) { - var crt_target = std.mem.zeroes(c.SDL_GPUColorTargetInfo); - crt_target.texture = target; - crt_target.load_op = c.SDL_GPU_LOADOP_DONT_CARE; - crt_target.store_op = c.SDL_GPU_STOREOP_STORE; - const crt_pass = c.SDL_BeginGPURenderPass(cmd, &crt_target, 1, null); - c.SDL_BindGPUGraphicsPipeline(crt_pass, g.crt_pipeline); - const samp_binding = c.SDL_GPUTextureSamplerBinding{ .texture = g.scene_tex.?, .sampler = g.linear_sampler }; - c.SDL_BindGPUFragmentSamplers(crt_pass, 0, &samp_binding, 1); - const now_ns = c.SDL_GetTicksNS(); - const time_wrap_ns: u64 = 4096 * std.time.ns_per_s; - const time_seconds: f32 = @floatCast( - @as(f64, @floatFromInt(now_ns % time_wrap_ns)) / @as(f64, std.time.ns_per_s), - ); - rendered_scene = .{ - .effects = .{ - .crt = scene_effects.crt, - .ripple = scene_effects.ripple, - .glitch = scene_effects.glitch, - }, - .time_seconds = time_seconds, - }; - const crt_uniforms = [8]f32{ - time_seconds, - @floatFromInt((now_ns / (std.time.ns_per_s / 60)) % 65_536), - @floatFromInt(sw), - @floatFromInt(sh), - @floatFromInt(@intFromBool(scene_effects.crt)), - @floatFromInt(@intFromBool(scene_effects.ripple)), - @floatFromInt(@intFromBool(scene_effects.glitch)), - 0, + if (core) |p| { + var boxes: [max_cursors]CursorBox = undefined; + const cursors = if (cursors_shown) cursorBoxes(g, surface, layout, win_h, &boxes) else 0; + g.post.describe(.{ + .cursor = if (cursors > 0) boxes[0] else null, + .background = chrome.page orelse bg_default, + .foreground = chrome.fg orelse fg_default, + .selection_foreground = p.theme().sel_fg, + .selection_background = p.theme().sel_bg, + }); + } + const chain_zone = tracy.zone(@src(), "post chain"); + defer chain_zone.end(); + g.post.draw(g.device, cmd, g.linear_sampler, g.swapchain_format, g.scene_tex.?, target, sw, sh, c.SDL_GetTicksNS(), background_opacity >= 1) catch { + g.scene_target_failed = true; }; - c.SDL_PushGPUFragmentUniformData(cmd, 0, &crt_uniforms, @sizeOf(@TypeOf(crt_uniforms))); - c.SDL_DrawGPUPrimitives(crt_pass, 3, 1, 0, 0); - c.SDL_EndGPURenderPass(crt_pass); } + command_consumed = true; + const shown = try finishFrame(g, gpa, cmd, frame_target, if (groups.tracks != 0) groups.items[1].track.?.frame else null); + if (shown and groups.tracks == 0) rememberPresentedImages(g, gpa, surface); + return shown; +} + +const Target = struct { texture: *c.SDL_GPUTexture, w: u32, h: u32 }; + +/// What this frame draws into: the capture texture in test mode (the grid's +/// size plus PARDES_TEST_PAD) or with a soft presenter, else the swapchain's +/// image; null when there is none to be had this time. +fn acquireTarget(g: *Gui, cmd: *c.SDL_GPUCommandBuffer, capture_w: u32, capture_h: u32) !?Target { if (g.capture) { - command_consumed = true; - try captureFrame(g, gpa, cmd, target, sw, sh, if (groups.tracks != 0) groups.items[1].track.?.frame else null); - g.presented_scene = rendered_scene; - if (groups.tracks == 0) rememberPresentedImages(g, gpa, surface); + if (capture_w == 0 or capture_h == 0) return null; + try ensureCaptureTexture(g, capture_w, capture_h); + return .{ .texture = g.capture_tex.?, .w = capture_w, .h = capture_h }; + } + if (g.soft_present) { + var pw: c_int = 0; + var ph: c_int = 0; + if (!c.SDL_GetWindowSizeInPixels(g.window, &pw, &ph) or pw <= 0 or ph <= 0) return null; + try ensureCaptureTexture(g, @intCast(pw), @intCast(ph)); + return .{ .texture = g.capture_tex.?, .w = @intCast(pw), .h = @intCast(ph) }; + } + var swap_tex: ?*c.SDL_GPUTexture = null; + var sw: u32 = 0; + var sh: u32 = 0; + if (!c.SDL_AcquireGPUSwapchainTexture(cmd, g.window, &swap_tex, &sw, &sh)) return null; + // A busy swapchain skips the frame. + return .{ .texture = swap_tex orelse return null, .w = sw, .h = sh }; +} + +/// Submits the frame: captured, handed to the soft presenter, or presented. +fn finishFrame(g: *Gui, gpa: std.mem.Allocator, cmd: *c.SDL_GPUCommandBuffer, target: Target, transition: ?u16) !bool { + if (g.capture) { + try captureFrame(g, gpa, cmd, target.texture, target.w, target.h, transition); return true; } if (g.soft_present) { - command_consumed = true; - try softPresentFrame(g, cmd, target, sw, sh); - g.presented_scene = rendered_scene; - if (groups.tracks == 0) rememberPresentedImages(g, gpa, surface); + try softPresentFrame(g, cmd, target.texture, target.w, target.h); return true; } - command_consumed = true; - const submitted = c.SDL_SubmitGPUCommandBuffer(cmd); - if (submitted) { - g.presented_scene = rendered_scene; - if (groups.tracks == 0) rememberPresentedImages(g, gpa, surface); + return c.SDL_SubmitGPUCommandBuffer(cmd); +} + +/// Redraw level A (docs ยง5.5): the post chain alone, over the frame the core +/// last drew, for a pass that moves on its own. No core render, no cell. +fn redrawPost(g: *Gui, gpa: std.mem.Allocator) void { + const scene = g.scene_tex orelse return; + const cmd = c.SDL_AcquireGPUCommandBuffer(g.device) orelse return; + const target = (acquireTarget(g, cmd, g.scene_tex_w, g.scene_tex_h) catch null) orelse { + _ = c.SDL_SubmitGPUCommandBuffer(cmd); + return; + }; + // A window the retained frame no longer fits waits for the core's. + if (target.w != g.scene_tex_w or target.h != g.scene_tex_h) { + _ = c.SDL_SubmitGPUCommandBuffer(cmd); + return; } - return submitted; + const zone = tracy.zone(@src(), "post chain redraw"); + g.post.draw(g.device, cmd, g.linear_sampler, g.swapchain_format, scene, target.texture, target.w, target.h, c.SDL_GetTicksNS(), backgroundOpacity(g.applied_window_opacity) >= 1) catch {}; + zone.end(); + _ = finishFrame(g, gpa, cmd, target, null) catch {}; } const ResolvedCell = struct { @@ -7016,25 +7021,7 @@ fn makeImagePipeline(device: *c.SDL_GPUDevice, color_format: c.SDL_GPUTextureFor return c.SDL_CreateGPUGraphicsPipeline(device, &info) orelse error.GpuCreate; } -fn makeCrtPipeline(device: *c.SDL_GPUDevice, color_format: c.SDL_GPUTextureFormat) !*c.SDL_GPUGraphicsPipeline { - const vs = try makeShader(device, crt_vert_spv, c.SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); - defer c.SDL_ReleaseGPUShader(device, vs); - const fs = try makeShader(device, crt_frag_spv, c.SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); - defer c.SDL_ReleaseGPUShader(device, fs); - - var col_desc = c.SDL_GPUColorTargetDescription{ .format = color_format, .blend_state = std.mem.zeroes(c.SDL_GPUColorTargetBlendState) }; - var info = std.mem.zeroes(c.SDL_GPUGraphicsPipelineCreateInfo); - info.vertex_shader = vs; - info.fragment_shader = fs; - info.primitive_type = c.SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; - info.rasterizer_state.fill_mode = c.SDL_GPU_FILLMODE_FILL; - info.rasterizer_state.cull_mode = c.SDL_GPU_CULLMODE_NONE; - info.multisample_state.sample_count = c.SDL_GPU_SAMPLECOUNT_1; - info.target_info = .{ .color_target_descriptions = &col_desc, .num_color_targets = 1 }; - return c.SDL_CreateGPUGraphicsPipeline(device, &info) orelse error.GpuCreate; -} - -fn makeShader(device: *c.SDL_GPUDevice, code: []const u8, stage: c.SDL_GPUShaderStage, n_samplers: u32, n_uniform_buffers: u32) !*c.SDL_GPUShader { +pub fn makeShader(device: *c.SDL_GPUDevice, code: []const u8, stage: c.SDL_GPUShaderStage, n_samplers: u32, n_uniform_buffers: u32) !*c.SDL_GPUShader { var info = std.mem.zeroes(c.SDL_GPUShaderCreateInfo); info.code = code.ptr; info.code_size = code.len; @@ -7519,25 +7506,32 @@ test "decor is read from the regions, each pane's in its own group" { try std.testing.expectEqual(@as(u32, 0), groups.items[groups.ink].decor_count); } -/// The bar cursors: the grid's where no layer has the cell, and each layer's -/// at its own pitch. -fn cursorDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, layout: CellLayout, win_w: f32, win_h: f32) !void { +const CursorBox = Post.Frame.Cursor; +const max_cursors = 1 + pardes.MAX_PANES + pardes.MAX_TAG_LAYERS; + +/// Every cursor on the frame where it is drawn, in window pixels: the grid's +/// where no layer has its cell, and each layer's at that layer's pitch. A bar +/// is an eighth of its cell wide, a block the whole cell. +fn cursorBoxes(g: *const Gui, surface: *const pardes.Surface, layout: CellLayout, win_h: f32, out: *[max_cursors]CursorBox) usize { const chrome = &surface.chrome; + const page = chrome.page orelse bg_default; + var n: usize = 0; if (surface.cursor) |cursor| { - if (cursor.bar and cursor.x < surface.cols and cursor.y < surface.rows and bodyLayerAt(surface, cursor.x, cursor.y) == null and tagLayerIn(surface.tagLayers(), cursor.x, cursor.y) == null) { + if (cursor.x < surface.cols and cursor.y < surface.rows and bodyLayerAt(surface, cursor.x, cursor.y) == null and tagLayerIn(surface.tagLayers(), cursor.x, cursor.y) == null) { const cell = &surface.cells[@as(usize, cursor.y) * surface.cols + cursor.x]; const role = cellFontRole(cell); const width: f32 = if (role == .tagline) @floatFromInt(g.tagline_width) else layout.w; const height = std.math.clamp(if (role == .tagline) @as(f32, @floatFromInt(g.tagline_height)) else layout.h, 1.0, layout.h); const x0 = @as(f32, @floatFromInt(cursor.x)) * layout.w; const y0 = @as(f32, @floatFromInt(cursor.y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(cursor.y, win_h, @intFromFloat(layout.h), @intFromFloat(height)))); - try addDecor(g, gpa, x0, y0, x0 + @max(1.0, width / 8.0), y0 + height, cursorColor(chrome, cell, role), 1, null, win_w, win_h); + out[n] = .{ .x0 = x0, .y0 = y0, .x1 = x0 + if (cursor.bar) @max(1.0, width / 8.0) else width, .y1 = y0 + height, .rgb = cursorColor(chrome, cell, role), .text = page, .bar = cursor.bar }; + n += 1; } } for (surface.bodyLayers()) |*layer| { if (layer.rows == 0) continue; const cursor = layer.cursor orelse continue; - if (!cursor.bar or cursor.y >= layer.rows or cursor.x >= layer.cols) continue; + if (cursor.y >= layer.rows or cursor.x >= layer.cols) continue; const left = @as(f32, @floatFromInt(layer.viewport.x)) * layout.w; const bottom = @as(f32, @floatFromInt(layer.viewport.y + layer.viewport.h)) * layout.h; const top = layer.rowTop(cursor.y, layout.h, @floatFromInt(g.tagline_height)); @@ -7547,12 +7541,13 @@ fn cursorDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, const width: f32 = if (role == .tagline) @floatFromInt(g.tagline_width) else layout.w; const x = left + @as(f32, @floatFromInt(cursor.x)) * width; const cell = &layer.cells[@as(usize, cursor.y) * layer.cols + cursor.x]; - try addDecor(g, gpa, x, top, x + @max(1, width / 8), @min(bottom, top + height), cursorColor(chrome, cell, role), 1, null, win_w, win_h); + out[n] = .{ .x0 = x, .y0 = top, .x1 = x + if (cursor.bar) @max(1, width / 8) else width, .y1 = @min(bottom, top + height), .rgb = cursorColor(chrome, cell, role), .text = page, .bar = cursor.bar }; + n += 1; } for (surface.tagLayers()) |*layer| { if (layer.rows == 0) continue; const cursor = layer.cursor orelse continue; - if (!cursor.bar or cursor.x >= layer.cols or cursor.y >= layer.rows) continue; + if (cursor.x >= layer.cols or cursor.y >= layer.rows) continue; const width: f32 = @floatFromInt(g.tagline_width); const left = @as(f32, @floatFromInt(layer.viewport.x)) * layout.w; const right = left + @as(f32, @floatFromInt(layer.viewport.w)) * layout.w; @@ -7560,8 +7555,17 @@ fn cursorDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, if (x >= right) continue; const y = layer.viewport.y + cursor.y; const top = @as(f32, @floatFromInt(y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(y, win_h, g.cell_h, g.tagline_height))); - try addDecor(g, gpa, x, top, @min(right, x + @max(1, width / 8)), top + @as(f32, @floatFromInt(g.tagline_height)), cursorColor(chrome, &layer.cells[@as(usize, cursor.y) * layer.cols + cursor.x], .tagline), 1, null, win_w, win_h); + out[n] = .{ .x0 = x, .y0 = top, .x1 = @min(right, x + if (cursor.bar) @max(1, width / 8) else width), .y1 = top + @as(f32, @floatFromInt(g.tagline_height)), .rgb = cursorColor(chrome, &layer.cells[@as(usize, cursor.y) * layer.cols + cursor.x], .tagline), .text = page, .bar = cursor.bar }; + n += 1; } + return n; +} + +/// The bar cursors as decor; a block cursor is its cell's own colours. +fn cursorDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, layout: CellLayout, win_w: f32, win_h: f32) !void { + var boxes: [max_cursors]CursorBox = undefined; + for (boxes[0..cursorBoxes(g, surface, layout, win_h, &boxes)]) |box| + if (box.bar) try addDecor(g, gpa, box.x0, box.y0, box.x1, box.y1, box.rgb, 1, null, win_w, win_h); } /// The triangle overlay: the workspace pet and the touch debug view, over |
