summaryrefslogtreecommitdiff
path: root/ui_sdl.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-04 00:57:06 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:07 -0300
commite36e946f9a56c4ab1d10cef3aace20fe7d274dfa (patch)
treebaccd0be04b9b901bb74b4f3647bdb531c8dd450 /ui_sdl.zig
parentf3f0e27372d8cf111269dde674d4581cf4cb0c41 (diff)
downloadpardes-e36e946f9a56c4ab1d10cef3aace20fe7d274dfa.tar.gz
pardes-e36e946f9a56c4ab1d10cef3aace20fe7d274dfa.zip
touch debug overlay: multi-touch circles with trails, finger event handling, 4x larger, fade trails, SDL GPU + WebGL overlay shaders, touch-action:none in HTML, --debug-overlay flag, pinch event dispatch for all SDL backends
Diffstat (limited to 'ui_sdl.zig')
-rw-r--r--ui_sdl.zig467
1 files changed, 465 insertions, 2 deletions
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,
}
}