summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build.zig9
-rw-r--r--main.zig26
-rw-r--r--replay.zig20
-rw-r--r--shaders/overlay.frag.es.glsl11
-rw-r--r--shaders/overlay.frag.glsl9
-rw-r--r--shaders/overlay.vert.es.glsl13
-rw-r--r--shaders/overlay.vert.glsl11
-rw-r--r--ui_sdl.zig467
8 files changed, 555 insertions, 11 deletions
diff --git a/build.zig b/build.zig
index b3ae8e37..8b1b6a3b 100644
--- a/build.zig
+++ b/build.zig
@@ -250,6 +250,8 @@ pub fn build(b: *std.Build) void {
if (is_emscripten) {
ui_mod.addAnonymousImport("ui.vert.es.glsl", .{ .root_source_file = b.path("shaders/ui.vert.es.glsl") });
ui_mod.addAnonymousImport("ui.frag.es.glsl", .{ .root_source_file = b.path("shaders/ui.frag.es.glsl") });
+ ui_mod.addAnonymousImport("overlay.vert.es.glsl", .{ .root_source_file = b.path("shaders/overlay.vert.es.glsl") });
+ ui_mod.addAnonymousImport("overlay.frag.es.glsl", .{ .root_source_file = b.path("shaders/overlay.frag.es.glsl") });
} else {
// GLSL -> SPIR-V at build time, embedded into ui_sdl.zig via @embedFile.
ui_mod.addAnonymousImport("ui.vert.spv", .{
@@ -258,6 +260,12 @@ pub fn build(b: *std.Build) void {
ui_mod.addAnonymousImport("ui.frag.spv", .{
.root_source_file = compileGlsl(b, "shaders/ui.frag.glsl", "fragment", "ui.frag.spv"),
});
+ ui_mod.addAnonymousImport("overlay.vert.spv", .{
+ .root_source_file = compileGlsl(b, "shaders/overlay.vert.glsl", "vertex", "overlay.vert.spv"),
+ });
+ ui_mod.addAnonymousImport("overlay.frag.spv", .{
+ .root_source_file = compileGlsl(b, "shaders/overlay.frag.glsl", "fragment", "overlay.frag.spv"),
+ });
}
}
@@ -431,6 +439,7 @@ pub fn build(b: *std.Build) void {
\\ image-rendering: crisp-edges; image-rendering: pixelated;
\\ -ms-interpolation-mode: nearest-neighbor;
\\ transform: translateZ(0); backface-visibility: hidden; }
+ \\ touch-action: none; }
\\ </style>
\\</head>
\\<body>
diff --git a/main.zig b/main.zig
index dbd35f68..b96ea3a5 100644
--- a/main.zig
+++ b/main.zig
@@ -150,6 +150,7 @@ pub const Command = struct {
winsize: vaxis.Winsize,
mouse: vaxis.Mouse,
paste: []const u8, // OSC 52 system-clipboard read reply (the loop decodes it)
+ pinch: f32,
} = .nop;
};
@@ -176,6 +177,7 @@ const Cli = struct {
tty_toggle: u21 = default_tty_toggle,
help: bool = false,
perf_startup: bool = false,
+ debug_overlay: bool = false,
};
const help_text =
@@ -185,8 +187,9 @@ const help_text =
\\ --tty start as one shell pane already in tty mode
\\ --tty-toggle <key> use Ctrl-<key> to enter/leave tty mode
\\ --tty-toggle=<key> same as above; accepts b, Ctrl-b, or ^b
+ \\ --debug-overlay start with debug overlay visible
\\ -h, --help show this help and exit
- \\
+ \\\;
;
fn printHelp(io: std.Io) !void {
@@ -225,6 +228,8 @@ fn parseCli(args: []const [:0]const u8) !Cli {
i += 1;
if (i >= args.len) return error.BadArgs;
cli.tty_toggle = parseCtrlKey(args[i]) orelse return error.BadArgs;
+ } else if (std.mem.eql(u8, a, "--debug-overlay")) {
+ cli.debug_overlay = true;
} else {
return error.BadArgs;
}
@@ -305,6 +310,7 @@ var colors_on: bool = true;
// The stats overlay, toggled by the "Debug" builtin. Global (like colors_on) so the
// central command dispatcher can flip it; the render reads it.
var show_debug: bool = false;
+var ov_pinch_scale: f32 = 1.0;
// Builtins. Executing the name (middle-click / Tab) runs the action through the ONE
// dispatcher (onSelect + actOnSelection), no matter where the name is shown — a pane
@@ -2804,6 +2810,7 @@ pub fn main(init: std.process.Init) !void {
const tty_only = cli.tty_only;
const tty_toggle = cli.tty_toggle;
const perf_startup = cli.perf_startup;
+ if (cli.debug_overlay) show_debug = true;
var screen_w: u16 = 80;
var screen_h: u16 = 24;
@@ -2830,7 +2837,7 @@ pub fn main(init: std.process.Init) !void {
var frame_arena: std.heap.ArenaAllocator = .init(gpa);
defer frame_arena.deinit();
defer freeYank(gpa);
- var ov_buf: [128]u8 = undefined; // overlay text; must outlive each vx.render()
+ var ov_buf: [192]u8 = undefined; // overlay text; must outlive each vx.render()
// system_clipboard_allocator lets the loop's reader thread decode OSC 52 paste
// replies (delivered as .paste events). gpa is already used cross-thread by the
@@ -3383,6 +3390,9 @@ pub fn main(init: std.process.Init) !void {
normalPaste(gpa, t);
}
},
+ .pinch => |scale| {
+ ov_pinch_scale = scale;
+ },
.quit => break,
else => {},
}
@@ -3567,12 +3577,12 @@ pub fn main(init: std.process.Init) !void {
sb_total = sb.total;
}
const s = std.fmt.bufPrint(
- &ov_buf,
- "pardes resizes={d}\nterms {d} cols {d}\nactive #{d} {d}x{d}\nscroll {d}/{d}",
- .{ resize_count, nlive, layout.ncol, active, at.cols, at.rows, sb_off, sb_total },
- ) catch "";
- const ow: u16 = 26;
- const oh: u16 = 6;
+ &ov_buf,
+ "pardes resizes={d}\nterms {d} cols {d}\nactive #{d} {d}x{d}\nscroll {d}/{d}\npinch {d:6.4}",
+ .{ resize_count, nlive, layout.ncol, active, at.cols, at.rows, sb_off, sb_total, ov_pinch_scale },
+ ) catch "";
+ const ow: u16 = 30;
+ const oh: u16 = 7;
const ox: u16 = if (screen_w > ow) screen_w - ow else 0;
const ov = win.child(.{ .x_off = ox, .y_off = TOPBAR_H, .width = ow, .height = oh, .border = .{ .where = .all } });
ov.fill(.{ .char = .{ .grapheme = " " }, .style = .{ .bg = .{ .rgb = gray.bar } } });
diff --git a/replay.zig b/replay.zig
index 19b894fe..04448eb9 100644
--- a/replay.zig
+++ b/replay.zig
@@ -82,6 +82,7 @@ pub const Command = struct {
winsize: ui.Winsize,
mouse: ui.Mouse,
paste: []const u8,
+ pinch: f32,
} = .nop;
};
@@ -293,6 +294,7 @@ fn render(
drag: Drag,
hover_col: u16,
hover_row: u16,
+ ov_pinch_scale: f32,
) !void {
var arena: std.heap.ArenaAllocator = .init(gpa);
defer arena.deinit();
@@ -317,6 +319,20 @@ fn render(
pane_ui.drawInteractionPreview(win, layout, rects, col_x, col_w, screen_h, drag, hover_col, hover_row, T().scroll_thumb);
+ // debug overlay with pinch scale
+ if (ov_pinch_scale != 1.0) {
+ var ov_buf: [64]u8 = undefined;
+ const s = std.fmt.bufPrint(&ov_buf, "pinch {d:6.4}", .{ov_pinch_scale}) catch "";
+ if (s.len > 0) {
+ const ow: u16 = 16;
+ const oh: u16 = 2;
+ const ox: u16 = if (screen_w > ow) screen_w - ow else 0;
+ const ov = win.child(.{ .x_off = ox, .y_off = TOPBAR_H, .width = ow, .height = oh, .border = .{ .where = .all } });
+ ov.fill(.{ .char = .{ .grapheme = " " }, .style = .{ .bg = rgbColor(T().scroll_track) } });
+ _ = ov.printSegment(.{ .text = s, .style = .{ .fg = rgbColor(accent) } }, .{});
+ }
+ }
+
try vx.render(writer);
}
@@ -424,6 +440,7 @@ const App = struct {
drag: Drag = .none,
hover_col: u16 = 0,
hover_row: u16 = 0,
+ ov_pinch_scale: f32 = 1.0,
fn init(
self: *App,
@@ -643,6 +660,7 @@ const App = struct {
if (key.matches('q', .{}) or key.matches(ui.Key.escape, .{}) or key.matches('c', .{ .ctrl = true })) return false;
},
.paste => |text| self.gpa.free(text),
+ .pinch => |scale| self.ov_pinch_scale = scale,
.quit, .close => return false,
else => {},
}
@@ -659,7 +677,7 @@ const App = struct {
p.rows = rows;
clampScroll(p);
}
- try render(&self.vx, self.gpa, self.tty.writer(), self.state, self.panes, &self.rects, &self.layout, &self.col_x, &self.col_w, self.screen_w, self.screen_h, self.active, self.drag, self.hover_col, self.hover_row);
+ try render(&self.vx, self.gpa, self.tty.writer(), self.state, self.panes, &self.rects, &self.layout, &self.col_x, &self.col_w, self.screen_w, self.screen_h, self.active, self.drag, self.hover_col, self.hover_row, self.ov_pinch_scale);
return true;
}
};
diff --git a/shaders/overlay.frag.es.glsl b/shaders/overlay.frag.es.glsl
new file mode 100644
index 00000000..37032c9a
--- /dev/null
+++ b/shaders/overlay.frag.es.glsl
@@ -0,0 +1,11 @@
+#version 300 es
+
+precision mediump float;
+
+in vec4 v_color;
+
+out vec4 o_col;
+
+void main() {
+ o_col = v_color;
+}
diff --git a/shaders/overlay.frag.glsl b/shaders/overlay.frag.glsl
new file mode 100644
index 00000000..86c0188b
--- /dev/null
+++ b/shaders/overlay.frag.glsl
@@ -0,0 +1,9 @@
+#version 450
+
+layout(location = 0) in vec4 v_color;
+
+layout(location = 0) out vec4 o_col;
+
+void main() {
+ o_col = v_color;
+}
diff --git a/shaders/overlay.vert.es.glsl b/shaders/overlay.vert.es.glsl
new file mode 100644
index 00000000..edea124d
--- /dev/null
+++ b/shaders/overlay.vert.es.glsl
@@ -0,0 +1,13 @@
+#version 300 es
+
+precision mediump float;
+
+layout(location = 0) in vec2 a_pos;
+layout(location = 1) in vec4 a_color;
+
+out vec4 v_color;
+
+void main() {
+ v_color = a_color;
+ gl_Position = vec4(a_pos, 0.0, 1.0);
+}
diff --git a/shaders/overlay.vert.glsl b/shaders/overlay.vert.glsl
new file mode 100644
index 00000000..5f6be593
--- /dev/null
+++ b/shaders/overlay.vert.glsl
@@ -0,0 +1,11 @@
+#version 450
+
+layout(location = 0) in vec2 a_pos;
+layout(location = 1) in vec4 a_color;
+
+layout(location = 0) out vec4 v_color;
+
+void main() {
+ v_color = a_color;
+ gl_Position = vec4(a_pos, 0.0, 1.0);
+}
diff --git a/ui_sdl.zig b/ui_sdl.zig
index ee7f118c..ea95f707 100644
--- a/ui_sdl.zig
+++ b/ui_sdl.zig
@@ -75,8 +75,12 @@ pub fn adoptAltScreen(vx: *Vaxis) void {
const font_ttf = @embedFile("assets/AdwaitaMono-Regular.ttf");
const vert_spv_bytes = if (is_emscripten) "" else @embedFile("ui.vert.spv");
const frag_spv_bytes = if (is_emscripten) "" else @embedFile("ui.frag.spv");
+const overlay_vert_spv_bytes = if (is_emscripten) "" else @embedFile("overlay.vert.spv");
+const overlay_frag_spv_bytes = if (is_emscripten) "" else @embedFile("overlay.frag.spv");
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 "";
+const overlay_vert_glsl_es = if (is_emscripten) @embedFile("overlay.vert.es.glsl") else "";
+const overlay_frag_glsl_es = if (is_emscripten) @embedFile("overlay.frag.es.glsl") else "";
const log = std.log.scoped(.ui_sdl);
const web_render_scale: f32 = ui_sdl_config.web_render_scale;
@@ -107,12 +111,15 @@ swapchain_format: c.SDL_GPUTextureFormat,
present_mode: c.SDL_GPUPresentMode,
pipeline: *c.SDL_GPUGraphicsPipeline,
+overlay_pipeline: *c.SDL_GPUGraphicsPipeline,
atlas_tex: *c.SDL_GPUTexture,
atlas_sampler: *c.SDL_GPUSampler,
atlas_xfer: *c.SDL_GPUTransferBuffer,
vbuf: *c.SDL_GPUBuffer,
vxfer: *c.SDL_GPUTransferBuffer,
+overlay_vbuf: *c.SDL_GPUBuffer,
+overlay_vxfer: *c.SDL_GPUTransferBuffer,
vbuf_cells: u32 = 0, // capacity (cols*rows) the vbuf/vxfer were sized for
cell_keys: []CellKey = &.{},
dirty_ranges: []DirtyRange = &.{},
@@ -124,10 +131,14 @@ last_render_h: u32 = 0,
// GLuint/GLint values, kept as plain C integers so native builds need no GL API.
gl_context: ?c.SDL_GLContext = null,
gl_program: c_uint = 0,
+gl_overlay_program: c_uint = 0,
gl_atlas_tex: c_uint = 0,
gl_vbo: c_uint = 0,
+gl_overlay_vbo: c_uint = 0,
gl_u_atlas: c_int = -1,
gl_instances: []CellInstance = &.{},
+overlay_vertices: []OverlayVertex = &.{},
+touches: [max_touch_points]TouchPoint = [_]TouchPoint{.{}} ** max_touch_points,
// font + cell metrics
font: ?*c.UIFont,
@@ -178,6 +189,10 @@ pub const atlas_h: u32 = if (is_emscripten) 2048 else 1024;
pub const Slot = struct { u: u32, v: u32 };
pub const CellKey = packed struct { cp: u32, fg: u32, bg: u32 };
pub const DirtyRange = struct { start: u32, count: u32 };
+pub const max_touch_points: usize = 16;
+pub const max_touch_trail_points: usize = 8;
+pub const overlay_circle_vertices: usize = 16 * 3;
+pub const max_overlay_vertices: usize = max_touch_points * (1100 + max_touch_trail_points * overlay_circle_vertices);
// One instance per cell. The vertex shader expands each instance into a 2-triangle
// quad using gl_VertexIndex, so dirty uploads move 56 B/cell instead of 240 B/cell.
@@ -198,6 +213,45 @@ const CellInstance = extern struct {
bb: f32,
};
+const OverlayVertex = extern struct {
+ x: f32,
+ y: f32,
+ r: f32,
+ g: f32,
+ b: f32,
+ a: f32,
+};
+
+const TouchSample = struct {
+ x: f32 = 0,
+ y: f32 = 0,
+ pressure: f32 = 1,
+};
+
+const TouchPoint = struct {
+ active: bool = false,
+ id: u64 = 0,
+ x: f32 = 0,
+ y: f32 = 0,
+ pressure: f32 = 1,
+ trail: [max_touch_trail_points]TouchSample = [_]TouchSample{.{}} ** max_touch_trail_points,
+ trail_len: usize = 0,
+ trail_next: usize = 0,
+
+ fn pushTrail(self: *TouchPoint, x: f32, y: f32, pressure: f32) void {
+ self.trail[self.trail_next] = .{ .x = x, .y = y, .pressure = pressure };
+ self.trail_next = (self.trail_next + 1) % max_touch_trail_points;
+ if (self.trail_len < max_touch_trail_points) self.trail_len += 1;
+ }
+};
+
+const OverlayColor = struct {
+ r: f32,
+ g: f32,
+ b: f32,
+ a: f32,
+};
+
const CellLayout = struct {
w: f32,
h: f32,
@@ -497,9 +551,17 @@ pub fn init(
// ---- pipeline ----
const pipeline = try makePipeline(device, swapchain_format);
+ const overlay_pipeline = try makeOverlayPipeline(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 };
+ const overlay_vbuf = c.SDL_CreateGPUBuffer(device, &ovb_info) orelse return error.GpuCreate;
+ var ovx_info = c.SDL_GPUTransferBufferCreateInfo{ .usage = c.SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD, .size = overlay_buffer_size, .props = 0 };
+ const overlay_vxfer = c.SDL_CreateGPUTransferBuffer(device, &ovx_info) orelse return error.GpuCreate;
const atlas_stage = try alloc.alloc(u8, atlas_w * atlas_h);
@memset(atlas_stage, 0);
+ const overlay_vertices = try alloc.alloc(OverlayVertex, max_overlay_vertices);
var self: SdlBackend = .{
.io = io,
@@ -509,11 +571,15 @@ pub fn init(
.swapchain_format = swapchain_format,
.present_mode = present_mode,
.pipeline = pipeline,
+ .overlay_pipeline = overlay_pipeline,
.atlas_tex = atlas_tex,
.atlas_sampler = atlas_sampler,
.atlas_xfer = atlas_xfer,
.vbuf = undefined,
.vxfer = undefined,
+ .overlay_vbuf = overlay_vbuf,
+ .overlay_vxfer = overlay_vxfer,
+ .overlay_vertices = overlay_vertices,
.font = font,
.scale = scale,
.cell_w = @intCast(@max(cw, 1)),
@@ -581,6 +647,7 @@ fn initGl(
const ascent: c_int = @intFromFloat(@ceil(@as(f32, @floatFromInt(@max(asc, 1))) * web_render_scale));
const program = try makeGlProgram();
+ const overlay_program = try makeOverlayGlProgram();
var atlas_tex: c_uint = 0;
c.glGenTextures(1, &atlas_tex);
c.glBindTexture(c.GL_TEXTURE_2D, atlas_tex);
@@ -594,9 +661,12 @@ fn initGl(
var vbo: c_uint = 0;
c.glGenBuffers(1, &vbo);
+ var overlay_vbo: c_uint = 0;
+ c.glGenBuffers(1, &overlay_vbo);
const atlas_stage = try alloc.alloc(u8, atlas_w * atlas_h);
@memset(atlas_stage, 0);
+ const overlay_vertices = try alloc.alloc(OverlayVertex, max_overlay_vertices);
var self: SdlBackend = .{
.io = io,
@@ -606,16 +676,22 @@ fn initGl(
.swapchain_format = undefined,
.present_mode = c.SDL_GPU_PRESENTMODE_VSYNC,
.pipeline = undefined,
+ .overlay_pipeline = undefined,
.atlas_tex = undefined,
.atlas_sampler = undefined,
.atlas_xfer = undefined,
.vbuf = undefined,
.vxfer = undefined,
+ .overlay_vbuf = undefined,
+ .overlay_vxfer = undefined,
.gl_context = gl_context,
.gl_program = program,
+ .gl_overlay_program = overlay_program,
.gl_atlas_tex = atlas_tex,
.gl_vbo = vbo,
+ .gl_overlay_vbo = overlay_vbo,
.gl_u_atlas = c.glGetUniformLocation(program, "u_atlas"),
+ .overlay_vertices = overlay_vertices,
.font = font,
.scale = scale,
.cell_w = cell_w,
@@ -650,11 +726,14 @@ fn initTest(io: std.Io, alloc: std.mem.Allocator) !SdlBackend {
.swapchain_format = undefined,
.present_mode = c.SDL_GPU_PRESENTMODE_VSYNC,
.pipeline = undefined,
+ .overlay_pipeline = undefined,
.atlas_tex = undefined,
.atlas_sampler = undefined,
.atlas_xfer = undefined,
.vbuf = undefined,
.vxfer = undefined,
+ .overlay_vbuf = undefined,
+ .overlay_vxfer = undefined,
.font = null,
.scale = 1,
.cell_w = 10,
@@ -676,10 +755,13 @@ pub fn deinit(self: *SdlBackend, alloc: ?std.mem.Allocator, writer: *std.Io.Writ
return;
}
if (is_emscripten) {
+ if (self.gl_overlay_vbo != 0) c.glDeleteBuffers(1, &self.gl_overlay_vbo);
if (self.gl_vbo != 0) c.glDeleteBuffers(1, &self.gl_vbo);
if (self.gl_atlas_tex != 0) c.glDeleteTextures(1, &self.gl_atlas_tex);
+ if (self.gl_overlay_program != 0) c.glDeleteProgram(self.gl_overlay_program);
if (self.gl_program != 0) c.glDeleteProgram(self.gl_program);
self.alloc.free(self.gl_instances);
+ self.alloc.free(self.overlay_vertices);
if (self.font) |f| c.ui_font_free(f);
self.screen.deinit(self.alloc);
self.glyphs.deinit();
@@ -692,13 +774,17 @@ pub fn deinit(self: *SdlBackend, alloc: ?std.mem.Allocator, writer: *std.Io.Writ
if (self.capture_tex) |t| c.SDL_ReleaseGPUTexture(self.device, t);
if (self.capture_xfer) |x| c.SDL_ReleaseGPUTransferBuffer(self.device, x);
if (self.trace_fd >= 0 and self.trace_fd != 2) closeFdBestEffort(self.trace_fd);
+ c.SDL_ReleaseGPUBuffer(self.device, self.overlay_vbuf);
+ c.SDL_ReleaseGPUTransferBuffer(self.device, self.overlay_vxfer);
c.SDL_ReleaseGPUBuffer(self.device, self.vbuf);
c.SDL_ReleaseGPUTransferBuffer(self.device, self.vxfer);
+ self.alloc.free(self.overlay_vertices);
self.alloc.free(self.cell_keys);
self.alloc.free(self.dirty_ranges);
c.SDL_ReleaseGPUTransferBuffer(self.device, self.atlas_xfer);
c.SDL_ReleaseGPUSampler(self.device, self.atlas_sampler);
c.SDL_ReleaseGPUTexture(self.device, self.atlas_tex);
+ c.SDL_ReleaseGPUGraphicsPipeline(self.device, self.overlay_pipeline);
c.SDL_ReleaseGPUGraphicsPipeline(self.device, self.pipeline);
if (self.font) |f| c.ui_font_free(f);
self.screen.deinit(self.alloc);
@@ -787,6 +873,8 @@ pub fn render(self: *SdlBackend, writer: *std.Io.Writer) !void {
const t_acquired = nowNs();
const win_w: f32 = @floatFromInt(sw);
const win_h: f32 = @floatFromInt(sh);
+ const overlay_vertex_count = self.buildTouchOverlay(sw, sh);
+ if (overlay_vertex_count != 0) self.uploadOverlayGpu(cmd, overlay_vertex_count);
const cols = self.screen.width;
const rows = self.screen.height;
@@ -873,6 +961,7 @@ pub fn render(self: *SdlBackend, writer: *std.Io.Writer) !void {
const vbinding = c.SDL_GPUBufferBinding{ .buffer = self.vbuf, .offset = 0 };
c.SDL_BindGPUVertexBuffers(rp, 0, &vbinding, 1);
c.SDL_DrawGPUPrimitives(rp, 6, cells, 0, 0);
+ self.drawOverlayGpu(rp, overlay_vertex_count);
c.SDL_EndGPURenderPass(rp);
t_drawn = nowNs();
} else {
@@ -888,6 +977,7 @@ pub fn render(self: *SdlBackend, writer: *std.Io.Writer) !void {
color_target.load_op = c.SDL_GPU_LOADOP_CLEAR;
color_target.store_op = c.SDL_GPU_STOREOP_STORE;
const rp = c.SDL_BeginGPURenderPass(cmd, &color_target, 1, null);
+ self.drawOverlayGpu(rp, overlay_vertex_count);
c.SDL_EndGPURenderPass(rp);
t_drawn = nowNs();
}
@@ -926,7 +1016,9 @@ fn renderGl(self: *SdlBackend) !void {
const cols = self.screen.width;
const rows = self.screen.height;
const cells: u32 = @as(u32, cols) * @as(u32, rows);
+ const overlay_vertex_count = self.buildTouchOverlay(sw, sh);
if (cells == 0) {
+ self.drawOverlayGl(overlay_vertex_count);
_ = c.SDL_GL_SwapWindow(self.sdl_window);
return;
}
@@ -967,6 +1059,7 @@ fn renderGl(self: *SdlBackend) !void {
c.glVertexAttribDivisor(3, 1);
c.glDrawArraysInstanced(c.GL_TRIANGLES, 0, 6, @intCast(cells));
+ self.drawOverlayGl(overlay_vertex_count);
_ = c.SDL_GL_SwapWindow(self.sdl_window);
}
@@ -1052,6 +1145,315 @@ fn mouseCell(self: *const SdlBackend, x: f32, y: f32) MouseCell {
};
}
+fn clampF32(value: f32, lo: f32, hi: f32) f32 {
+ if (value < lo) return lo;
+ if (value > hi) return hi;
+ return value;
+}
+
+fn findTouchSlot(self: *SdlBackend, id: u64) ?usize {
+ var i: usize = 0;
+ while (i < self.touches.len) : (i += 1) {
+ if (self.touches[i].active and self.touches[i].id == id) return i;
+ }
+ return null;
+}
+
+fn inactiveTouchSlot(self: *SdlBackend) ?usize {
+ var i: usize = 0;
+ while (i < self.touches.len) : (i += 1) {
+ if (!self.touches[i].active) return i;
+ }
+ return null;
+}
+
+fn updateTouch(self: *SdlBackend, tfinger: c.SDL_TouchFingerEvent) void {
+ const id: u64 = @intCast(tfinger.fingerID);
+ switch (tfinger.type) {
+ c.SDL_EVENT_FINGER_UP, c.SDL_EVENT_FINGER_CANCELED => {
+ if (self.findTouchSlot(id)) |idx| self.touches[idx].active = false;
+ },
+ else => {
+ const n: u64 = self.touches.len;
+ const idx = self.findTouchSlot(id) orelse inactiveTouchSlot(self) orelse @as(usize, @intCast(id % n));
+ if (!self.touches[idx].active or self.touches[idx].id != id) {
+ self.touches[idx] = .{ .active = true, .id = id };
+ } else {
+ self.touches[idx].active = true;
+ }
+ self.touches[idx].x = tfinger.x;
+ self.touches[idx].y = tfinger.y;
+ self.touches[idx].pressure = tfinger.pressure;
+ self.touches[idx].pushTrail(tfinger.x, tfinger.y, tfinger.pressure);
+ },
+ }
+}
+
+fn touchColor(id: u64) OverlayColor {
+ const palette = [_]OverlayColor{
+ .{ .r = 0.10, .g = 0.72, .b = 1.00, .a = 0.88 },
+ .{ .r = 1.00, .g = 0.32, .b = 0.50, .a = 0.88 },
+ .{ .r = 0.35, .g = 0.90, .b = 0.38, .a = 0.88 },
+ .{ .r = 1.00, .g = 0.78, .b = 0.18, .a = 0.88 },
+ .{ .r = 0.78, .g = 0.52, .b = 1.00, .a = 0.88 },
+ .{ .r = 0.10, .g = 0.88, .b = 0.75, .a = 0.88 },
+ .{ .r = 1.00, .g = 0.48, .b = 0.18, .a = 0.88 },
+ .{ .r = 0.78, .g = 0.90, .b = 1.00, .a = 0.88 },
+ };
+ const n: u64 = palette.len;
+ return palette[@intCast(id % n)];
+}
+
+fn buildTouchOverlay(self: *SdlBackend, sw: u32, sh: u32) u32 {
+ if (self.overlay_vertices.len == 0 or sw == 0 or sh == 0) return 0;
+
+ const win_w: f32 = @floatFromInt(sw);
+ const win_h: f32 = @floatFromInt(sh);
+ const ui_scale: f32 = if (is_emscripten) web_render_scale else 1.0;
+ var builder = OverlayBuilder{
+ .vertices = self.overlay_vertices,
+ .win_w = win_w,
+ .win_h = win_h,
+ };
+
+ for (self.touches) |tp| {
+ if (!tp.active) continue;
+ const cx = clampF32(tp.x, 0.0, 1.0) * win_w;
+ const cy = clampF32(tp.y, 0.0, 1.0) * win_h;
+ const radius = (12.0 + clampF32(tp.pressure, 0.0, 1.0) * 4.0) * 4.0 * ui_scale;
+ const color = touchColor(tp.id);
+
+ if (tp.trail_len > 1) {
+ const first = if (tp.trail_len == max_touch_trail_points) tp.trail_next else 0;
+ const trail_count = tp.trail_len - 1;
+ var trail_i: usize = 0;
+ while (trail_i < trail_count) : (trail_i += 1) {
+ const sample = tp.trail[(first + trail_i) % max_touch_trail_points];
+ const fade = @as(f32, @floatFromInt(trail_i + 1)) / @as(f32, @floatFromInt(tp.trail_len));
+ var trail_color = color;
+ trail_color.a *= 0.42 * fade;
+ const tx = clampF32(sample.x, 0.0, 1.0) * win_w;
+ const ty = clampF32(sample.y, 0.0, 1.0) * win_h;
+ const trail_radius = (12.0 + clampF32(sample.pressure, 0.0, 1.0) * 4.0) * 2.8 * ui_scale;
+ builder.addCircle(tx, ty, trail_radius, trail_color);
+ }
+ }
+
+ builder.addCircle(cx, cy, radius + 4.0 * ui_scale, .{ .r = 0.0, .g = 0.0, .b = 0.0, .a = 0.62 });
+ builder.addCircle(cx, cy, radius, color);
+ builder.addCircle(cx, cy, 2.4 * ui_scale, .{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 0.92 });
+ builder.addIdTag(tp.id, cx, cy, ui_scale, color);
+ }
+
+ return @intCast(builder.len);
+}
+
+fn uploadOverlayGpu(self: *SdlBackend, cmd: *c.SDL_GPUCommandBuffer, vertex_count: u32) void {
+ if (vertex_count == 0 or is_emscripten) return;
+ const byte_len: u32 = vertex_count * @sizeOf(OverlayVertex);
+ const ptr = c.SDL_MapGPUTransferBuffer(self.device, self.overlay_vxfer, false) orelse return;
+ const dst: [*]u8 = @ptrCast(ptr);
+ const src: [*]const u8 = @ptrCast(self.overlay_vertices.ptr);
+ @memcpy(dst[0..byte_len], src[0..byte_len]);
+ c.SDL_UnmapGPUTransferBuffer(self.device, self.overlay_vxfer);
+
+ const copy = c.SDL_BeginGPUCopyPass(cmd);
+ const src_loc = c.SDL_GPUTransferBufferLocation{ .transfer_buffer = self.overlay_vxfer, .offset = 0 };
+ const dst_region = c.SDL_GPUBufferRegion{ .buffer = self.overlay_vbuf, .offset = 0, .size = byte_len };
+ c.SDL_UploadToGPUBuffer(copy, &src_loc, &dst_region, false);
+ c.SDL_EndGPUCopyPass(copy);
+}
+
+fn drawOverlayGpu(self: *SdlBackend, rp: ?*c.SDL_GPURenderPass, vertex_count: u32) void {
+ if (vertex_count == 0 or is_emscripten) return;
+ const pass = rp orelse return;
+ c.SDL_BindGPUGraphicsPipeline(pass, self.overlay_pipeline);
+ const binding = c.SDL_GPUBufferBinding{ .buffer = self.overlay_vbuf, .offset = 0 };
+ c.SDL_BindGPUVertexBuffers(pass, 0, &binding, 1);
+ c.SDL_DrawGPUPrimitives(pass, vertex_count, 1, 0, 0);
+}
+
+fn drawOverlayGl(self: *SdlBackend, vertex_count: u32) void {
+ if (!is_emscripten or vertex_count == 0) return;
+ c.glUseProgram(self.gl_overlay_program);
+ c.glBindBuffer(c.GL_ARRAY_BUFFER, self.gl_overlay_vbo);
+ c.glBufferData(c.GL_ARRAY_BUFFER, @intCast(vertex_count * @sizeOf(OverlayVertex)), self.overlay_vertices.ptr, c.GL_STREAM_DRAW);
+
+ const stride: c_int = @intCast(@sizeOf(OverlayVertex));
+ c.glEnableVertexAttribArray(0);
+ c.glVertexAttribPointer(0, 2, c.GL_FLOAT, c.GL_FALSE, stride, @ptrFromInt(@offsetOf(OverlayVertex, "x")));
+ c.glVertexAttribDivisor(0, 0);
+ c.glEnableVertexAttribArray(1);
+ c.glVertexAttribPointer(1, 4, c.GL_FLOAT, c.GL_FALSE, stride, @ptrFromInt(@offsetOf(OverlayVertex, "r")));
+ c.glVertexAttribDivisor(1, 0);
+ c.glDisableVertexAttribArray(2);
+ c.glDisableVertexAttribArray(3);
+
+ c.glEnable(c.GL_BLEND);
+ c.glBlendFuncSeparate(c.GL_SRC_ALPHA, c.GL_ONE_MINUS_SRC_ALPHA, c.GL_ONE, c.GL_ONE_MINUS_SRC_ALPHA);
+ c.glDrawArrays(c.GL_TRIANGLES, 0, @intCast(vertex_count));
+ c.glDisable(c.GL_BLEND);
+}
+
+const OverlayBuilder = struct {
+ vertices: []OverlayVertex,
+ len: usize = 0,
+ win_w: f32,
+ win_h: f32,
+
+ fn addVertex(self: *OverlayBuilder, px: f32, py: f32, color: OverlayColor) void {
+ if (self.len >= self.vertices.len) return;
+ self.vertices[self.len] = .{
+ .x = (px / self.win_w) * 2.0 - 1.0,
+ .y = 1.0 - (py / self.win_h) * 2.0,
+ .r = color.r,
+ .g = color.g,
+ .b = color.b,
+ .a = color.a,
+ };
+ self.len += 1;
+ }
+
+ fn addTri(self: *OverlayBuilder, x0: f32, y0: f32, x1: f32, y1: f32, x2: f32, y2: f32, color: OverlayColor) void {
+ if (self.len + 3 > self.vertices.len) return;
+ self.addVertex(x0, y0, color);
+ self.addVertex(x1, y1, color);
+ self.addVertex(x2, y2, color);
+ }
+
+ fn addRect(self: *OverlayBuilder, x0_in: f32, y0_in: f32, x1_in: f32, y1_in: f32, color: OverlayColor) void {
+ if (self.len + 6 > self.vertices.len) return;
+ const x0 = clampF32(@min(x0_in, x1_in), 0.0, self.win_w);
+ const x1 = clampF32(@max(x0_in, x1_in), 0.0, self.win_w);
+ const y0 = clampF32(@min(y0_in, y1_in), 0.0, self.win_h);
+ const y1 = clampF32(@max(y0_in, y1_in), 0.0, self.win_h);
+ if (x1 <= x0 or y1 <= y0) return;
+ self.addTri(x0, y0, x1, y0, x1, y1, color);
+ self.addTri(x0, y0, x1, y1, x0, y1, color);
+ }
+
+ fn addCircle(self: *OverlayBuilder, cx: f32, cy: f32, radius: f32, color: OverlayColor) void {
+ var i: usize = 0;
+ while (i + 1 < circle_points.len) : (i += 1) {
+ const p0 = circle_points[i];
+ const p1 = circle_points[i + 1];
+ self.addTri(
+ cx,
+ cy,
+ cx + p0[0] * radius,
+ cy + p0[1] * radius,
+ cx + p1[0] * radius,
+ cy + p1[1] * radius,
+ color,
+ );
+ }
+ }
+
+ fn addIdTag(self: *OverlayBuilder, id: u64, cx: f32, cy: f32, ui_scale: f32, color: OverlayColor) void {
+ const label = touchIdLabel(id);
+ const glyph_scale = 2.0 * ui_scale;
+ const char_step = 4.0 * glyph_scale;
+ const pad = 4.0 * ui_scale;
+ const stripe_w = 3.0 * ui_scale;
+ const label_w = pad * 2.0 + stripe_w + 3.0 * ui_scale + @as(f32, @floatFromInt(label.len)) * char_step - glyph_scale;
+ const label_h = pad * 2.0 + 5.0 * glyph_scale;
+
+ var x = cx + 20.0 * ui_scale;
+ if (x + label_w > self.win_w - 2.0 * ui_scale) x = cx - label_w - 20.0 * ui_scale;
+ x = clampF32(x, 2.0 * ui_scale, @max(2.0 * ui_scale, self.win_w - label_w - 2.0 * ui_scale));
+
+ var y = cy - label_h - 10.0 * ui_scale;
+ if (y < 2.0 * ui_scale) y = cy + 20.0 * ui_scale;
+ y = clampF32(y, 2.0 * ui_scale, @max(2.0 * ui_scale, self.win_h - label_h - 2.0 * ui_scale));
+
+ self.addRect(x, y, x + label_w, y + label_h, .{ .r = 0.0, .g = 0.0, .b = 0.0, .a = 0.72 });
+ self.addRect(x + pad, y + pad, x + pad + stripe_w, y + label_h - pad, color);
+ self.addMiniText(label[0..], x + pad + stripe_w + 3.0 * ui_scale, y + pad, glyph_scale, .{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 0.95 });
+ }
+
+ fn addMiniText(self: *OverlayBuilder, text: []const u8, x: f32, y: f32, scale: f32, color: OverlayColor) void {
+ var cx = x;
+ for (text) |ch| {
+ self.addMiniGlyph(ch, cx, y, scale, color);
+ cx += 4.0 * scale;
+ }
+ }
+
+ fn addMiniGlyph(self: *OverlayBuilder, ch: u8, x: f32, y: f32, scale: f32, color: OverlayColor) void {
+ const rows = miniGlyph(ch);
+ const masks = [_]u8{ 0b100, 0b010, 0b001 };
+ var row: usize = 0;
+ while (row < rows.len) : (row += 1) {
+ var col: usize = 0;
+ while (col < masks.len) : (col += 1) {
+ if ((rows[row] & masks[col]) == 0) continue;
+ const x0 = x + @as(f32, @floatFromInt(col)) * scale;
+ const y0 = y + @as(f32, @floatFromInt(row)) * scale;
+ self.addRect(x0, y0, x0 + scale, y0 + scale, color);
+ }
+ }
+ }
+};
+
+const circle_points = [_][2]f32{
+ .{ 1.0000000, 0.0000000 },
+ .{ 0.9238795, 0.3826834 },
+ .{ 0.7071068, 0.7071068 },
+ .{ 0.3826834, 0.9238795 },
+ .{ 0.0000000, 1.0000000 },
+ .{ -0.3826834, 0.9238795 },
+ .{ -0.7071068, 0.7071068 },
+ .{ -0.9238795, 0.3826834 },
+ .{ -1.0000000, 0.0000000 },
+ .{ -0.9238795, -0.3826834 },
+ .{ -0.7071068, -0.7071068 },
+ .{ -0.3826834, -0.9238795 },
+ .{ 0.0000000, -1.0000000 },
+ .{ 0.3826834, -0.9238795 },
+ .{ 0.7071068, -0.7071068 },
+ .{ 0.9238795, -0.3826834 },
+ .{ 1.0000000, 0.0000000 },
+};
+
+fn touchIdLabel(id: u64) [9]u8 {
+ var out: [9]u8 = undefined;
+ out[0] = '#';
+ const low: u32 = @truncate(id);
+ var i: usize = 0;
+ while (i < 8) : (i += 1) {
+ const shift: u5 = @intCast((7 - i) * 4);
+ out[i + 1] = hexDigit((low >> shift) & 0xf);
+ }
+ return out;
+}
+
+fn hexDigit(n: u32) u8 {
+ return if (n < 10) @intCast('0' + n) else @intCast('A' + (n - 10));
+}
+
+fn miniGlyph(ch: u8) [5]u8 {
+ return switch (ch) {
+ '#' => .{ 0b101, 0b111, 0b101, 0b111, 0b101 },
+ '0' => .{ 0b111, 0b101, 0b101, 0b101, 0b111 },
+ '1' => .{ 0b010, 0b110, 0b010, 0b010, 0b111 },
+ '2' => .{ 0b111, 0b001, 0b111, 0b100, 0b111 },
+ '3' => .{ 0b111, 0b001, 0b111, 0b001, 0b111 },
+ '4' => .{ 0b101, 0b101, 0b111, 0b001, 0b001 },
+ '5' => .{ 0b111, 0b100, 0b111, 0b001, 0b111 },
+ '6' => .{ 0b111, 0b100, 0b111, 0b101, 0b111 },
+ '7' => .{ 0b111, 0b001, 0b010, 0b010, 0b010 },
+ '8' => .{ 0b111, 0b101, 0b111, 0b101, 0b111 },
+ '9' => .{ 0b111, 0b101, 0b111, 0b001, 0b111 },
+ 'A' => .{ 0b111, 0b101, 0b111, 0b101, 0b101 },
+ 'B' => .{ 0b110, 0b101, 0b110, 0b101, 0b110 },
+ 'C' => .{ 0b111, 0b100, 0b100, 0b100, 0b111 },
+ 'D' => .{ 0b110, 0b101, 0b101, 0b101, 0b110 },
+ 'E' => .{ 0b111, 0b100, 0b111, 0b100, 0b111 },
+ 'F' => .{ 0b111, 0b100, 0b111, 0b100, 0b100 },
+ else => .{ 0, 0, 0, 0, 0 },
+ };
+}
+
// PARDES_TEST_GRID debug mode: dump the retained cell grid as text to stdout
// (fd 1). One frame per render, delimited by ---FRAME---/---ENDFRAME--- markers.
fn dumpGrid(self: *SdlBackend) !void {
@@ -1595,6 +1997,52 @@ fn makePipeline(device: *c.SDL_GPUDevice, color_format: c.SDL_GPUTextureFormat)
return c.SDL_CreateGPUGraphicsPipeline(device, &info) orelse error.GpuCreate;
}
+fn makeOverlayPipeline(device: *c.SDL_GPUDevice, color_format: c.SDL_GPUTextureFormat) !*c.SDL_GPUGraphicsPipeline {
+ const vs = try makeShader(device, overlay_vert_spv_bytes, c.SDL_GPU_SHADERSTAGE_VERTEX, 0);
+ defer c.SDL_ReleaseGPUShader(device, vs);
+ const fs = try makeShader(device, overlay_frag_spv_bytes, c.SDL_GPU_SHADERSTAGE_FRAGMENT, 0);
+ defer c.SDL_ReleaseGPUShader(device, fs);
+
+ var vbuf_desc = c.SDL_GPUVertexBufferDescription{
+ .slot = 0,
+ .pitch = @sizeOf(OverlayVertex),
+ .input_rate = c.SDL_GPU_VERTEXINPUTRATE_VERTEX,
+ .instance_step_rate = 0,
+ };
+ var attrs = [_]c.SDL_GPUVertexAttribute{
+ .{ .location = 0, .buffer_slot = 0, .format = c.SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2, .offset = @offsetOf(OverlayVertex, "x") },
+ .{ .location = 1, .buffer_slot = 0, .format = c.SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4, .offset = @offsetOf(OverlayVertex, "r") },
+ };
+ var blend = std.mem.zeroes(c.SDL_GPUColorTargetBlendState);
+ blend.src_color_blendfactor = c.SDL_GPU_BLENDFACTOR_SRC_ALPHA;
+ blend.dst_color_blendfactor = c.SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
+ blend.color_blend_op = c.SDL_GPU_BLENDOP_ADD;
+ blend.src_alpha_blendfactor = c.SDL_GPU_BLENDFACTOR_ONE;
+ blend.dst_alpha_blendfactor = c.SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
+ blend.alpha_blend_op = c.SDL_GPU_BLENDOP_ADD;
+ blend.enable_blend = true;
+ var col_desc = c.SDL_GPUColorTargetDescription{ .format = color_format, .blend_state = blend };
+
+ 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.vertex_input_state = .{
+ .vertex_buffer_descriptions = &vbuf_desc,
+ .num_vertex_buffers = 1,
+ .vertex_attributes = &attrs,
+ .num_vertex_attributes = attrs.len,
+ };
+ 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,
@@ -1612,9 +2060,17 @@ fn makeShader(
}
fn makeGlProgram() !c_uint {
- const vs = try makeGlShader(c.GL_VERTEX_SHADER, vert_glsl_es);
+ return makeGlProgramFrom(vert_glsl_es, frag_glsl_es);
+}
+
+fn makeOverlayGlProgram() !c_uint {
+ return makeGlProgramFrom(overlay_vert_glsl_es, overlay_frag_glsl_es);
+}
+
+fn makeGlProgramFrom(vertex_source: []const u8, fragment_source: []const u8) !c_uint {
+ const vs = try makeGlShader(c.GL_VERTEX_SHADER, vertex_source);
defer c.glDeleteShader(vs);
- const fs = try makeGlShader(c.GL_FRAGMENT_SHADER, frag_glsl_es);
+ const fs = try makeGlShader(c.GL_FRAGMENT_SHADER, fragment_source);
defer c.glDeleteShader(fs);
const program = c.glCreateProgram();
@@ -1925,6 +2381,13 @@ fn SdlLoop(comptime EventType: type) type {
.type = .press,
} };
},
+ c.SDL_EVENT_FINGER_DOWN, c.SDL_EVENT_FINGER_MOTION, c.SDL_EVENT_FINGER_UP, c.SDL_EVENT_FINGER_CANCELED => {
+ self.vx.updateTouch(sev.tfinger);
+ return .tick;
+ },
+ c.SDL_EVENT_PINCH_BEGIN, c.SDL_EVENT_PINCH_UPDATE, c.SDL_EVENT_PINCH_END => {
+ return .{ .pinch = sev.pinch.scale };
+ },
else => return null,
}
}