summaryrefslogtreecommitdiff
path: root/src/gui.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-08 12:25:04 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:07 -0300
commit172eb56fee0b6bde698e72be2bca55a90fe81ce9 (patch)
treef2fd94709facf134c3670db4c1a20a5eaca1d53d /src/gui.zig
parentb91e5ebf8b529d0ced0bf18e83b17df0bd2374ce (diff)
downloadpardes-172eb56fee0b6bde698e72be2bca55a90fe81ce9.tar.gz
pardes-172eb56fee0b6bde698e72be2bca55a90fe81ce9.zip
steam deck nice version
Diffstat (limited to 'src/gui.zig')
-rw-r--r--src/gui.zig298
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);