diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-08 12:25:04 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:07 -0300 |
| commit | 172eb56fee0b6bde698e72be2bca55a90fe81ce9 (patch) | |
| tree | f2fd94709facf134c3670db4c1a20a5eaca1d53d /src/gui.zig | |
| parent | b91e5ebf8b529d0ced0bf18e83b17df0bd2374ce (diff) | |
| download | pardes-172eb56fee0b6bde698e72be2bca55a90fe81ce9.tar.gz pardes-172eb56fee0b6bde698e72be2bca55a90fe81ce9.zip | |
steam deck nice version
Diffstat (limited to 'src/gui.zig')
| -rw-r--r-- | src/gui.zig | 298 |
1 files changed, 252 insertions, 46 deletions
diff --git a/src/gui.zig b/src/gui.zig index d9ddb280..aba4f7a2 100644 --- a/src/gui.zig +++ b/src/gui.zig @@ -18,6 +18,8 @@ const vaxis = @import("vaxis"); // test modes only: the stdin escape-seq parser const ghostty_vt = @import("ghostty-vt"); // 256-color palette for .index cells const pardes = @import("pardes.zig"); const look = @import("look.zig"); +const deck = @import("deck.zig"); +const crt = @import("crt.zig"); const is_emscripten = builtin.os.tag == .emscripten; @@ -48,6 +50,8 @@ const vert_spv = if (is_emscripten) "" else @embedFile("ui.vert.spv"); const frag_spv = if (is_emscripten) "" else @embedFile("ui.frag.spv"); const overlay_vert_spv = if (is_emscripten) "" else @embedFile("overlay.vert.spv"); const overlay_frag_spv = if (is_emscripten) "" else @embedFile("overlay.frag.spv"); +const crt_vert_spv = if (is_emscripten) "" else @embedFile("crt.vert.spv"); +const crt_frag_spv = if (is_emscripten) "" else @embedFile("crt.frag.spv"); // web: GL ES shader SOURCES, compiled by WebGL at init (no SPIR-V there) const vert_glsl_es = if (is_emscripten) @embedFile("ui.vert.es.glsl") else ""; const frag_glsl_es = if (is_emscripten) @embedFile("ui.frag.es.glsl") else ""; @@ -60,13 +64,37 @@ const embedded_dump = if (is_emscripten) @embedFile("embedded.dump.zon") else "" const bg_default = [3]u8{ 18, 18, 18 }; const fg_default = [3]u8{ 204, 204, 204 }; +// the plan9 arrow cursor, bytes verbatim from 9front /sys/src/9/port/ +// devmouse.c (Cursor arrow, 16x16 MSB-first): clr is the white outline, set +// the black ink, offset {-1,-1} puts the hot point at (1,1) in the bitmap. +// SDL_CreateCursor's scheme: data=1,mask=1 black; data=0,mask=1 white; +// mask=0 transparent — so data = set and mask = set|clr. +const p9_arrow_clr = [32]u8{ + 0xFF, 0xFF, 0x80, 0x01, 0x80, 0x02, 0x80, 0x0C, + 0x80, 0x10, 0x80, 0x10, 0x80, 0x08, 0x80, 0x04, + 0x80, 0x02, 0x80, 0x01, 0x80, 0x02, 0x8C, 0x04, + 0x92, 0x08, 0x91, 0x10, 0xA0, 0xA0, 0xC0, 0x40, +}; +const p9_arrow_set = [32]u8{ + 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFC, 0x7F, 0xF0, + 0x7F, 0xE0, 0x7F, 0xE0, 0x7F, 0xF0, 0x7F, 0xF8, + 0x7F, 0xFC, 0x7F, 0xFE, 0x7F, 0xFC, 0x73, 0xF8, + 0x61, 0xF0, 0x60, 0xE0, 0x40, 0x40, 0x00, 0x00, +}; +const p9_arrow_mask = blk: { + var m: [32]u8 = undefined; + for (&m, p9_arrow_set, p9_arrow_clr) |*b, s, cl| b.* = s | cl; + break :blk m; +}; + // web: glyphs raster at 1.5x and the canvas backs at CSS×1.5 so text stays // sharp on scaled canvases (the prototype's -Dweb-render-scale, which never // shipped non-default — hardcoded here instead of a build option). const web_render_scale: f32 = 1.5; -const atlas_w: u32 = if (is_emscripten) 2048 else 1024; -const atlas_h: u32 = if (is_emscripten) 2048 else 1024; +// 2048 fits ~2500 glyphs at the 2x native cell (~20x40); web needs it too +const atlas_w: u32 = 2048; +const atlas_h: u32 = 2048; const Slot = struct { u: u32, v: u32 }; const max_touch_points: usize = 16; @@ -370,6 +398,7 @@ const Gui = struct { swapchain_format: c.SDL_GPUTextureFormat, pipeline: *c.SDL_GPUGraphicsPipeline, overlay_pipeline: *c.SDL_GPUGraphicsPipeline, + crt_pipeline: *c.SDL_GPUGraphicsPipeline, atlas_tex: *c.SDL_GPUTexture, atlas_sampler: *c.SDL_GPUSampler, atlas_xfer: *c.SDL_GPUTransferBuffer, @@ -400,6 +429,12 @@ const Gui = struct { live_ctrl: bool = false, live_alt: bool = false, + // Crt builtin: the scene renders into this texture, then a fullscreen + // CRT pass warps it onto the real target + scene_tex: ?*c.SDL_GPUTexture = null, + scene_tex_w: u32 = 0, + scene_tex_h: u32 = 0, + // PARDES_TEST frame capture (render into an offscreen target, dump PPM) capture: bool = false, capture_dir: []const u8 = "", @@ -414,6 +449,7 @@ const Gui = struct { pad_x: f32 = 0, pad_y: f32 = 0, pad_scroll: f32 = 0, + deck: deck.Deck = .{}, // web: WebGL2 handles (plain GL ints) + the CPU-side instance staging gl_program: c_uint = 0, @@ -480,6 +516,12 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void { log.err("SDL_CreateWindow: {s}", .{c.SDL_GetError()}); return error.SdlInit; }; + if (c.SDL_CreateCursor(&p9_arrow_set, &p9_arrow_mask, 16, 16, 1, 1)) |cur| { + _ = c.SDL_SetCursor(cur); + } else log.err("SDL_CreateCursor: {s}", .{c.SDL_GetError()}); + // fullscreen deck use: keep the pointer inside the window. Absolute + // coords stay (no relative mode — clicks need positions). + if (!test_mode) _ = c.SDL_SetWindowMouseGrab(window, true); const device = c.SDL_CreateGPUDevice(c.SDL_GPU_SHADERFORMAT_SPIRV, true, null) orelse { log.err("SDL_CreateGPUDevice: {s}", .{c.SDL_GetError()}); return error.SdlInit; @@ -515,7 +557,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void { log.err("ui_font_new failed", .{}); return error.FontInit; }; - const px: f32 = 18.0; + const px: f32 = 27.0; const scale = c.ui_font_scale_for_height(font, px); var cw: c_int = 10; var chh: c_int = 20; @@ -556,6 +598,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void { const pipeline = try makePipeline(device, swapchain_format); const overlay_pipeline = try makeOverlayPipeline(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 }; @@ -575,6 +618,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void { .swapchain_format = swapchain_format, .pipeline = pipeline, .overlay_pipeline = overlay_pipeline, + .crt_pipeline = crt_pipeline, .atlas_tex = atlas_tex, .atlas_sampler = atlas_sampler, .atlas_xfer = atlas_xfer, @@ -713,7 +757,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void { // 7. render _ = frame_arena.reset(.retain_capacity); const surface = try core.render(frame_arena.allocator()); - renderFrame(&g, gpa, surface) catch |err| log.err("render: {t}", .{err}); + renderFrame(&g, gpa, surface, core.crt_on) catch |err| log.err("render: {t}", .{err}); } } @@ -805,6 +849,7 @@ fn runWeb(opts_in: pardes.Options) !void { .swapchain_format = undefined, .pipeline = undefined, .overlay_pipeline = undefined, + .crt_pipeline = undefined, .atlas_tex = undefined, .atlas_sampler = undefined, .atlas_xfer = undefined, @@ -1033,12 +1078,21 @@ const StdinFeed = struct { f.fill = len - seq_start; break; }; - if (parseFinger(body[0..end])) |finger| { + if (parseFinger(body[0..end])) |finger_in| { + var finger = finger_in; if (g) |gp| { var wpw: c_int = 0; var wph: c_int = 0; _ = c.SDL_GetWindowSizeInPixels(gp.window, &wpw, &wph); - handleFinger(&gp.touch, core, finger, @floatFromInt(@max(wpw, 1)), @floatFromInt(@max(wph, 1)), @floatFromInt(gp.cell_w), @floatFromInt(gp.cell_h)); + const win_w: f32 = @floatFromInt(@max(wpw, 1)); + const win_h: f32 = @floatFromInt(@max(wph, 1)); + if (core.crt_on) { + // touch points are physical: same CRT mapping as the mouse + const m = crt.map(finger.x * win_w, finger.y * win_h, win_w, win_h); + finger.x = m.x / win_w; + finger.y = m.y / win_h; + } + handleFinger(&gp.touch, core, finger, win_w, win_h, @floatFromInt(gp.cell_w), @floatFromInt(gp.cell_h)); } else { handleFinger(&grid_touch, core, finger, @floatFromInt(core.screen_w), @floatFromInt(core.screen_h), 1, 1); } @@ -1249,6 +1303,9 @@ fn dispatch(g: *Gui, core: *pardes.Pardes, sev: *const c.SDL_Event) void { const m = sev.motion; g.mouse_x = m.x; g.mouse_y = m.y; + // pad cursor continues from wherever the pointer last was + g.pad_x = m.x; + g.pad_y = m.y; // drag = motion with a button held (selection extension keys off it) const held: ?pardes.Mouse.Button = if ((m.state & c.SDL_BUTTON_LMASK) != 0) .left @@ -1306,6 +1363,12 @@ fn dispatch(g: *Gui, core: *pardes.Pardes, sev: *const c.SDL_Event) void { var win_h: f32 = @floatFromInt(@max(ph, 1)); var fcw: f32 = @floatFromInt(g.cell_w); var fch: f32 = @floatFromInt(g.cell_h); + if (!is_emscripten and core.crt_on) { + // touch points are physical too: same CRT mapping as the mouse + const m = crt.map(finger.x * win_w, finger.y * win_h, win_w, win_h); + finger.x = m.x / win_w; + finger.y = m.y / win_h; + } if (is_emscripten) { // remap normalized coords into the letterboxed grid so the // shared two-finger machine's cell math stays exact @@ -1333,22 +1396,104 @@ fn dispatch(g: *Gui, core: *pardes.Pardes, sev: *const c.SDL_Event) void { g.gamepad = null; } }, - c.SDL_EVENT_GAMEPAD_BUTTON_DOWN, c.SDL_EVENT_GAMEPAD_BUTTON_UP => { - const down = sev.type == c.SDL_EVENT_GAMEPAD_BUTTON_DOWN; - const col = pixelCell(g.pad_x, g.cell_w); - const row = pixelCell(g.pad_y, g.cell_h); - const button: pardes.Mouse.Button = switch (sev.gbutton.button) { - c.SDL_GAMEPAD_BUTTON_SOUTH => .left, // A - c.SDL_GAMEPAD_BUTTON_EAST => .right, // B - c.SDL_GAMEPAD_BUTTON_NORTH => .middle, // Y - c.SDL_GAMEPAD_BUTTON_LEFT_SHOULDER => .wheel_up, - c.SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER => .wheel_down, - else => return, + // deck.zig maps buttons/triggers/trackpads to pointer, clicks, + // wheel, keys and rumble; this arm just applies its actions + c.SDL_EVENT_GAMEPAD_BUTTON_DOWN, + c.SDL_EVENT_GAMEPAD_BUTTON_UP, + c.SDL_EVENT_GAMEPAD_AXIS_MOTION, + c.SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN, + c.SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION, + c.SDL_EVENT_GAMEPAD_TOUCHPAD_UP, + => { + const in: deck.Input = switch (sev.type) { + c.SDL_EVENT_GAMEPAD_BUTTON_DOWN, c.SDL_EVENT_GAMEPAD_BUTTON_UP => .{ .button = .{ + .idx = sev.gbutton.button, + .down = sev.gbutton.down, + } }, + c.SDL_EVENT_GAMEPAD_AXIS_MOTION => .{ .axis = .{ + .idx = sev.gaxis.axis, + .value = sev.gaxis.value, + } }, + else => .{ .touch = .{ + .pad = @intCast(std.math.clamp(sev.gtouchpad.touchpad, 0, 255)), + .phase = switch (sev.type) { + c.SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN => .down, + c.SDL_EVENT_GAMEPAD_TOUCHPAD_UP => .up, + else => .motion, + }, + .x = sev.gtouchpad.x, + .y = sev.gtouchpad.y, + } }, + }; + var acts: [2]deck.Action = undefined; + for (acts[0..g.deck.feed(in, &acts)]) |act| switch (act) { + .move => |mv| { + var pw: c_int = 0; + var ph: c_int = 0; + _ = c.SDL_GetWindowSizeInPixels(g.window, &pw, &ph); + // pad_x/pad_y stay PHYSICAL; mouseCell applies the CRT + // mapping once at cell conversion, never into the pad + const old = mouseCell(g, core, g.pad_x, g.pad_y); + g.pad_x = std.math.clamp(g.pad_x + mv.dx, 0, @as(f32, @floatFromInt(@max(pw, 1))) - 1); + g.pad_y = std.math.clamp(g.pad_y + mv.dy, 0, @as(f32, @floatFromInt(@max(ph, 1))) - 1); + // the SDL cursor (plan9 arrow) rides along, so the pad + // cursor and a hardware mouse are one visible pointer + c.SDL_WarpMouseInWindow(g.window, g.pad_x, g.pad_y); + const mc = mouseCell(g, core, g.pad_x, g.pad_y); + if (mc.col == old.col and mc.row == old.row) continue; + // moving with a click held drags, so selections stretch + const held: ?pardes.Mouse.Button = if (g.deck.r2_held) + .left + else if (g.deck.l2_held) + .middle + else if (g.deck.look_held) + .right + else + null; + core.update(.{ .mouse = .{ + .button = held orelse .none, + .kind = if (held != null) .drag else .motion, + .col = mc.col, + .row = mc.row, + } }); + }, + .click => |ck| { + const mc = mouseCell(g, core, g.pad_x, g.pad_y); + core.update(.{ .mouse = .{ + .button = switch (ck.which) { + .select => .left, + .execute => .middle, + .look => .right, + }, + .kind = if (ck.down) .press else .release, + .col = mc.col, + .row = mc.row, + } }); + }, + .wheel => |ticks| { + const mc = mouseCell(g, core, g.pad_x, g.pad_y); + var left = ticks; + while (left != 0) { + left += if (ticks > 0) -1 else 1; + core.update(.{ .mouse = .{ + .button = if (ticks > 0) .wheel_down else .wheel_up, + .kind = .press, + .col = mc.col, + .row = mc.row, + } }); + } + }, + .key => |k| core.update(.{ .key = switch (k) { + .n => .{ .cp = 'n', .text = "n" }, + .cap_n => .{ .cp = 'N', .text = "N" }, + .enter => .{ .cp = pardes.Key.enter }, + .tab => .{ .cp = pardes.Key.tab }, + } }), + // a brief gentle ack for execute/look, not a buzz + .rumble => if (g.gamepad) |pad| { + _ = c.SDL_RumbleGamepad(pad, 0x4000, 0x4000, 80); + }, }; - switch (button) { - .wheel_up, .wheel_down => if (down) core.update(.{ .mouse = .{ .button = button, .kind = .press, .col = col, .row = row } }), - else => core.update(.{ .mouse = .{ .button = button, .kind = if (down) .press else .release, .col = col, .row = row } }), - } }, else => {}, } @@ -1396,9 +1541,24 @@ fn pixelCellF(px: f32, cell: f32) u16 { } /// mouse coords → cell. Native windows are pixel-exact; web SDL reports CSS -/// points into a letterboxed canvas backed at web_render_scale. +/// points into a letterboxed canvas backed at web_render_scale. With the Crt +/// builtin on, the physical point first maps to the scene point the shader +/// displays there (crt.map) — this is the one physical→logical spot shared +/// by the real mouse and the deck's virtual cursor. fn mouseCell(g: *const Gui, core: *const pardes.Pardes, x: f32, y: f32) struct { col: u16, row: u16 } { - if (!is_emscripten) return .{ .col = pixelCell(x, g.cell_w), .row = pixelCell(y, g.cell_h) }; + if (!is_emscripten) { + var mx = x; + var my = y; + if (core.crt_on) { + var pw: c_int = 0; + var ph: c_int = 0; + _ = c.SDL_GetWindowSizeInPixels(g.window, &pw, &ph); + const m = crt.map(mx, my, @floatFromInt(@max(pw, 1)), @floatFromInt(@max(ph, 1))); + mx = m.x; + my = m.y; + } + return .{ .col = pixelCell(mx, g.cell_w), .row = pixelCell(my, g.cell_h) }; + } var pw: c_int = 0; var ph: c_int = 0; _ = c.SDL_GetWindowSizeInPixels(g.window, &pw, &ph); @@ -1427,22 +1587,23 @@ fn pollGamepad(g: *Gui, core: *pardes.Pardes) void { const deadzone: f32 = 8000; const ax: f32 = @floatFromInt(c.SDL_GetGamepadAxis(pad, c.SDL_GAMEPAD_AXIS_LEFTX)); const ay: f32 = @floatFromInt(c.SDL_GetGamepadAxis(pad, c.SDL_GAMEPAD_AXIS_LEFTY)); - const old_col = pixelCell(g.pad_x, g.cell_w); - const old_row = pixelCell(g.pad_y, g.cell_h); - // full tilt ≈ one cell-height per frame; ~60fps makes that a brisk glide - const speed: f32 = @floatFromInt(g.cell_h); + const old = mouseCell(g, core, g.pad_x, g.pad_y); + // full tilt ≈ 0.4 cell-heights per frame: a gentle, aimable glide + const speed: f32 = @as(f32, @floatFromInt(g.cell_h)) * 0.4; if (@abs(ax) > deadzone) g.pad_x = std.math.clamp(g.pad_x + ax / 32767.0 * speed, 0, win_w - 1); if (@abs(ay) > deadzone) g.pad_y = std.math.clamp(g.pad_y + ay / 32767.0 * speed, 0, win_h - 1); - const col = pixelCell(g.pad_x, g.cell_w); - const row = pixelCell(g.pad_y, g.cell_h); - if (col != old_col or row != old_row) - core.update(.{ .mouse = .{ .button = .none, .kind = .motion, .col = col, .row = row } }); + // only on actual stick movement — an unconditional per-frame warp would + // pin the pointer and fight any hardware mouse + if (@abs(ax) > deadzone or @abs(ay) > deadzone) c.SDL_WarpMouseInWindow(g.window, g.pad_x, g.pad_y); + const mc = mouseCell(g, core, g.pad_x, g.pad_y); + if (mc.col != old.col or mc.row != old.row) + core.update(.{ .mouse = .{ .button = .none, .kind = .motion, .col = mc.col, .row = mc.row } }); const ry: f32 = @floatFromInt(c.SDL_GetGamepadAxis(pad, c.SDL_GAMEPAD_AXIS_RIGHTY)); if (@abs(ry) > deadzone) g.pad_scroll += ry / 32767.0 * 0.15; while (@abs(g.pad_scroll) >= 1.0) { const button: pardes.Mouse.Button = if (g.pad_scroll < 0) .wheel_up else .wheel_down; g.pad_scroll += if (g.pad_scroll < 0) 1.0 else -1.0; - core.update(.{ .mouse = .{ .button = button, .kind = .press, .col = col, .row = row } }); + core.update(.{ .mouse = .{ .button = button, .kind = .press, .col = mc.col, .row = mc.row } }); } } @@ -1524,15 +1685,6 @@ fn drainEffects( defer gpa.free(z); _ = c.SDL_SetClipboardText(z.ptr); }, - .get_clipboard => { - if (g == null) continue; - // synchronous, unlike the tty's OSC 52 round-trip: reply is a paste - const t = c.SDL_GetClipboardText(); - defer c.SDL_free(t); - var tlen: usize = 0; - while (t[tlen] != 0) : (tlen += 1) {} - if (tlen > 0) core.update(.{ .paste = t[0..tlen] }); - }, .quit => {}, }; } @@ -1570,9 +1722,8 @@ fn drainEffectsWeb(core: *pardes.Pardes, gpa: std.mem.Allocator, g: *Gui) void { }, // look on a URL → a new tab .open_link => |url| openLinkWeb(gpa, url.slice()), - // nothing to spawn/write/resize/save/dump into; get_clipboard would - // need an async permission round-trip — all no-ops - .spawn, .write, .resize_pty, .save_file, .write_dump, .get_clipboard, .quit => {}, + // nothing to spawn/write/resize/save/dump into — all no-ops + .spawn, .write, .resize_pty, .save_file, .write_dump, .quit => {}, }; } @@ -1595,7 +1746,7 @@ fn openLinkWeb(gpa: std.mem.Allocator, url: []const u8) void { // render: Surface → instanced quads → SDL GPU // ===================================================================== -fn renderFrame(g: *Gui, gpa: std.mem.Allocator, surface: *pardes.Surface) !void { +fn renderFrame(g: *Gui, gpa: std.mem.Allocator, surface: *pardes.Surface, crt_on: bool) !void { const cmd = c.SDL_AcquireGPUCommandBuffer(g.device) orelse return; var sw: u32 = 0; var sh: u32 = 0; @@ -1624,6 +1775,11 @@ fn renderFrame(g: *Gui, gpa: std.mem.Allocator, surface: *pardes.Surface) !void const win_w: f32 = @floatFromInt(sw); const win_h: f32 = @floatFromInt(sh); + // Crt on: the scene pass lands in an offscreen texture, then a second + // pass draws it through the CRT shader onto the real target + if (crt_on) try ensureSceneTexture(g, sw, sh); + const scene: *c.SDL_GPUTexture = if (crt_on) g.scene_tex.? else target; + const overlay_count = buildTouchOverlay(g, sw, sh); if (overlay_count != 0) uploadOverlayGpu(g, cmd, overlay_count); @@ -1631,7 +1787,7 @@ fn renderFrame(g: *Gui, gpa: std.mem.Allocator, surface: *pardes.Surface) !void if (cells != 0) try ensureVbuf(g, cells); var color_target = std.mem.zeroes(c.SDL_GPUColorTargetInfo); - color_target.texture = target; + color_target.texture = scene; color_target.clear_color = .{ .r = @as(f32, @floatFromInt(bg_default[0])) / 255.0, .g = @as(f32, @floatFromInt(bg_default[1])) / 255.0, @@ -1688,6 +1844,19 @@ fn renderFrame(g: *Gui, gpa: std.mem.Allocator, surface: *pardes.Surface) !void c.SDL_EndGPURenderPass(rp); } + if (crt_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 rp = c.SDL_BeginGPURenderPass(cmd, &crt_target, 1, null); + c.SDL_BindGPUGraphicsPipeline(rp, g.crt_pipeline); + const samp_binding = c.SDL_GPUTextureSamplerBinding{ .texture = g.scene_tex.?, .sampler = g.atlas_sampler }; + c.SDL_BindGPUFragmentSamplers(rp, 0, &samp_binding, 1); + c.SDL_DrawGPUPrimitives(rp, 3, 1, 0, 0); + c.SDL_EndGPURenderPass(rp); + } + if (g.capture) return captureFrame(g, gpa, cmd, target, sw, sh); _ = c.SDL_SubmitGPUCommandBuffer(cmd); } @@ -1904,6 +2073,26 @@ fn makeOverlayPipeline(device: *c.SDL_GPUDevice, color_format: c.SDL_GPUTextureF return c.SDL_CreateGPUGraphicsPipeline(device, &info) orelse error.GpuCreate; } +// the CRT pass: a fullscreen triangle sampling the scene texture, no vertex +// buffers at all (positions from gl_VertexIndex) +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); + defer c.SDL_ReleaseGPUShader(device, vs); + const fs = try makeShader(device, crt_frag_spv, c.SDL_GPU_SHADERSTAGE_FRAGMENT, 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) !*c.SDL_GPUShader { var info = std.mem.zeroes(c.SDL_GPUShaderCreateInfo); info.code = code.ptr; @@ -2054,6 +2243,23 @@ fn makeGlShader(kind: c_uint, source: []const u8) !c_uint { // PARDES_TEST frame capture: download the render target, write latest.ppm // ===================================================================== +fn ensureSceneTexture(g: *Gui, width: u32, height: u32) !void { + if (g.scene_tex != null and g.scene_tex_w == width and g.scene_tex_h == height) return; + if (g.scene_tex) |t| c.SDL_ReleaseGPUTexture(g.device, t); + var info = std.mem.zeroes(c.SDL_GPUTextureCreateInfo); + info.type = c.SDL_GPU_TEXTURETYPE_2D; + info.format = g.swapchain_format; + info.usage = c.SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | c.SDL_GPU_TEXTUREUSAGE_SAMPLER; + info.width = width; + info.height = height; + info.layer_count_or_depth = 1; + info.num_levels = 1; + info.sample_count = c.SDL_GPU_SAMPLECOUNT_1; + g.scene_tex = c.SDL_CreateGPUTexture(g.device, &info) orelse return error.GpuCreate; + g.scene_tex_w = width; + g.scene_tex_h = height; +} + fn ensureCaptureTexture(g: *Gui, width: u32, height: u32) !void { if (g.capture_tex != null and g.capture_tex_w == width and g.capture_tex_h == height) return; if (g.capture_tex) |t| c.SDL_ReleaseGPUTexture(g.device, t); |
