summaryrefslogtreecommitdiff
path: root/ui_sdl.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-05 20:21:50 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:07 -0300
commitde4def3548a6729b0dfd2120495a61beef8c8c2c (patch)
tree12144f0f64dc96bb4741459c1f9db57f44349330 /ui_sdl.zig
parent7988d9bc6e31210ff13994f18d5622dd57ba9341 (diff)
downloadpardes-de4def3548a6729b0dfd2120495a61beef8c8c2c.tar.gz
pardes-de4def3548a6729b0dfd2120495a61beef8c8c2c.zip
pardes v2: the rewrite, complete and organized. src/ (core + three shells), test/ (snapshot parity harness + 18 frozen goldens). One sans-IO core, vaxis tty + SDL3 GPU native + wasm web shells, 18/18 parity with the purged prototype, 7.6k lines vs 12.1k. Fix: gui shell pre-sized the core at init so the greet-releasing resize never fired (blank panes until first interaction); live sessions now init at defaults and get the real grid as a resize event (the shell contract, documented on Options).
Diffstat (limited to 'ui_sdl.zig')
-rw-r--r--ui_sdl.zig2916
1 files changed, 0 insertions, 2916 deletions
diff --git a/ui_sdl.zig b/ui_sdl.zig
deleted file mode 100644
index 2e967fb8..00000000
--- a/ui_sdl.zig
+++ /dev/null
@@ -1,2916 +0,0 @@
-// The SDL3 GPU UI backend (`-Dui=sdl`). Same surface as `ui_vaxis.zig` — the
-// app (main.zig / image.zig) imports `"ui"` and talks to `Vaxis`/`Tty`/`Loop`/
-// `init` plus the shared cell types — but here those are backed by an SDL
-// window + SDL3 GPU renderer instead of a real terminal.
-//
-// The retained model is identical to libvaxis: `main` builds a 2D grid of
-// `Cell`s each frame (via `vaxis.Window` over a `vaxis.Screen` — both pure,
-// I/O-free libvaxis types we reuse verbatim) and hands it to `render()`. Instead
-// of diffing it into escape sequences, `render()` rasterizes the grid with SDL3
-// GPU: one textured-quad draw per cell, glyphs pulled from an stb_truetype
-// monospace atlas. Input flows the other way through `SdlLoop`, which polls SDL
-// events and converts them to the same `Key`/`Mouse`/`Winsize` the app already
-// handles, posting them on a thread-safe queue (so the per-terminal pty readers
-// work unchanged). Kitty image panes are unsupported (caps.kitty_graphics is
-// false), so image.zig falls back to its PETSCII cell-art path — which renders
-// fine here, since it just writes cells.
-//
-// This is the quick-and-dirty first cut: ASCII + box-drawing + Nerd Font glyphs via
-// a fixed 1024² R8 atlas (dynamic, grows as new codepoints appear), reverse /
-// invisible / dim honored, bold/italic/underline/strikethrough ignored. Terminal
-// appearance is preserved: a monospace grid of styled cells on a dark page.
-const std = @import("std");
-const builtin = @import("builtin");
-const vaxis = @import("vaxis");
-const ui_sdl_config = @import("ui_sdl_config");
-const posix = std.posix;
-const linux = std.os.linux;
-const os_tag = builtin.os.tag;
-const is_emscripten = builtin.os.tag == .emscripten;
-
-pub const c = @cImport({
- @cDefine("SDL_DISABLE_OLD_NAMES", "1");
- @cInclude("SDL3/SDL.h");
- if (is_emscripten) @cInclude("GLES3/gl3.h");
- @cInclude("font.h");
-});
-
-extern "c" fn emscripten_get_element_css_size(target: [*:0]const u8, width: *f64, height: *f64) c_int;
-const EMSCRIPTEN_RESULT_SUCCESS: c_int = 0;
-
-// ---- shared (backend-agnostic) types: re-exported straight from libvaxis ----
-pub const Key = vaxis.Key;
-pub const Mouse = vaxis.Mouse;
-pub const Winsize = vaxis.Winsize;
-pub const Color = vaxis.Color;
-pub const Style = vaxis.Style;
-pub const Cell = vaxis.Cell;
-pub const Segment = vaxis.Segment;
-pub const Window = vaxis.Window;
-pub const Screen = vaxis.Screen;
-pub const Image = vaxis.Image;
-pub const Event = vaxis.Event;
-pub const Capabilities = vaxis.Vaxis.Capabilities;
-pub const CursorShape = vaxis.Cell.CursorShape;
-
-// `Options` mirrors the subset of vaxis.Vaxis.Options the app constructs. Only
-// system_clipboard_allocator is used (for OSC 52 paste in the terminal backend);
-// here it's accepted and unused — SDL reads the clipboard synchronously.
-pub const Options = struct {
- system_clipboard_allocator: ?std.mem.Allocator = null,
-};
-
-// The backend struct is named `Vaxis` so `image.zig`'s `*vaxis.Vaxis` resolves.
-pub const Vaxis = SdlBackend;
-pub const Tty = SdlTty;
-pub const supports_terminal_prepaint = false;
-pub fn Loop(comptime EventType: type) type {
- return SdlLoop(EventType);
-}
-
-pub fn adoptAltScreen(vx: *Vaxis) void {
- _ = vx;
-}
-
-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;
-const web_render_scale_f64: f64 = @floatCast(web_render_scale);
-
-// A standard 16-color ANSI palette (xterm), used to resolve vaxis.Color.index
-// cells — the dark theme passes palette indices through (palColor), and there's
-// no real terminal here to resolve them.
-const ansi = [16][3]u8{
- .{ 0, 0, 0 }, .{ 205, 0, 0 }, .{ 0, 205, 0 }, .{ 205, 205, 0 },
- .{ 0, 0, 238 }, .{ 205, 0, 205 }, .{ 0, 205, 205 }, .{ 229, 229, 229 },
- .{ 127, 127, 127 }, .{ 255, 0, 0 }, .{ 0, 255, 0 }, .{ 255, 255, 0 },
- .{ 92, 92, 255 }, .{ 255, 0, 255 }, .{ 0, 255, 255 }, .{ 255, 255, 255 },
-};
-
-// =====================================================================
-// Backend
-// =====================================================================
-
-const SdlBackend = @This();
-
-io: std.Io,
-alloc: std.mem.Allocator,
-
-sdl_window: *c.SDL_Window,
-device: *c.SDL_GPUDevice,
-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 = &.{},
-cell_cache_valid: bool = false,
-last_render_w: u32 = 0,
-last_render_h: u32 = 0,
-
-// Emscripten/browser path: SDL window + WebGL2 shader renderer. Handles are GL
-// 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,
-touch_scroll: TouchScrollState = .{},
-touch_middle_clicks: u32 = 0,
-touch_middle_flash_frames: u8 = 0,
-
-// font + cell metrics
-font: ?*c.UIFont,
-scale: f32,
-cell_w: u32,
-cell_h: u32,
-ascent: i32,
-
-// retained cell grid (the same type vaxis uses; main writes here via window())
-screen: vaxis.Screen,
-caps: vaxis.Vaxis.Capabilities = .{}, // kitty_graphics stays false → PETSCII images
-test_mode: bool = false, // PARDES_TEST=1: stdin→events for e2e
-grid_test_mode: bool = false, // PARDES_TEST_GRID=1: old no-SDL text-grid dump path
-capture_images: bool = false, // PARDES_TEST=1: capture real rendered framebuffer images
-capture_dir: ?[]const u8 = null,
-capture_tex: ?*c.SDL_GPUTexture = null,
-capture_tex_w: u32 = 0,
-capture_tex_h: u32 = 0,
-capture_xfer: ?*c.SDL_GPUTransferBuffer = null,
-capture_xfer_size: u32 = 0,
-capture_frame: u64 = 0,
-
-trace_enabled: bool = false,
-trace_fd: posix.fd_t = -1,
-trace_every: u32 = 1,
-trace_frame: u64 = 0,
-
-// glyph atlas: a 1024² R8 staging bitmap + a map codepoint → texel slot.
-atlas_stage: []u8,
-glyphs: std.AutoHashMap(u32, Slot),
-pen_x: u32 = 0,
-pen_y: u32 = 0,
-atlas_dirty: bool = true,
-space_slot: Slot = .{ .u = 0, .v = 0 },
-
-// default page colors (the dark-theme "terminal native" the app expects when a
-// cell's color is .default).
-bg_default: [3]u8 = .{ 18, 18, 18 },
-fg_default: [3]u8 = .{ 204, 204, 204 },
-
-// last grid size we reported to the app (so SDL only posts winsize on change)
-reported_cols: u16 = 0,
-reported_rows: u16 = 0,
-
-// ---- constants + helper types (must come after all fields in Zig 0.16) ----
-pub const atlas_w: u32 = if (is_emscripten) 2048 else 1024;
-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 touch_middle_flash_max_frames: u8 = 14;
-pub const touch_middle_flash_vertices: usize = 1400;
-pub const touch_scroll_tick: f32 = 0.02;
-pub const max_overlay_vertices: usize = touch_middle_flash_vertices + 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.
-const CellInstance = extern struct {
- x0: f32,
- y0: f32,
- x1: f32,
- y1: f32,
- u0: f32,
- v0: f32,
- u1: f32,
- v1: f32,
- fr: f32,
- fg: f32,
- fb: f32,
- br: f32,
- bg: f32,
- 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 TouchNormPoint = struct {
- x: f32 = 0,
- y: f32 = 0,
-};
-
-const TouchScrollState = struct {
- active: bool = false,
- ids: [2]u64 = .{ 0, 0 },
- accum_y: f32 = 0,
- scrolled: bool = false,
- last_center: TouchNormPoint = .{},
-};
-
-const TouchScrollResult = struct {
- center: TouchNormPoint,
- ticks: i32,
- delta_y: f32,
-};
-
-const OverlayColor = struct {
- r: f32,
- g: f32,
- b: f32,
- a: f32,
-};
-
-const CellLayout = struct {
- w: f32,
- h: f32,
- x_off: f32,
- y_off: f32,
-};
-
-const MouseCell = struct {
- col: i16,
- row: i16,
-};
-
-fn webGlFilter() c_int {
- return switch (ui_sdl_config.web_filter) {
- .linear => c.GL_LINEAR,
- .nearest => c.GL_NEAREST,
- };
-}
-
-fn envU16(env_map: *std.process.Environ.Map, name: []const u8) ?u16 {
- const raw = env_map.get(name) orelse return null;
- return std.fmt.parseInt(u16, raw, 10) catch null;
-}
-
-fn envU32(env_map: *std.process.Environ.Map, name: []const u8) ?u32 {
- const raw = env_map.get(name) orelse return null;
- return std.fmt.parseInt(u32, raw, 10) catch null;
-}
-
-fn envFlag(env_map: *std.process.Environ.Map, name: []const u8) bool {
- const raw = env_map.get(name) orelse return false;
- return !(raw.len == 0 or std.mem.eql(u8, raw, "0") or std.ascii.eqlIgnoreCase(raw, "false"));
-}
-
-fn nowNs() i128 {
- if (is_emscripten) return @intCast(c.SDL_GetTicksNS());
- switch (os_tag) {
- .linux => {
- var ts: linux.timespec = undefined;
- _ = linux.clock_gettime(linux.CLOCK.MONOTONIC, &ts);
- return @as(i128, ts.sec) * 1_000_000_000 + @as(i128, ts.nsec);
- },
- else => {
- var ts: posix.timespec = undefined;
- switch (posix.errno(posix.system.clock_gettime(posix.CLOCK.MONOTONIC, &ts))) {
- .SUCCESS => return @as(i128, ts.sec) * 1_000_000_000 + @as(i128, ts.nsec),
- else => return 0,
- }
- },
- }
-}
-
-fn openCreateTrunc(path: [*:0]const u8) ?posix.fd_t {
- switch (os_tag) {
- .linux => {
- const fd_rc = linux.open(path, .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, 0o644);
- const fd_s: isize = @bitCast(fd_rc);
- return if (fd_s < 0) null else @intCast(fd_s);
- },
- else => return posix.openatZ(posix.AT.FDCWD, path, .{
- .ACCMODE = .WRONLY,
- .CREAT = true,
- .TRUNC = true,
- }, 0o644) catch null,
- }
-}
-
-fn closeFdBestEffort(fd: posix.fd_t) void {
- switch (os_tag) {
- .linux => _ = linux.close(fd),
- else => switch (posix.errno(posix.system.close(fd))) {
- .SUCCESS, .INTR => {},
- else => {},
- },
- }
-}
-
-fn writeAllFd(fd: posix.fd_t, bytes: []const u8) !void {
- var off: usize = 0;
- while (off < bytes.len) {
- switch (os_tag) {
- .linux => {
- const rc = linux.write(fd, bytes[off..].ptr, bytes.len - off);
- const n: isize = @bitCast(rc);
- if (n < 0) {
- if (n == -@as(isize, @intFromEnum(linux.E.INTR))) continue;
- return error.WriteFailed;
- }
- if (n == 0) return error.WriteFailed;
- off += @intCast(n);
- },
- else => {
- const rc = posix.system.write(fd, bytes[off..].ptr, bytes.len - off);
- switch (posix.errno(rc)) {
- .SUCCESS => {
- const n: usize = @intCast(rc);
- if (n == 0) return error.WriteFailed;
- off += n;
- },
- .INTR => continue,
- else => return error.WriteFailed,
- }
- },
- }
- }
-}
-
-fn writeAllFdIgnore(fd: posix.fd_t, bytes: []const u8) void {
- writeAllFd(fd, bytes) catch {};
-}
-
-fn renameZ(old_path: [*:0]const u8, new_path: [*:0]const u8) !void {
- switch (os_tag) {
- .linux => {
- const rr = linux.rename(old_path, new_path);
- const rs: isize = @bitCast(rr);
- if (rs < 0) return error.RenameFailed;
- },
- else => switch (posix.errno(posix.system.rename(old_path, new_path))) {
- .SUCCESS => {},
- else => return error.RenameFailed,
- },
- }
-}
-
-fn nsToUs(delta: i128) u64 {
- if (delta <= 0) return 0;
- return @intCast(@divTrunc(delta, 1000));
-}
-
-fn parsePresentMode(raw: []const u8) ?c.SDL_GPUPresentMode {
- if (std.ascii.eqlIgnoreCase(raw, "immediate")) return c.SDL_GPU_PRESENTMODE_IMMEDIATE;
- if (std.ascii.eqlIgnoreCase(raw, "mailbox")) return c.SDL_GPU_PRESENTMODE_MAILBOX;
- if (std.ascii.eqlIgnoreCase(raw, "vsync")) return c.SDL_GPU_PRESENTMODE_VSYNC;
- return null;
-}
-
-fn presentModeName(mode: c.SDL_GPUPresentMode) []const u8 {
- return switch (mode) {
- c.SDL_GPU_PRESENTMODE_IMMEDIATE => "immediate",
- c.SDL_GPU_PRESENTMODE_MAILBOX => "mailbox",
- c.SDL_GPU_PRESENTMODE_VSYNC => "vsync",
- else => "unknown",
- };
-}
-
-fn choosePresentMode(env_map: *std.process.Environ.Map, device: *c.SDL_GPUDevice, sdl_win: *c.SDL_Window) c.SDL_GPUPresentMode {
- if (env_map.get("PARDES_SDL_PRESENT")) |raw| {
- if (parsePresentMode(raw)) |mode| {
- if (c.SDL_WindowSupportsGPUPresentMode(device, sdl_win, mode)) return mode;
- }
- }
- const preferred = [_]c.SDL_GPUPresentMode{
- c.SDL_GPU_PRESENTMODE_IMMEDIATE,
- c.SDL_GPU_PRESENTMODE_MAILBOX,
- c.SDL_GPU_PRESENTMODE_VSYNC,
- };
- for (preferred) |mode| {
- if (c.SDL_WindowSupportsGPUPresentMode(device, sdl_win, mode)) return mode;
- }
- return c.SDL_GPU_PRESENTMODE_VSYNC;
-}
-
-pub fn init(
- io: std.Io,
- alloc: std.mem.Allocator,
- env_map: *std.process.Environ.Map,
- opts: Options,
-) !SdlBackend {
- _ = opts;
-
- // ---- test modes -------------------------------------------------------
- // PARDES_TEST=1 keeps the real SDL/GPU/window renderer active, but drives
- // input from stdin (raw terminal escape sequences parsed by vaxis.Parser,
- // plus private synthetic SDL event OSCs) and writes rendered-frame captures
- // into PARDES_TEST_CAPTURE_DIR.
- //
- // PARDES_TEST_GRID=1 is retained as a debug escape hatch: no SDL/GPU/window,
- // stdin→events, retained cell grid→stdout. The real SDL e2e suite does not
- // use it because it bypasses the renderer.
- const test_mode = env_map.get("PARDES_TEST") != null;
- const grid_test_mode = env_map.get("PARDES_TEST_GRID") != null;
- if (grid_test_mode) return initTest(io, alloc);
- const capture_dir = env_map.get("PARDES_TEST_CAPTURE_DIR");
- if (test_mode and capture_dir == null) {
- log.err("PARDES_TEST requires PARDES_TEST_CAPTURE_DIR", .{});
- return error.SdlInit;
- }
- const trace_enabled = envFlag(env_map, "PARDES_SDL_TRACE");
- const trace_every = envU32(env_map, "PARDES_SDL_TRACE_EVERY") orelse 1;
- var trace_fd: posix.fd_t = -1;
- if (!is_emscripten and trace_enabled) {
- if (env_map.get("PARDES_SDL_TRACE_FILE")) |path| {
- trace_fd = openTraceFile(path);
- } else if (capture_dir) |dir| {
- var path_buf: [4096]u8 = undefined;
- const path = std.fmt.bufPrintZ(&path_buf, "{s}/perf.log", .{dir}) catch null;
- trace_fd = if (path) |p| openTraceFile(p) else 2;
- } else {
- trace_fd = 2;
- }
- }
-
- if (!c.SDL_Init(c.SDL_INIT_VIDEO)) {
- log.err("SDL_Init: {s}", .{c.SDL_GetError()});
- return error.SdlInit;
- }
-
- const win_w: c_int = 1120;
- const win_h: c_int = 720;
- var win_flags: c.SDL_WindowFlags = c.SDL_WINDOW_RESIZABLE;
- if (!test_mode) win_flags |= c.SDL_WINDOW_HIGH_PIXEL_DENSITY;
- if (is_emscripten) win_flags |= c.SDL_WINDOW_OPENGL;
- const sdl_win = c.SDL_CreateWindow(
- "pardes",
- win_w,
- win_h,
- win_flags,
- ) orelse {
- log.err("SDL_CreateWindow: {s}", .{c.SDL_GetError()});
- return error.SdlInit;
- };
- if (is_emscripten) {
- return initGl(io, alloc, sdl_win, test_mode, capture_dir, trace_enabled, trace_fd, trace_every);
- }
-
- const device = c.SDL_CreateGPUDevice(
- c.SDL_GPU_SHADERFORMAT_SPIRV,
- true,
- null,
- ) orelse {
- log.err("SDL_CreateGPUDevice: {s}", .{c.SDL_GetError()});
- return error.SdlInit;
- };
- if (!c.SDL_ClaimWindowForGPUDevice(device, sdl_win)) {
- log.err("ClaimWindowForGPUDevice: {s}", .{c.SDL_GetError()});
- return error.SdlInit;
- }
- const present_mode = choosePresentMode(env_map, device, sdl_win);
- if (!test_mode) _ = c.SDL_SetGPUAllowedFramesInFlight(device, 1);
- _ = c.SDL_SetGPUSwapchainParameters(
- device,
- sdl_win,
- c.SDL_GPU_SWAPCHAINCOMPOSITION_SDR,
- present_mode,
- );
- const swapchain_format = c.SDL_GetGPUSwapchainTextureFormat(device, sdl_win);
-
- // ---- font + cell metrics ----
- const font = c.ui_font_new(font_ttf.ptr, @intCast(font_ttf.len)) orelse {
- log.err("ui_font_new failed", .{});
- return error.FontInit;
- };
- const px: f32 = 18.0;
- const scale = c.ui_font_scale_for_height(font, px);
- var cw: c_int = 10;
- var chh: c_int = 20;
- var asc: c_int = 16;
- c.ui_font_cell_metrics(font, scale, &cw, &chh, &asc);
- if (test_mode) {
- const cols = envU16(env_map, "PARDES_TEST_COLS") orelse 80;
- const rows = envU16(env_map, "PARDES_TEST_ROWS") orelse 24;
- _ = c.SDL_SetWindowSize(
- sdl_win,
- @intCast(@as(u32, cols) * @as(u32, @intCast(@max(cw, 1)))),
- @intCast(@as(u32, rows) * @as(u32, @intCast(@max(chh, 1)))),
- );
- _ = c.SDL_SyncWindow(sdl_win);
- }
-
- // ---- glyph atlas (R8) ----
- var tex_info = std.mem.zeroes(c.SDL_GPUTextureCreateInfo);
- tex_info.type = c.SDL_GPU_TEXTURETYPE_2D;
- tex_info.format = c.SDL_GPU_TEXTUREFORMAT_R8_UNORM;
- tex_info.usage = c.SDL_GPU_TEXTUREUSAGE_SAMPLER;
- tex_info.width = atlas_w;
- tex_info.height = atlas_h;
- tex_info.layer_count_or_depth = 1;
- tex_info.num_levels = 1;
- tex_info.sample_count = c.SDL_GPU_SAMPLECOUNT_1;
- const atlas_tex = c.SDL_CreateGPUTexture(device, &tex_info) orelse return error.GpuCreate;
-
- var samp_info = std.mem.zeroes(c.SDL_GPUSamplerCreateInfo);
- samp_info.min_filter = c.SDL_GPU_FILTER_LINEAR;
- samp_info.mag_filter = c.SDL_GPU_FILTER_LINEAR;
- samp_info.mipmap_mode = c.SDL_GPU_FILTER_LINEAR;
- samp_info.address_mode_u = c.SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
- samp_info.address_mode_v = c.SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
- samp_info.address_mode_w = c.SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
- const atlas_sampler = c.SDL_CreateGPUSampler(device, &samp_info) orelse return error.GpuCreate;
-
- var ax_info = c.SDL_GPUTransferBufferCreateInfo{
- .usage = c.SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD,
- .size = atlas_w * atlas_h,
- .props = 0,
- };
- const atlas_xfer = c.SDL_CreateGPUTransferBuffer(device, &ax_info) orelse return error.GpuCreate;
-
- // ---- 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,
- .alloc = alloc,
- .sdl_window = sdl_win,
- .device = device,
- .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)),
- .cell_h = @intCast(@max(chh, 1)),
- .ascent = asc,
- .screen = .{},
- .atlas_stage = atlas_stage,
- .glyphs = std.AutoHashMap(u32, Slot).init(alloc),
- .test_mode = test_mode,
- .capture_images = test_mode,
- .capture_dir = capture_dir,
- .trace_enabled = trace_enabled,
- .trace_fd = trace_fd,
- .trace_every = @max(trace_every, 1),
- };
- // reserve slot (0,0) for the space glyph (an empty cell), start the pen after it.
- _ = c.ui_font_raster(font, scale, ' ', atlas_stage.ptr, @intCast(atlas_w), @intCast(self.cell_w), @intCast(self.cell_h), asc);
- self.space_slot = .{ .u = 0, .v = 0 };
- self.pen_x = self.cell_w;
- self.tracef("sdl.init present={s} frames_in_flight={d} trace_every={d}", .{
- presentModeName(self.present_mode),
- if (test_mode) @as(u32, 2) else @as(u32, 1),
- self.trace_every,
- });
- return self;
-}
-
-fn initGl(
- io: std.Io,
- alloc: std.mem.Allocator,
- sdl_win: *c.SDL_Window,
- test_mode: bool,
- capture_dir: ?[]const u8,
- trace_enabled: bool,
- trace_fd: posix.fd_t,
- trace_every: u32,
-) !SdlBackend {
- _ = c.SDL_GL_SetAttribute(c.SDL_GL_CONTEXT_PROFILE_MASK, c.SDL_GL_CONTEXT_PROFILE_ES);
- _ = c.SDL_GL_SetAttribute(c.SDL_GL_CONTEXT_MAJOR_VERSION, 3);
- _ = c.SDL_GL_SetAttribute(c.SDL_GL_CONTEXT_MINOR_VERSION, 0);
- _ = c.SDL_GL_SetAttribute(c.SDL_GL_DOUBLEBUFFER, 1);
- const gl_context = c.SDL_GL_CreateContext(sdl_win) orelse {
- log.err("SDL_GL_CreateContext: {s}", .{c.SDL_GetError()});
- return error.SdlInit;
- };
- if (!c.SDL_GL_MakeCurrent(sdl_win, gl_context)) {
- log.err("SDL_GL_MakeCurrent: {s}", .{c.SDL_GetError()});
- return error.SdlInit;
- }
- _ = c.SDL_GL_SetSwapInterval(1);
-
- const font = c.ui_font_new(font_ttf.ptr, @intCast(font_ttf.len)) orelse {
- log.err("ui_font_new failed", .{});
- return error.FontInit;
- };
- const px: f32 = 18.0;
- const base_scale = c.ui_font_scale_for_height(font, px);
- var cw: c_int = 10;
- var chh: c_int = 20;
- var asc: c_int = 16;
- c.ui_font_cell_metrics(font, base_scale, &cw, &chh, &asc);
- const scale = c.ui_font_scale_for_height(font, px * web_render_scale);
- const cell_w: u32 = @intFromFloat(@ceil(@as(f32, @floatFromInt(@max(cw, 1))) * web_render_scale));
- const cell_h: u32 = @intFromFloat(@ceil(@as(f32, @floatFromInt(@max(chh, 1))) * web_render_scale));
- 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);
- const filter = webGlFilter();
- c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, filter);
- c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, filter);
- c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_CLAMP_TO_EDGE);
- c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_CLAMP_TO_EDGE);
- c.glPixelStorei(c.GL_UNPACK_ALIGNMENT, 1);
- c.glTexImage2D(c.GL_TEXTURE_2D, 0, c.GL_R8, atlas_w, atlas_h, 0, c.GL_RED, c.GL_UNSIGNED_BYTE, null);
-
- 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,
- .alloc = alloc,
- .sdl_window = sdl_win,
- .device = undefined,
- .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,
- .cell_h = cell_h,
- .ascent = ascent,
- .screen = .{},
- .atlas_stage = atlas_stage,
- .glyphs = std.AutoHashMap(u32, Slot).init(alloc),
- .test_mode = test_mode,
- .capture_images = false,
- .capture_dir = capture_dir,
- .trace_enabled = trace_enabled,
- .trace_fd = trace_fd,
- .trace_every = @max(trace_every, 1),
- };
- _ = c.ui_font_raster(font, scale, ' ', atlas_stage.ptr, @intCast(atlas_w), @intCast(self.cell_w), @intCast(self.cell_h), self.ascent);
- self.space_slot = .{ .u = 0, .v = 0 };
- self.pen_x = self.cell_w;
- self.tracef("sdl.init browser=webgl2 trace_every={d}", .{self.trace_every});
- return self;
-}
-
-// Headless debug init for PARDES_TEST_GRID=1: no SDL/GPU/window, just the cell
-// grid. Input comes from stdin (the pty), output goes to stdout (grid text
-// snapshots). Cell metrics are dummies: the pty gives us rows/cols directly.
-fn initTest(io: std.Io, alloc: std.mem.Allocator) !SdlBackend {
- return .{
- .io = io,
- .alloc = alloc,
- .sdl_window = undefined,
- .device = undefined,
- .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,
- .cell_h = 20,
- .ascent = 16,
- .screen = .{},
- .atlas_stage = &.{},
- .glyphs = std.AutoHashMap(u32, Slot).init(alloc),
- .test_mode = true,
- .grid_test_mode = true,
- };
-}
-
-pub fn deinit(self: *SdlBackend, alloc: ?std.mem.Allocator, writer: *std.Io.Writer) void {
- _ = writer;
- if (self.grid_test_mode) {
- self.screen.deinit(self.alloc);
- self.glyphs.deinit();
- 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();
- if (alloc) |a| a.free(self.atlas_stage);
- if (self.gl_context) |ctx| _ = c.SDL_GL_DestroyContext(ctx);
- c.SDL_DestroyWindow(self.sdl_window);
- c.SDL_Quit();
- return;
- }
- 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);
- self.glyphs.deinit();
- if (alloc) |a| a.free(self.atlas_stage);
- c.SDL_ReleaseWindowFromGPUDevice(self.device, self.sdl_window);
- c.SDL_DestroyWindow(self.sdl_window);
- c.SDL_DestroyGPUDevice(self.device);
- c.SDL_Quit();
-}
-
-// ---- vaxis-compatible surface ----
-
-pub fn resize(self: *SdlBackend, alloc: std.mem.Allocator, writer: *std.Io.Writer, ws: Winsize) !void {
- _ = writer;
- if (ws.cols == 0 or ws.rows == 0) return; // ignore zero-size (no pty yet)
- self.screen.deinit(alloc);
- self.screen = try vaxis.Screen.init(alloc, ws);
- self.screen.width_method = .wcwidth;
- if (self.capture_images) {
- _ = c.SDL_SetWindowSize(
- self.sdl_window,
- @intCast(@as(u32, ws.cols) * self.cell_w),
- @intCast(@as(u32, ws.rows) * self.cell_h),
- );
- _ = c.SDL_SyncWindow(self.sdl_window);
- }
- if (is_emscripten) return;
- if (!self.grid_test_mode) try self.ensureVbuf(ws.cols * ws.rows);
-}
-
-pub fn window(self: *SdlBackend) vaxis.Window {
- return .{
- .x_off = 0,
- .y_off = 0,
- .parent_x_off = 0,
- .parent_y_off = 0,
- .width = self.screen.width,
- .height = self.screen.height,
- .screen = &self.screen,
- };
-}
-
-pub fn render(self: *SdlBackend, writer: *std.Io.Writer) !void {
- _ = writer;
- if (!is_emscripten and self.grid_test_mode) return self.dumpGrid();
- if (is_emscripten) return self.renderGl();
-
- const trace_this = self.trace_enabled and (self.trace_frame % self.trace_every == 0);
- const t0 = nowNs();
- const cmd = c.SDL_AcquireGPUCommandBuffer(self.device) orelse return;
- const t_cmd = nowNs();
- var sw: u32 = 0;
- var sh: u32 = 0;
- var target: *c.SDL_GPUTexture = undefined;
- if (self.capture_images) {
- sw = @as(u32, self.screen.width) * self.cell_w;
- sh = @as(u32, self.screen.height) * self.cell_h;
- if (sw == 0 or sh == 0) {
- _ = c.SDL_SubmitGPUCommandBuffer(cmd);
- return;
- }
- try self.ensureCaptureTexture(sw, sh);
- target = self.capture_tex orelse {
- _ = c.SDL_SubmitGPUCommandBuffer(cmd);
- return;
- };
- } else {
- var swap_tex: ?*c.SDL_GPUTexture = null;
- const t_acquire0 = nowNs();
- if (!c.SDL_AcquireGPUSwapchainTexture(cmd, self.sdl_window, &swap_tex, &sw, &sh)) {
- _ = c.SDL_SubmitGPUCommandBuffer(cmd);
- return;
- }
- const t_acquire1 = nowNs();
- if (trace_this) self.tracef("sdl.event swapchain_acquire_us={d}", .{nsToUs(t_acquire1 - t_acquire0)});
- target = swap_tex orelse {
- if (trace_this) {
- self.tracef("sdl.frame n={d} skipped=swapchain_busy acquire_us={d}", .{ self.trace_frame, nsToUs(t_acquire1 - t_acquire0) });
- }
- self.trace_frame += 1;
- _ = c.SDL_SubmitGPUCommandBuffer(cmd);
- return;
- };
- }
- 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;
- const cells: u32 = @as(u32, cols) * @as(u32, rows);
- var t_mapped = t_acquired;
- var t_built = t_acquired;
- var t_atlas = t_acquired;
- var t_uploaded = t_acquired;
- var t_drawn = t_acquired;
- var dirty_cells: u32 = 0;
- var dirty_ranges: u32 = 0;
- var atlas_uploaded = false;
-
- // 1. build + upload the vertex buffer (one quad per cell).
- if (cells != 0 and self.vbuf_cells >= cells) {
- try self.ensureVbuf(cells); // no-op if already sized; safe guard
- const force_full_upload = !self.cell_cache_valid or self.last_render_w != sw or self.last_render_h != sh;
- const vptr: [*]u8 = @ptrCast(c.SDL_MapGPUTransferBuffer(self.device, self.vxfer, false) orelse {
- _ = c.SDL_SubmitGPUCommandBuffer(cmd);
- return;
- });
- const instances: [*]CellInstance = @ptrCast(@alignCast(vptr));
- t_mapped = nowNs();
- const layout = self.fixedCellLayout();
- var r: u16 = 0;
- while (r < rows) : (r += 1) {
- var col: u16 = 0;
- while (col < cols) : (col += 1) {
- const cell_index: u32 = @as(u32, r) * @as(u32, cols) + @as(u32, col);
- const key = self.cellKey(self.screen.buf[cell_index]);
- if (!force_full_upload and sameCellKey(self.cell_keys[cell_index], key)) continue;
- self.cell_keys[cell_index] = key;
- self.addDirtyRange(&dirty_ranges, cell_index);
- dirty_cells += 1;
- self.emitInstance(instances, cell_index, col, r, layout, win_w, win_h, key);
- }
- }
- c.SDL_UnmapGPUTransferBuffer(self.device, self.vxfer);
- t_built = nowNs();
- self.cell_cache_valid = true;
- self.last_render_w = sw;
- self.last_render_h = sh;
-
- // `emitQuad` calls ensureGlyph, which may rasterize new glyphs into the
- // atlas staging buffer. Upload after vertex generation so the first
- // rendered frame has the glyphs it references.
- if (self.atlas_dirty) {
- self.uploadAtlas(cmd);
- atlas_uploaded = true;
- }
- t_atlas = nowNs();
-
- if (dirty_cells != 0) {
- const copy = c.SDL_BeginGPUCopyPass(cmd);
- var i: u32 = 0;
- while (i < dirty_ranges) : (i += 1) {
- const range = self.dirty_ranges[i];
- const byte_offset: u32 = range.start * @sizeOf(CellInstance);
- const byte_size: u32 = range.count * @sizeOf(CellInstance);
- const src = c.SDL_GPUTransferBufferLocation{ .transfer_buffer = self.vxfer, .offset = byte_offset };
- const dst = c.SDL_GPUBufferRegion{ .buffer = self.vbuf, .offset = byte_offset, .size = byte_size };
- c.SDL_UploadToGPUBuffer(copy, &src, &dst, false);
- }
- c.SDL_EndGPUCopyPass(copy);
- }
- t_uploaded = nowNs();
-
- // 3. render pass: clear to the page bg, draw every cell's quad.
- var color_target = std.mem.zeroes(c.SDL_GPUColorTargetInfo);
- color_target.texture = target;
- color_target.clear_color = .{
- .r = @as(f32, @floatFromInt(self.bg_default[0])) / 255.0,
- .g = @as(f32, @floatFromInt(self.bg_default[1])) / 255.0,
- .b = @as(f32, @floatFromInt(self.bg_default[2])) / 255.0,
- .a = 1.0,
- };
- 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);
- c.SDL_BindGPUGraphicsPipeline(rp, self.pipeline);
- const samp_binding = c.SDL_GPUTextureSamplerBinding{ .texture = self.atlas_tex, .sampler = self.atlas_sampler };
- c.SDL_BindGPUFragmentSamplers(rp, 0, &samp_binding, 1);
- 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 {
- // no grid yet (or vbuf not sized): just clear the swapchain to the page bg.
- var color_target = std.mem.zeroes(c.SDL_GPUColorTargetInfo);
- color_target.texture = target;
- color_target.clear_color = .{
- .r = @as(f32, @floatFromInt(self.bg_default[0])) / 255.0,
- .g = @as(f32, @floatFromInt(self.bg_default[1])) / 255.0,
- .b = @as(f32, @floatFromInt(self.bg_default[2])) / 255.0,
- .a = 1.0,
- };
- 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();
- }
-
- if (self.capture_images) {
- const t_capture0 = nowNs();
- try self.captureSwapchain(cmd, target, sw, sh);
- const t_done = nowNs();
- if (trace_this) self.traceFrame(cells, dirty_cells, dirty_ranges, sw, sh, atlas_uploaded, t0, t_cmd, t_acquired, t_mapped, t_built, t_atlas, t_uploaded, t_drawn, t_capture0, t_done);
- self.trace_frame += 1;
- return;
- }
-
- const t_submit0 = nowNs();
- _ = c.SDL_SubmitGPUCommandBuffer(cmd);
- const t_done = nowNs();
- if (trace_this) self.traceFrame(cells, dirty_cells, dirty_ranges, sw, sh, atlas_uploaded, t0, t_cmd, t_acquired, t_mapped, t_built, t_atlas, t_uploaded, t_drawn, t_submit0, t_done);
- self.trace_frame += 1;
-}
-
-fn renderGl(self: *SdlBackend) !void {
- var pw: c_int = 0;
- var ph: c_int = 0;
- _ = c.SDL_GetWindowSizeInPixels(self.sdl_window, &pw, &ph);
- const sw: u32 = @max(1, @as(u32, @intCast(pw)));
- const sh: u32 = @max(1, @as(u32, @intCast(ph)));
- c.glViewport(0, 0, @intCast(sw), @intCast(sh));
- c.glClearColor(
- @as(f32, @floatFromInt(self.bg_default[0])) / 255.0,
- @as(f32, @floatFromInt(self.bg_default[1])) / 255.0,
- @as(f32, @floatFromInt(self.bg_default[2])) / 255.0,
- 1.0,
- );
- c.glClear(c.GL_COLOR_BUFFER_BIT);
-
- 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;
- }
-
- try self.ensureGlInstances(cells);
- const layout = self.cellLayout(sw, sh);
- const win_w: f32 = @floatFromInt(sw);
- const win_h: f32 = @floatFromInt(sh);
- var r: u16 = 0;
- while (r < rows) : (r += 1) {
- var col: u16 = 0;
- while (col < cols) : (col += 1) {
- const cell_index: u32 = @as(u32, r) * @as(u32, cols) + @as(u32, col);
- self.emitInstance(self.gl_instances.ptr, cell_index, col, r, layout, win_w, win_h, self.cellKey(self.screen.buf[cell_index]));
- }
- }
- if (self.atlas_dirty) self.uploadAtlasGl();
-
- c.glUseProgram(self.gl_program);
- c.glActiveTexture(c.GL_TEXTURE0);
- c.glBindTexture(c.GL_TEXTURE_2D, self.gl_atlas_tex);
- c.glUniform1i(self.gl_u_atlas, 0);
-
- c.glBindBuffer(c.GL_ARRAY_BUFFER, self.gl_vbo);
- c.glBufferData(c.GL_ARRAY_BUFFER, @intCast(cells * @sizeOf(CellInstance)), self.gl_instances.ptr, c.GL_STREAM_DRAW);
- const stride: c_int = @intCast(@sizeOf(CellInstance));
- c.glEnableVertexAttribArray(0);
- c.glVertexAttribPointer(0, 4, c.GL_FLOAT, c.GL_FALSE, stride, @ptrFromInt(@offsetOf(CellInstance, "x0")));
- c.glVertexAttribDivisor(0, 1);
- c.glEnableVertexAttribArray(1);
- c.glVertexAttribPointer(1, 4, c.GL_FLOAT, c.GL_FALSE, stride, @ptrFromInt(@offsetOf(CellInstance, "u0")));
- c.glVertexAttribDivisor(1, 1);
- c.glEnableVertexAttribArray(2);
- c.glVertexAttribPointer(2, 3, c.GL_FLOAT, c.GL_FALSE, stride, @ptrFromInt(@offsetOf(CellInstance, "fr")));
- c.glVertexAttribDivisor(2, 1);
- c.glEnableVertexAttribArray(3);
- c.glVertexAttribPointer(3, 3, c.GL_FLOAT, c.GL_FALSE, stride, @ptrFromInt(@offsetOf(CellInstance, "br")));
- 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);
-}
-
-fn fixedCellLayout(self: *const SdlBackend) CellLayout {
- return .{
- .w = @floatFromInt(self.cell_w),
- .h = @floatFromInt(self.cell_h),
- .x_off = 0,
- .y_off = 0,
- };
-}
-
-fn cellLayout(self: *const SdlBackend, sw: u32, sh: u32) CellLayout {
- const fixed = self.fixedCellLayout();
- if (!is_emscripten) return fixed;
-
- const cols = self.screen.width;
- const rows = self.screen.height;
- if (cols == 0 or rows == 0) return fixed;
-
- const cols_f: f32 = @floatFromInt(cols);
- const rows_f: f32 = @floatFromInt(rows);
- const grid_w = cols_f * fixed.w;
- const grid_h = rows_f * fixed.h;
- if (grid_w <= 0 or grid_h <= 0) return fixed;
-
- const win_w: f32 = @floatFromInt(sw);
- const win_h: f32 = @floatFromInt(sh);
- const scale = @min(win_w / grid_w, win_h / grid_h);
- const scaled_grid_w = grid_w * scale;
- const scaled_grid_h = grid_h * scale;
- return .{
- .w = fixed.w * scale,
- .h = fixed.h * scale,
- .x_off = if (win_w > scaled_grid_w) (win_w - scaled_grid_w) * 0.5 else 0,
- .y_off = if (win_h > scaled_grid_h) (win_h - scaled_grid_h) * 0.5 else 0,
- };
-}
-
-fn fixedMouseCell(self: *const SdlBackend, x: f32, y: f32) MouseCell {
- return .{
- .col = @as(i16, @intCast(@divTrunc(@as(i32, @intFromFloat(x)), @as(i32, @intCast(self.cell_w))))),
- .row = @as(i16, @intCast(@divTrunc(@as(i32, @intFromFloat(y)), @as(i32, @intCast(self.cell_h))))),
- };
-}
-
-fn floorToI16(value: f32) i16 {
- const floored = @floor(value);
- const min: f32 = @floatFromInt(std.math.minInt(i16));
- const max: f32 = @floatFromInt(std.math.maxInt(i16));
- if (floored <= min) return std.math.minInt(i16);
- if (floored >= max) return std.math.maxInt(i16);
- return @intFromFloat(floored);
-}
-
-fn mousePixel(self: *const SdlBackend, x: f32, y: f32, pixel_w: u32, pixel_h: u32) struct { x: f32, y: f32 } {
- if (!is_emscripten) return .{ .x = x, .y = y };
-
- var ww: c_int = 0;
- var wh: c_int = 0;
- _ = c.SDL_GetWindowSize(self.sdl_window, &ww, &wh);
- if (ww <= 0 or wh <= 0) return .{ .x = x, .y = y };
-
- return .{
- .x = x * (@as(f32, @floatFromInt(pixel_w)) / @as(f32, @floatFromInt(ww))),
- .y = y * (@as(f32, @floatFromInt(pixel_h)) / @as(f32, @floatFromInt(wh))),
- };
-}
-
-fn mouseCell(self: *const SdlBackend, x: f32, y: f32) MouseCell {
- if (!is_emscripten) return self.fixedMouseCell(x, y);
-
- var pw: c_int = 0;
- var ph: c_int = 0;
- _ = c.SDL_GetWindowSizeInPixels(self.sdl_window, &pw, &ph);
- const sw: u32 = @max(1, @as(u32, @intCast(pw)));
- const sh: u32 = @max(1, @as(u32, @intCast(ph)));
- const layout = self.cellLayout(sw, sh);
- const pixel = self.mousePixel(x, y, sw, sh);
- return .{
- .col = floorToI16((pixel.x - layout.x_off) / layout.w),
- .row = floorToI16((pixel.y - layout.y_off) / layout.h),
- };
-}
-
-fn clampF32(value: f32, lo: f32, hi: f32) f32 {
- if (value < lo) return lo;
- if (value > hi) return hi;
- return value;
-}
-
-fn findTouchSlot(self: *const 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 sortedTouchPair(a: u64, b: u64) [2]u64 {
- return if (a <= b) .{ a, b } else .{ b, a };
-}
-
-fn sameTouchPair(a: [2]u64, b: [2]u64) bool {
- return a[0] == b[0] and a[1] == b[1];
-}
-
-fn touchPairHas(ids: [2]u64, id: u64) bool {
- return ids[0] == id or ids[1] == id;
-}
-
-fn activeTouchPair(self: *const SdlBackend) ?[2]u64 {
- var ids: [2]u64 = .{ 0, 0 };
- var count: usize = 0;
- for (self.touches) |tp| {
- if (!tp.active) continue;
- if (count >= 2) return null;
- ids[count] = tp.id;
- count += 1;
- }
- return if (count == 2) sortedTouchPair(ids[0], ids[1]) else null;
-}
-
-fn touchPairCenter(self: *const SdlBackend, ids: [2]u64) ?TouchNormPoint {
- var sum_x: f32 = 0;
- var sum_y: f32 = 0;
- var count: usize = 0;
- for (ids) |id| {
- const idx = self.findTouchSlot(id) orelse return null;
- sum_x += self.touches[idx].x;
- sum_y += self.touches[idx].y;
- count += 1;
- }
- if (count != 2) return null;
- return .{ .x = sum_x * 0.5, .y = sum_y * 0.5 };
-}
-
-fn touchPairCenterWithEvent(self: *const SdlBackend, ids: [2]u64, tfinger: c.SDL_TouchFingerEvent) ?TouchNormPoint {
- const event_id: u64 = @intCast(tfinger.fingerID);
- var sum_x: f32 = 0;
- var sum_y: f32 = 0;
- var count: usize = 0;
- for (ids) |id| {
- if (id == event_id) {
- sum_x += tfinger.x;
- sum_y += tfinger.y;
- } else {
- const idx = self.findTouchSlot(id) orelse return null;
- sum_x += self.touches[idx].x;
- sum_y += self.touches[idx].y;
- }
- count += 1;
- }
- if (count != 2) return null;
- return .{ .x = sum_x * 0.5, .y = sum_y * 0.5 };
-}
-
-fn resetTouchScroll(self: *SdlBackend) void {
- self.touch_scroll = .{};
-}
-
-fn beginTouchScrollPair(self: *SdlBackend, ids: [2]u64) void {
- self.touch_scroll = .{
- .active = true,
- .ids = ids,
- .last_center = self.touchPairCenter(ids) orelse .{},
- };
-}
-
-fn syncTouchScrollPair(self: *SdlBackend) void {
- const ids = self.activeTouchPair() orelse {
- self.resetTouchScroll();
- return;
- };
- if (!self.touch_scroll.active or !sameTouchPair(self.touch_scroll.ids, ids)) {
- self.beginTouchScrollPair(ids);
- }
-}
-
-fn finishTouchScrollPair(self: *SdlBackend, tfinger: c.SDL_TouchFingerEvent) ?TouchNormPoint {
- const id: u64 = @intCast(tfinger.fingerID);
- if (!self.touch_scroll.active or !touchPairHas(self.touch_scroll.ids, id)) return null;
-
- const tap = tfinger.type == c.SDL_EVENT_FINGER_UP and !self.touch_scroll.scrolled;
- const center = if (tap)
- self.touchPairCenterWithEvent(self.touch_scroll.ids, tfinger) orelse self.touch_scroll.last_center
- else
- null;
- self.resetTouchScroll();
- return center;
-}
-
-fn noteTouchScrollMotion(self: *SdlBackend, tfinger: c.SDL_TouchFingerEvent) ?TouchScrollResult {
- const id: u64 = @intCast(tfinger.fingerID);
- if (!self.touch_scroll.active or !touchPairHas(self.touch_scroll.ids, id)) return null;
-
- const prev_center = self.touch_scroll.last_center;
- const center = self.touchPairCenter(self.touch_scroll.ids) orelse self.touch_scroll.last_center;
- self.touch_scroll.last_center = center;
- const delta_y = center.y - prev_center.y;
- self.touch_scroll.accum_y += delta_y;
-
- var ticks: i32 = 0;
- while (self.touch_scroll.accum_y >= touch_scroll_tick) {
- self.touch_scroll.accum_y -= touch_scroll_tick;
- ticks += 1;
- }
- while (self.touch_scroll.accum_y <= -touch_scroll_tick) {
- self.touch_scroll.accum_y += touch_scroll_tick;
- ticks -= 1;
- }
- if (ticks == 0) return null;
-
- self.touch_scroll.scrolled = true;
- return .{
- .center = center,
- .ticks = ticks,
- .delta_y = @as(f32, @floatFromInt(ticks)) * touch_scroll_tick,
- };
-}
-
-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 noteTouchMiddleClick(self: *SdlBackend, is_press: bool) void {
- if (is_press) self.touch_middle_clicks +%= 1;
- self.touch_middle_flash_frames = touch_middle_flash_max_frames;
-}
-
-fn fingerMousePoint(self: *const SdlBackend, x_norm: f32, y_norm: f32) struct { x: f32, y: f32 } {
- var ww: c_int = 0;
- var wh: c_int = 0;
- if (is_emscripten) {
- _ = c.SDL_GetWindowSize(self.sdl_window, &ww, &wh);
- } else {
- _ = c.SDL_GetWindowSizeInPixels(self.sdl_window, &ww, &wh);
- }
- const w: f32 = @floatFromInt(if (ww > 0) ww else 1);
- const h: f32 = @floatFromInt(if (wh > 0) wh else 1);
- return .{
- .x = clampF32(x_norm, 0.0, 1.0) * w,
- .y = clampF32(y_norm, 0.0, 1.0) * h,
- };
-}
-
-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,
- };
-
- if (self.touch_middle_flash_frames != 0) {
- const flash = @as(f32, @floatFromInt(self.touch_middle_flash_frames)) / @as(f32, @floatFromInt(touch_middle_flash_max_frames));
- const label = touchMiddleClickLabel(self.touch_middle_clicks);
- const glyph_scale = 3.0 * ui_scale;
- const char_step = 4.0 * glyph_scale;
- const pad = 5.0 * ui_scale;
- const stripe_w = 4.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;
- const x = 8.0 * ui_scale;
- const y = 8.0 * ui_scale;
-
- builder.addRect(0.0, 0.0, win_w, win_h, .{ .r = 1.0, .g = 0.78, .b = 0.18, .a = 0.055 * flash });
- builder.addRect(x, y, x + label_w, y + label_h, .{ .r = 0.0, .g = 0.0, .b = 0.0, .a = 0.54 + 0.22 * flash });
- builder.addRect(x + pad, y + pad, x + pad + stripe_w, y + label_h - pad, .{ .r = 1.0, .g = 0.78, .b = 0.18, .a = 0.96 });
- builder.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.96 });
- self.touch_middle_flash_frames -= 1;
- }
-
- 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 touchMiddleClickLabel(count: u32) [13]u8 {
- var out: [13]u8 = undefined;
- out[0] = 'M';
- out[1] = 'I';
- out[2] = 'D';
- out[3] = ' ';
- out[4] = '#';
- var i: usize = 0;
- while (i < 8) : (i += 1) {
- const shift: u5 = @intCast((7 - i) * 4);
- out[i + 5] = hexDigit((count >> 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 },
- 'I' => .{ 0b111, 0b010, 0b010, 0b010, 0b111 },
- 'M' => .{ 0b101, 0b111, 0b111, 0b101, 0b101 },
- 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 {
- const w = self.screen.width;
- const h = self.screen.height;
- if (w == 0 or h == 0) return;
-
- // Build the whole frame into one buffer, then write it in one syscall so
- // the harness sees an atomic snapshot (no torn frames).
- var buf: std.ArrayList(u8) = .empty;
- defer buf.deinit(self.alloc);
- // header: rows cols cursor_x cursor_y cursor_vis
- var hdr: [128]u8 = undefined;
- const hdr_str = std.fmt.bufPrint(&hdr, "\n---FRAME:{d}:{d}:{d}:{d}:{}---\n", .{
- h, w, self.screen.cursor.col, self.screen.cursor.row, self.screen.cursor_vis,
- }) catch return;
- buf.appendSlice(self.alloc, hdr_str) catch return;
- for (0..h) |row| {
- for (0..w) |col| {
- const cell = self.screen.buf[row * w + col];
- const g = cell.char.grapheme;
- if (g.len > 0) buf.appendSlice(self.alloc, g) catch return else buf.append(self.alloc, ' ') catch return;
- }
- buf.append(self.alloc, '\n') catch return;
- }
- buf.appendSlice(self.alloc, "---ENDFRAME---\n") catch return;
-
- try writeAllFd(1, buf.items);
-}
-
-fn captureSwapchain(self: *SdlBackend, cmd: *c.SDL_GPUCommandBuffer, target: *c.SDL_GPUTexture, sw: u32, sh: u32) !void {
- if (sw == 0 or sh == 0) {
- _ = c.SDL_SubmitGPUCommandBuffer(cmd);
- return;
- }
- const size = c.SDL_CalculateGPUTextureFormatSize(self.swapchain_format, sw, sh, 1);
- if (size == 0) {
- _ = c.SDL_SubmitGPUCommandBuffer(cmd);
- return error.UnsupportedCaptureFormat;
- }
- try self.ensureCaptureXfer(size);
-
- const xfer = self.capture_xfer orelse {
- _ = c.SDL_SubmitGPUCommandBuffer(cmd);
- return error.GpuCreate;
- };
- const copy = c.SDL_BeginGPUCopyPass(cmd);
- const src = c.SDL_GPUTextureRegion{
- .texture = target,
- .mip_level = 0,
- .layer = 0,
- .x = 0,
- .y = 0,
- .z = 0,
- .w = sw,
- .h = sh,
- .d = 1,
- };
- const dst = c.SDL_GPUTextureTransferInfo{
- .transfer_buffer = xfer,
- .offset = 0,
- .pixels_per_row = sw,
- .rows_per_layer = sh,
- };
- c.SDL_DownloadFromGPUTexture(copy, &src, &dst);
- c.SDL_EndGPUCopyPass(copy);
-
- const fence = c.SDL_SubmitGPUCommandBufferAndAcquireFence(cmd) orelse return error.GpuSubmit;
- defer c.SDL_ReleaseGPUFence(self.device, fence);
- var fences = [_]*c.SDL_GPUFence{fence};
- if (!c.SDL_WaitForGPUFences(self.device, true, &fences, 1)) return error.GpuSubmit;
-
- const mapped = c.SDL_MapGPUTransferBuffer(self.device, xfer, false) orelse return error.GpuMap;
- defer c.SDL_UnmapGPUTransferBuffer(self.device, xfer);
- const pixels: [*]const u8 = @ptrCast(mapped);
- try self.writeCapturePpm(pixels[0..size], sw, sh);
-}
-
-fn ensureCaptureTexture(self: *SdlBackend, width: u32, height: u32) !void {
- if (self.capture_tex != null and self.capture_tex_w == width and self.capture_tex_h == height) return;
- if (self.capture_tex) |t| c.SDL_ReleaseGPUTexture(self.device, t);
- var info = std.mem.zeroes(c.SDL_GPUTextureCreateInfo);
- info.type = c.SDL_GPU_TEXTURETYPE_2D;
- info.format = self.swapchain_format;
- info.usage = c.SDL_GPU_TEXTUREUSAGE_COLOR_TARGET;
- info.width = width;
- info.height = height;
- info.layer_count_or_depth = 1;
- info.num_levels = 1;
- info.sample_count = c.SDL_GPU_SAMPLECOUNT_1;
- self.capture_tex = c.SDL_CreateGPUTexture(self.device, &info) orelse return error.GpuCreate;
- self.capture_tex_w = width;
- self.capture_tex_h = height;
-}
-
-fn ensureCaptureXfer(self: *SdlBackend, size: u32) !void {
- if (self.capture_xfer != null and self.capture_xfer_size >= size) return;
- if (self.capture_xfer) |x| c.SDL_ReleaseGPUTransferBuffer(self.device, x);
- var info = c.SDL_GPUTransferBufferCreateInfo{
- .usage = c.SDL_GPU_TRANSFERBUFFERUSAGE_DOWNLOAD,
- .size = size,
- .props = 0,
- };
- self.capture_xfer = c.SDL_CreateGPUTransferBuffer(self.device, &info) orelse return error.GpuCreate;
- self.capture_xfer_size = size;
-}
-
-fn writeCapturePpm(self: *SdlBackend, pixels: []const u8, width: u32, height: u32) !void {
- const dir = self.capture_dir orelse return;
- const bpp = c.SDL_GPUTextureFormatTexelBlockSize(self.swapchain_format);
- if (bpp != 4) return error.UnsupportedCaptureFormat;
-
- var tmp_buf: [4096]u8 = undefined;
- var final_buf: [4096]u8 = undefined;
- const tmp_path = std.fmt.bufPrintZ(&tmp_buf, "{s}/latest.ppm.tmp", .{dir}) catch return error.CapturePathTooLong;
- const final_path = std.fmt.bufPrintZ(&final_buf, "{s}/latest.ppm", .{dir}) catch return error.CapturePathTooLong;
-
- const fd = openCreateTrunc(tmp_path.ptr) orelse return error.CaptureWriteFailed;
- defer closeFdBestEffort(fd);
-
- var header: [64]u8 = undefined;
- const hdr = std.fmt.bufPrint(&header, "P6\n{d} {d}\n255\n", .{ width, height }) catch return error.CaptureWriteFailed;
- try writeAllFd(fd, hdr);
-
- const row_rgb = try self.alloc.alloc(u8, @as(usize, width) * 3);
- defer self.alloc.free(row_rgb);
- for (0..height) |row| {
- const row_start = @as(usize, row) * @as(usize, width) * 4;
- const src = pixels[row_start .. row_start + @as(usize, width) * 4];
- var x: usize = 0;
- while (x < @as(usize, width)) : (x += 1) {
- const si = x * 4;
- const di = x * 3;
- switch (self.swapchain_format) {
- c.SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM,
- c.SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM_SRGB,
- => {
- row_rgb[di + 0] = src[si + 0];
- row_rgb[di + 1] = src[si + 1];
- row_rgb[di + 2] = src[si + 2];
- },
- c.SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM,
- c.SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM_SRGB,
- => {
- row_rgb[di + 0] = src[si + 2];
- row_rgb[di + 1] = src[si + 1];
- row_rgb[di + 2] = src[si + 0];
- },
- else => return error.UnsupportedCaptureFormat,
- }
- }
- try writeAllFd(fd, row_rgb);
- }
-
- renameZ(tmp_path.ptr, final_path.ptr) catch return error.CaptureWriteFailed;
- self.capture_frame += 1;
-}
-
-fn openTraceFile(path: []const u8) posix.fd_t {
- var buf: [4096]u8 = undefined;
- const z = std.fmt.bufPrintZ(&buf, "{s}", .{path}) catch return 2;
- return openCreateTrunc(z.ptr) orelse 2;
-}
-
-fn tracef(self: *SdlBackend, comptime fmt: []const u8, args: anytype) void {
- if (is_emscripten) return;
- if (!self.trace_enabled or self.trace_fd < 0) return;
- var buf: [1024]u8 = undefined;
- const line = std.fmt.bufPrint(&buf, fmt ++ "\n", args) catch return;
- writeAllFdIgnore(self.trace_fd, line);
-}
-
-fn traceFrame(
- self: *SdlBackend,
- cells: u32,
- dirty_cells: u32,
- dirty_ranges: u32,
- sw: u32,
- sh: u32,
- atlas_uploaded: bool,
- t0: i128,
- t_cmd: i128,
- t_acquired: i128,
- t_mapped: i128,
- t_built: i128,
- t_atlas: i128,
- t_uploaded: i128,
- t_drawn: i128,
- t_submit0: i128,
- t_done: i128,
-) void {
- self.tracef(
- "sdl.frame n={d} size={d}x{d} cells={d} dirty={d} ranges={d} atlas={} cmd_us={d} acquire_us={d} map_us={d} build_us={d} atlas_us={d} upload_us={d} draw_us={d} submit_or_capture_us={d} total_us={d}",
- .{
- self.trace_frame,
- sw,
- sh,
- cells,
- dirty_cells,
- dirty_ranges,
- atlas_uploaded,
- nsToUs(t_cmd - t0),
- nsToUs(t_acquired - t_cmd),
- nsToUs(t_mapped - t_acquired),
- nsToUs(t_built - t_mapped),
- nsToUs(t_atlas - t_built),
- nsToUs(t_uploaded - t_atlas),
- nsToUs(t_drawn - t_uploaded),
- nsToUs(t_done - t_submit0),
- nsToUs(t_done - t0),
- },
- );
-}
-
-// no-ops for the terminal-protocol surface; SDL needs none of them.
-pub fn setMouseMode(self: *SdlBackend, writer: *std.Io.Writer, enable: bool) !void {
- _ = self;
- _ = writer;
- _ = enable;
-}
-pub fn enterAltScreen(self: *SdlBackend, writer: *std.Io.Writer) !void {
- _ = self;
- _ = writer;
-}
-pub fn exitAltScreen(self: *SdlBackend, writer: *std.Io.Writer) !void {
- _ = self;
- _ = writer;
-}
-pub fn queryTerminal(self: *SdlBackend, writer: *std.Io.Writer, timeout: std.Io.Duration) !void {
- _ = self;
- _ = writer;
- _ = timeout;
-}
-pub fn requestSystemClipboard(self: *SdlBackend, writer: *std.Io.Writer) !void {
- _ = self;
- _ = writer;
- // ponytail: the terminal backend gets the clipboard asynchronously via an
- // OSC 52 reply (.paste event). SDL could read it synchronously here, but the
- // result can't be posted without a loop pointer — left as a TODO for now.
-}
-pub fn copyToSystemClipboard(self: *SdlBackend, writer: *std.Io.Writer, text: []const u8, encode_allocator: std.mem.Allocator) !void {
- _ = self;
- _ = writer;
- _ = encode_allocator;
- var buf: [4096]u8 = undefined;
- const z = std.fmt.bufPrintZ(&buf, "{s}", .{text}) catch return;
- _ = c.SDL_SetClipboardText(z.ptr);
-}
-pub fn transmitPreEncodedImage(
- self: *SdlBackend,
- writer: *std.Io.Writer,
- bytes: []const u8,
- width: u16,
- height: u16,
- format: vaxis.Image.TransmitFormat,
-) !vaxis.Image {
- _ = self;
- _ = writer;
- _ = bytes;
- _ = width;
- _ = height;
- _ = format;
- return error.NoGraphicsCapability;
-}
-
-// ---- internals ----
-
-fn ensureVbuf(self: *SdlBackend, cells: u32) !void {
- if (self.vbuf_cells == cells and cells != 0) return;
- if (self.vbuf_cells != 0) {
- c.SDL_ReleaseGPUBuffer(self.device, self.vbuf);
- c.SDL_ReleaseGPUTransferBuffer(self.device, self.vxfer);
- }
- self.alloc.free(self.cell_keys);
- self.alloc.free(self.dirty_ranges);
- self.cell_keys = &.{};
- self.dirty_ranges = &.{};
- self.cell_cache_valid = false;
- self.last_render_w = 0;
- self.last_render_h = 0;
- const size = cells * @sizeOf(CellInstance);
- var vb_info = c.SDL_GPUBufferCreateInfo{ .usage = c.SDL_GPU_BUFFERUSAGE_VERTEX, .size = size, .props = 0 };
- self.vbuf = c.SDL_CreateGPUBuffer(self.device, &vb_info) orelse return error.GpuCreate;
- var xf_info = c.SDL_GPUTransferBufferCreateInfo{ .usage = c.SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD, .size = size, .props = 0 };
- self.vxfer = c.SDL_CreateGPUTransferBuffer(self.device, &xf_info) orelse return error.GpuCreate;
- self.cell_keys = try self.alloc.alloc(CellKey, cells);
- self.dirty_ranges = try self.alloc.alloc(DirtyRange, cells);
- self.vbuf_cells = cells;
-}
-
-fn ensureGlInstances(self: *SdlBackend, cells: u32) !void {
- if (self.gl_instances.len >= cells) return;
- self.alloc.free(self.gl_instances);
- self.gl_instances = try self.alloc.alloc(CellInstance, cells);
-}
-
-fn resolveColor(self: *const SdlBackend, col: vaxis.Color, is_bg: bool) [3]u8 {
- return switch (col) {
- .default => if (is_bg) self.bg_default else self.fg_default,
- .index => |i| ansi[i & 15],
- .rgb => |r| r,
- };
-}
-
-fn packRgb(rgb: [3]u8) u32 {
- return (@as(u32, rgb[0]) << 16) | (@as(u32, rgb[1]) << 8) | @as(u32, rgb[2]);
-}
-
-fn unpackRgb(rgb: u32) [3]u8 {
- return .{
- @intCast((rgb >> 16) & 0xff),
- @intCast((rgb >> 8) & 0xff),
- @intCast(rgb & 0xff),
- };
-}
-
-fn isDefaultColor(color: vaxis.Color) bool {
- return switch (color) {
- .default => true,
- else => false,
- };
-}
-
-// Decode the first codepoint of a UTF-8 grapheme; 0x20 (" ") on failure/empty.
-fn firstCp(s: []const u8) u32 {
- if (s.len == 0) return ' ';
- const cp = std.unicode.utf8Decode(s[0 .. std.unicode.utf8ByteSequenceLength(s[0]) catch return ' ']) catch return ' ';
- return @intCast(cp);
-}
-
-fn cellKey(self: *const SdlBackend, cell: vaxis.Cell) CellKey {
- if (cell.char.grapheme.len == 0 and
- isDefaultColor(cell.style.fg) and
- isDefaultColor(cell.style.bg) and
- !cell.style.reverse and
- !cell.style.invisible and
- !cell.style.dim)
- {
- return .{
- .cp = ' ',
- .fg = packRgb(self.fg_default),
- .bg = packRgb(self.bg_default),
- };
- }
-
- var fg = self.resolveColor(cell.style.fg, false);
- var bg = self.resolveColor(cell.style.bg, true);
- if (cell.style.reverse) {
- const t = fg;
- fg = bg;
- bg = t;
- }
- if (cell.style.invisible) fg = bg;
- if (cell.style.dim) {
- fg[0] = fg[0] * 6 / 10;
- fg[1] = fg[1] * 6 / 10;
- fg[2] = fg[2] * 6 / 10;
- }
- const cp = if (cell.char.grapheme.len == 0) ' ' else firstCp(cell.char.grapheme);
- return .{ .cp = cp, .fg = packRgb(fg), .bg = packRgb(bg) };
-}
-
-fn sameCellKey(a: CellKey, b: CellKey) bool {
- return a.cp == b.cp and a.fg == b.fg and a.bg == b.bg;
-}
-
-fn addDirtyRange(self: *SdlBackend, range_count: *u32, cell_index: u32) void {
- if (range_count.* > 0) {
- const last = &self.dirty_ranges[range_count.* - 1];
- if (last.start + last.count == cell_index) {
- last.count += 1;
- return;
- }
- }
- self.dirty_ranges[range_count.*] = .{ .start = cell_index, .count = 1 };
- range_count.* += 1;
-}
-
-fn ensureGlyph(self: *SdlBackend, cp: u32) Slot {
- if (self.glyphs.get(cp)) |s| return s;
- if (self.pen_x + self.cell_w > atlas_w) {
- self.pen_x = 0;
- self.pen_y += self.cell_h;
- }
- if (self.pen_y + self.cell_h > atlas_h) return self.space_slot; // atlas full: fall back
- const su = self.pen_x;
- const sv = self.pen_y;
- const out = self.atlas_stage.ptr + @as(usize, sv) * atlas_w + su;
- _ = c.ui_font_raster(
- self.font.?,
- self.scale,
- @intCast(cp),
- out,
- @intCast(atlas_w),
- @intCast(self.cell_w),
- @intCast(self.cell_h),
- self.ascent,
- );
- const s = Slot{ .u = su, .v = sv };
- self.glyphs.put(cp, s) catch return self.space_slot;
- self.pen_x += self.cell_w;
- self.atlas_dirty = true;
- return s;
-}
-
-fn emitInstance(
- self: *SdlBackend,
- instances: [*]CellInstance,
- cell_index: u32,
- col: u16,
- row: u16,
- layout: CellLayout,
- win_w: f32,
- win_h: f32,
- key: CellKey,
-) void {
- // cell pixel rect (top-left origin)
- const px0 = layout.x_off + @as(f32, @floatFromInt(col)) * layout.w;
- const py0 = layout.y_off + @as(f32, @floatFromInt(row)) * layout.h;
- const px1 = px0 + layout.w;
- const py1 = py0 + layout.h;
- // -> NDC (SDL GPU clip space: x right, y up; framebuffer row 0 = top)
- const x0 = (px0 / win_w) * 2.0 - 1.0;
- const x1 = (px1 / win_w) * 2.0 - 1.0;
- const y0 = 1.0 - (py0 / win_h) * 2.0;
- const y1 = 1.0 - (py1 / win_h) * 2.0;
-
- const fg = unpackRgb(key.fg);
- const bg = unpackRgb(key.bg);
- const fgn: [3]f32 = .{
- @as(f32, @floatFromInt(fg[0])) / 255.0,
- @as(f32, @floatFromInt(fg[1])) / 255.0,
- @as(f32, @floatFromInt(fg[2])) / 255.0,
- };
- const bgn: [3]f32 = .{
- @as(f32, @floatFromInt(bg[0])) / 255.0,
- @as(f32, @floatFromInt(bg[1])) / 255.0,
- @as(f32, @floatFromInt(bg[2])) / 255.0,
- };
-
- // glyph slot (the space slot covers blanks, which sample alpha=0 → bg only)
- const slot = if (key.cp == ' ') self.space_slot else self.ensureGlyph(key.cp);
- const aw_f: f32 = @floatFromInt(atlas_w);
- const ah_f: f32 = @floatFromInt(atlas_h);
- const u_start = @as(f32, @floatFromInt(slot.u)) / aw_f;
- const v_start = @as(f32, @floatFromInt(slot.v)) / ah_f;
- const u_end = @as(f32, @floatFromInt(slot.u + self.cell_w)) / aw_f;
- const v_end = @as(f32, @floatFromInt(slot.v + self.cell_h)) / ah_f;
-
- instances[cell_index] = .{
- .x0 = x0,
- .y0 = y0,
- .x1 = x1,
- .y1 = y1,
- .u0 = u_start,
- .v0 = v_start,
- .u1 = u_end,
- .v1 = v_end,
- .fr = fgn[0],
- .fg = fgn[1],
- .fb = fgn[2],
- .br = bgn[0],
- .bg = bgn[1],
- .bb = bgn[2],
- };
-}
-
-fn uploadAtlas(self: *SdlBackend, cmd: *c.SDL_GPUCommandBuffer) void {
- const ptr = c.SDL_MapGPUTransferBuffer(self.device, self.atlas_xfer, false) orelse return;
- const dst: [*]u8 = @ptrCast(ptr);
- @memcpy(dst[0 .. atlas_w * atlas_h], self.atlas_stage);
- c.SDL_UnmapGPUTransferBuffer(self.device, self.atlas_xfer);
-
- const copy = c.SDL_BeginGPUCopyPass(cmd);
- const src = c.SDL_GPUTextureTransferInfo{
- .transfer_buffer = self.atlas_xfer,
- .offset = 0,
- .pixels_per_row = atlas_w,
- .rows_per_layer = atlas_h,
- };
- const dstregion = c.SDL_GPUTextureRegion{
- .texture = self.atlas_tex,
- .mip_level = 0,
- .layer = 0,
- .x = 0,
- .y = 0,
- .z = 0,
- .w = atlas_w,
- .h = atlas_h,
- .d = 1,
- };
- c.SDL_UploadToGPUTexture(copy, &src, &dstregion, false);
- c.SDL_EndGPUCopyPass(copy);
- self.atlas_dirty = false;
-}
-
-fn uploadAtlasGl(self: *SdlBackend) void {
- c.glBindTexture(c.GL_TEXTURE_2D, self.gl_atlas_tex);
- c.glPixelStorei(c.GL_UNPACK_ALIGNMENT, 1);
- c.glTexSubImage2D(c.GL_TEXTURE_2D, 0, 0, 0, atlas_w, atlas_h, c.GL_RED, c.GL_UNSIGNED_BYTE, self.atlas_stage.ptr);
- self.atlas_dirty = false;
-}
-
-fn makePipeline(device: *c.SDL_GPUDevice, color_format: c.SDL_GPUTextureFormat) !*c.SDL_GPUGraphicsPipeline {
- const vs = try makeShader(device, vert_spv_bytes, c.SDL_GPU_SHADERSTAGE_VERTEX, 0);
- defer c.SDL_ReleaseGPUShader(device, vs);
- const fs = try makeShader(device, frag_spv_bytes, c.SDL_GPU_SHADERSTAGE_FRAGMENT, 1);
- defer c.SDL_ReleaseGPUShader(device, fs);
-
- var vbuf_desc = c.SDL_GPUVertexBufferDescription{
- .slot = 0,
- .pitch = @sizeOf(CellInstance),
- .input_rate = c.SDL_GPU_VERTEXINPUTRATE_INSTANCE,
- .instance_step_rate = 0,
- };
- var attrs = [_]c.SDL_GPUVertexAttribute{
- .{ .location = 0, .buffer_slot = 0, .format = c.SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4, .offset = @offsetOf(CellInstance, "x0") },
- .{ .location = 1, .buffer_slot = 0, .format = c.SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4, .offset = @offsetOf(CellInstance, "u0") },
- .{ .location = 2, .buffer_slot = 0, .format = c.SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, .offset = @offsetOf(CellInstance, "fr") },
- .{ .location = 3, .buffer_slot = 0, .format = c.SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, .offset = @offsetOf(CellInstance, "br") },
- };
- 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.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 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,
- stage: c.SDL_GPUShaderStage,
- n_samplers: u32,
-) !*c.SDL_GPUShader {
- var info = std.mem.zeroes(c.SDL_GPUShaderCreateInfo);
- info.code = code.ptr;
- info.code_size = code.len;
- info.entrypoint = "main";
- info.format = c.SDL_GPU_SHADERFORMAT_SPIRV;
- info.stage = stage;
- info.num_samplers = n_samplers;
- return c.SDL_CreateGPUShader(device, &info) orelse error.GpuCreate;
-}
-
-fn makeGlProgram() !c_uint {
- 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, fragment_source);
- defer c.glDeleteShader(fs);
-
- const program = c.glCreateProgram();
- if (program == 0) return error.GpuCreate;
- c.glAttachShader(program, vs);
- c.glAttachShader(program, fs);
- c.glLinkProgram(program);
- var ok: c_int = 0;
- c.glGetProgramiv(program, c.GL_LINK_STATUS, &ok);
- if (ok == 0) {
- log.err("WebGL program link failed", .{});
- c.glDeleteProgram(program);
- return error.GpuCreate;
- }
- return program;
-}
-
-fn makeGlShader(kind: c_uint, source: []const u8) !c_uint {
- const shader = c.glCreateShader(kind);
- if (shader == 0) return error.GpuCreate;
- var src_ptr = [_][*c]const u8{@ptrCast(source.ptr)};
- var src_len = [_]c_int{@intCast(source.len)};
- c.glShaderSource(shader, 1, &src_ptr, &src_len);
- c.glCompileShader(shader);
- var ok: c_int = 0;
- c.glGetShaderiv(shader, c.GL_COMPILE_STATUS, &ok);
- if (ok == 0) {
- log.err("WebGL shader compile failed", .{});
- c.glDeleteShader(shader);
- return error.GpuCreate;
- }
- return shader;
-}
-
-// =====================================================================
-// Tty stub — the app threads a `*std.Io.Writer` through every backend call; SDL
-// needs none of it, so we back it with /dev/null. The pointer just has to stay
-// valid for the app's lifetime.
-// =====================================================================
-
-const SdlTty = struct {
- const File = if (is_emscripten) void else std.Io.File;
- const Writer = if (is_emscripten) std.Io.Writer.Discarding else std.Io.File.Writer;
-
- file: File,
- tty_writer: Writer,
-
- pub fn init(io: std.Io, buffer: []u8) !SdlTty {
- if (is_emscripten) return .{
- .file = {},
- .tty_writer = .init(buffer),
- };
- const f = try std.Io.Dir.openFileAbsolute(io, "/dev/null", .{ .mode = .read_write });
- return .{ .file = f, .tty_writer = f.writerStreaming(io, buffer) };
- }
- pub fn deinit(self: SdlTty, io: std.Io) void {
- if (is_emscripten) return;
- self.file.close(io);
- }
- pub fn writer(self: *SdlTty) *std.Io.Writer {
- if (is_emscripten) return &self.tty_writer.writer;
- return &self.tty_writer.interface;
- }
-};
-
-// =====================================================================
-// Event loop — polls SDL on the main thread and feeds the same `Event` union
-// the app expects onto a thread-safe queue (so per-terminal pty readers, which
-// call postEvent from their own threads, keep working unchanged).
-// =====================================================================
-
-fn SdlLoop(comptime EventType: type) type {
- return struct {
- const Self = @This();
- const Reader = if (is_emscripten) void else ?std.Io.Future(void);
-
- io: std.Io,
- vx: *SdlBackend,
- queue: vaxis.Queue(EventType, 512),
- // live keyboard modifier state (updated on every key down/up), so
- // SDL_TEXT_INPUT — which carries no modifiers — can attach them.
- live_mods: vaxis.Key.Modifiers = .{},
- last_cols: u16 = 0,
- last_rows: u16 = 0,
- // test-mode stdin reader task + quit flag
- reader: Reader = if (is_emscripten) {} else null,
- should_quit: bool = false,
-
- pub fn init(io: std.Io, tty: *SdlTty, vx: *SdlBackend) Self {
- _ = tty;
- return .{ .io = io, .vx = vx, .queue = .init(io) };
- }
-
- pub fn start(self: *Self) !void {
- if (!is_emscripten and self.vx.test_mode) {
- setStdinRaw() catch {};
- // e2e: spawn a stdin reader that parses terminal escape sequences
- // (vaxis.Parser) into Key/Mouse/Winsize events and posts them.
- self.reader = try self.io.concurrent(Self.stdinRun, .{self});
- return;
- }
- // start delivering text input events (for printable keys)
- _ = c.SDL_StartTextInput(self.vx.sdl_window);
- // report the initial window size so the app sizes its grid
- self.postWinsize();
- }
- pub fn stop(self: *Self) void {
- if (!is_emscripten and self.vx.test_mode) {
- self.should_quit = true;
- if (self.reader) |*r| r.await(self.io);
- self.reader = null;
- return;
- }
- }
- pub fn installResizeHandler(self: *Self) !void {
- if (!is_emscripten and self.vx.test_mode) {
- var act = posix.Sigaction{
- .handler = .{ .handler = winchHandler },
- .mask = switch (builtin.os.tag) {
- .macos => 0,
- else => posix.sigemptyset(),
- },
- .flags = 0,
- };
- posix.sigaction(posix.SIG.WINCH, &act, null);
- return;
- }
- }
-
- pub fn postEvent(self: *Self, event: EventType) !void {
- try self.queue.push(event);
- if (!self.vx.test_mode) {
- // wake a blocking SDL_WaitEventTimeout in nextEvent (pty reads come
- // from other threads; without this the app wouldn't notice them
- // until the timeout elapses).
- var sev = std.mem.zeroes(c.SDL_Event);
- sev.type = c.SDL_EVENT_USER;
- _ = c.SDL_PushEvent(&sev);
- }
- }
-
- pub fn nextEvent(self: *Self) !EventType {
- if (!is_emscripten and self.vx.test_mode) return try self.queue.pop();
- while (true) {
- // pty/queued events first (they're already decoded)
- if (try self.queue.tryPop()) |ev| {
- if (self.vx.trace_enabled) self.vx.tracef("sdl.event source=queue wait_us=0", .{});
- return ev;
- }
- var sev = std.mem.zeroes(c.SDL_Event);
- const t0 = nowNs();
- if (c.SDL_WaitEventTimeout(&sev, 16)) {
- const t1 = nowNs();
- if (try self.dispatch(&sev)) |ev| {
- if (self.vx.trace_enabled) self.vx.tracef("sdl.event source=sdl type={d} wait_us={d}", .{ sev.type, nsToUs(t1 - t0) });
- return ev;
- }
- // else: an SDL event we ignore — loop and re-check the queue
- } else {
- if (self.vx.trace_enabled) self.vx.tracef("sdl.event source=tick wait_us={d}", .{nsToUs(nowNs() - t0)});
- return .tick; // timeout: a no-op that drives a render
- }
- }
- }
-
- pub fn tryEvent(self: *Self) !?EventType {
- if (try self.queue.tryPop()) |ev| return ev;
- if (!is_emscripten and self.vx.test_mode) return null;
- var sev = std.mem.zeroes(c.SDL_Event);
- while (c.SDL_PollEvent(&sev)) {
- if (try self.dispatch(&sev)) |ev| return ev;
- if (try self.queue.tryPop()) |ev| return ev;
- }
- if (is_emscripten) {
- self.postWinsize();
- if (try self.queue.tryPop()) |ev| return ev;
- }
- return null;
- }
-
- fn postWinsize(self: *Self) void {
- if (!is_emscripten and self.vx.test_mode) {
- // read winsize from stdin (fd 0 = the pty slave)
- var ws: posix.winsize = std.mem.zeroes(posix.winsize);
- const iorc = posix.system.ioctl(0, tiocgwinszReq(), @intFromPtr(&ws));
- if (iorc < 0) return; // fd 0 is not a pty
- const cols: u16 = ws.col;
- const rows: u16 = ws.row;
- if (cols == 0 or rows == 0 or cols > 1000 or rows > 1000) return;
- if (cols == self.last_cols and rows == self.last_rows) return;
- self.last_cols = cols;
- self.last_rows = rows;
- self.postEvent(.{ .winsize = .{
- .cols = cols,
- .rows = rows,
- .x_pixel = ws.xpixel,
- .y_pixel = ws.ypixel,
- } }) catch {};
- return;
- }
- var pw: c_int = 0;
- var ph: c_int = 0;
- if (is_emscripten) {
- var css_w: f64 = 0;
- var css_h: f64 = 0;
- if (emscripten_get_element_css_size("#canvas", &css_w, &css_h) == EMSCRIPTEN_RESULT_SUCCESS and css_w > 0 and css_h > 0) {
- pw = @intFromFloat(@round(css_w * web_render_scale_f64));
- ph = @intFromFloat(@round(css_h * web_render_scale_f64));
- var cur_w: c_int = 0;
- var cur_h: c_int = 0;
- _ = c.SDL_GetWindowSizeInPixels(self.vx.sdl_window, &cur_w, &cur_h);
- if (cur_w != pw or cur_h != ph) {
- _ = c.SDL_SetWindowSize(self.vx.sdl_window, pw, ph);
- _ = c.SDL_SyncWindow(self.vx.sdl_window);
- }
- } else {
- _ = c.SDL_GetWindowSizeInPixels(self.vx.sdl_window, &pw, &ph);
- }
- } else {
- _ = c.SDL_GetWindowSizeInPixels(self.vx.sdl_window, &pw, &ph);
- }
- const cw: u32 = self.vx.cell_w;
- const ch: u32 = self.vx.cell_h;
- const cols: u16 = @intCast(@max(1, @divTrunc(@as(u32, @intCast(pw)), cw)));
- const rows: u16 = @intCast(@max(1, @divTrunc(@as(u32, @intCast(ph)), ch)));
- if (cols == self.last_cols and rows == self.last_rows) return;
- self.last_cols = cols;
- self.last_rows = rows;
- self.postEvent(.{ .winsize = .{
- .cols = cols,
- .rows = rows,
- .x_pixel = @intCast(pw),
- .y_pixel = @intCast(ph),
- } }) catch {};
- }
-
- fn mouseEvent(self: *Self, cell: MouseCell, button: vaxis.Mouse.Button, t: vaxis.Mouse.Type) EventType {
- return EventType{ .mouse = .{
- .col = cell.col,
- .row = cell.row,
- .button = button,
- .mods = sdlMouseMods(self.live_mods),
- .type = t,
- } };
- }
-
- fn dispatch(self: *Self, sev: *const c.SDL_Event) !?EventType {
- switch (sev.type) {
- c.SDL_EVENT_QUIT, c.SDL_EVENT_WINDOW_CLOSE_REQUESTED => return .quit,
- c.SDL_EVENT_WINDOW_RESIZED, c.SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED => {
- self.postWinsize();
- return null;
- },
- c.SDL_EVENT_WINDOW_EXPOSED => return .tick,
- c.SDL_EVENT_KEY_DOWN => {
- const k = sev.key;
- self.live_mods = sdlMods(k.mod);
- return self.keyDown(k.key, k.mod);
- },
- c.SDL_EVENT_KEY_UP => {
- self.live_mods = sdlMods(sev.key.mod);
- return null;
- },
- c.SDL_EVENT_TEXT_INPUT => {
- const tptr: [*c]const u8 = sev.text.text;
- if (tptr == null) return null;
- var tlen: usize = 0;
- while (tptr[tlen] != 0) : (tlen += 1) {}
- const text: []const u8 = tptr[0..tlen];
- if (text.len == 0) return null;
- const len = std.unicode.utf8ByteSequenceLength(text[0]) catch return null;
- if (len > text.len) return null;
- const cp = std.unicode.utf8Decode(text[0..len]) catch return null;
- return EventType{ .key_press = .{
- .codepoint = @intCast(cp),
- .text = text[0..len],
- .mods = self.live_mods,
- } };
- },
- c.SDL_EVENT_MOUSE_BUTTON_DOWN, c.SDL_EVENT_MOUSE_BUTTON_UP => {
- const b = sev.button;
- const t: vaxis.Mouse.Type = if (b.down) .press else .release;
- const button = mouseButton(b.button) orelse return null;
- const cell = self.vx.mouseCell(b.x, b.y);
- return self.mouseEvent(cell, button, t);
- },
- c.SDL_EVENT_MOUSE_MOTION => {
- const m = sev.motion;
- // drag = motion with a button held; the app keys selection
- // extension off .drag (it never reads .motion).
- const held = heldButton(m.state);
- const t: vaxis.Mouse.Type = if (held != null) .drag else .motion;
- const button = held orelse .none;
- const cell = self.vx.mouseCell(m.x, m.y);
- return self.mouseEvent(cell, button, t);
- },
- c.SDL_EVENT_MOUSE_WHEEL => {
- const w = sev.wheel;
- const dy = w.y;
- const button: vaxis.Mouse.Button = if (dy > 0) .wheel_up else if (dy < 0) .wheel_down else return null;
- const cell = self.vx.mouseCell(w.mouse_x, w.mouse_y);
- return self.mouseEvent(cell, button, .press);
- },
- c.SDL_EVENT_FINGER_DOWN, c.SDL_EVENT_FINGER_MOTION, c.SDL_EVENT_FINGER_UP, c.SDL_EVENT_FINGER_CANCELED => {
- const f = sev.tfinger;
- switch (f.type) {
- c.SDL_EVENT_FINGER_DOWN => {
- self.vx.updateTouch(f);
- self.vx.syncTouchScrollPair();
- return .tick;
- },
- c.SDL_EVENT_FINGER_MOTION => {
- const had_pair = self.vx.touch_scroll.active;
- self.vx.updateTouch(f);
- self.vx.syncTouchScrollPair();
- if (had_pair) {
- if (self.vx.noteTouchScrollMotion(f)) |scroll| {
- const point = self.vx.fingerMousePoint(scroll.center.x, scroll.center.y);
- const cell = self.vx.mouseCell(point.x, point.y);
- const button: vaxis.Mouse.Button = if (scroll.ticks > 0) .wheel_up else .wheel_down;
- var remaining: u32 = @intCast(if (scroll.ticks > 0) scroll.ticks else -scroll.ticks);
- while (remaining > 0) : (remaining -= 1) {
- try self.postEvent(self.mouseEvent(cell, button, .press));
- }
- try self.postEvent(.{ .touch_scroll_delta = scroll.delta_y });
- return null;
- }
- }
- return .tick;
- },
- c.SDL_EVENT_FINGER_UP, c.SDL_EVENT_FINGER_CANCELED => {
- const tap_center = self.vx.finishTouchScrollPair(f);
- self.vx.updateTouch(f);
- self.vx.syncTouchScrollPair();
- if (tap_center) |center| {
- self.vx.noteTouchMiddleClick(true);
- const point = self.vx.fingerMousePoint(center.x, center.y);
- const cell = self.vx.mouseCell(point.x, point.y);
- try self.postEvent(self.mouseEvent(cell, .middle, .press));
- self.vx.noteTouchMiddleClick(false);
- try self.postEvent(self.mouseEvent(cell, .middle, .release));
- return null;
- }
- return .tick;
- },
- else => 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,
- }
- }
-
- // KEY_DOWN: special keys + ctrl/alt shortcuts. Plain printable keys
- // arrive as SDL_TEXT_INPUT instead (so shift/layout map correctly).
- fn keyDown(self: *Self, sym: c.SDL_Keycode, mod: c.SDL_Keymod) ?EventType {
- const mods = sdlMods(mod);
- const cp: u21 = switch (sym) {
- c.SDLK_RETURN => vaxis.Key.enter,
- c.SDLK_KP_ENTER => vaxis.Key.enter,
- c.SDLK_BACKSPACE => vaxis.Key.backspace,
- c.SDLK_TAB => vaxis.Key.tab,
- c.SDLK_ESCAPE => vaxis.Key.escape,
- c.SDLK_SPACE => ' ',
- c.SDLK_LEFT => vaxis.Key.left,
- c.SDLK_RIGHT => vaxis.Key.right,
- c.SDLK_UP => vaxis.Key.up,
- c.SDLK_DOWN => vaxis.Key.down,
- c.SDLK_HOME => vaxis.Key.home,
- c.SDLK_END => vaxis.Key.end,
- c.SDLK_PAGEUP => vaxis.Key.page_up,
- c.SDLK_PAGEDOWN => vaxis.Key.page_down,
- c.SDLK_INSERT => vaxis.Key.insert,
- c.SDLK_DELETE => vaxis.Key.delete,
- c.SDLK_F1 => vaxis.Key.f1,
- c.SDLK_F2 => vaxis.Key.f2,
- c.SDLK_F3 => vaxis.Key.f3,
- c.SDLK_F4 => vaxis.Key.f4,
- c.SDLK_F5 => vaxis.Key.f5,
- c.SDLK_F6 => vaxis.Key.f6,
- c.SDLK_F7 => vaxis.Key.f7,
- c.SDLK_F8 => vaxis.Key.f8,
- c.SDLK_F9 => vaxis.Key.f9,
- c.SDLK_F10 => vaxis.Key.f10,
- c.SDLK_F11 => vaxis.Key.f11,
- c.SDLK_F12 => vaxis.Key.f12,
- else => blk: {
- // letters / digits / punctuation: only emit on a modifier
- // shortcut (ctrl/alt/gui); plain printable goes via TEXT_INPUT.
- const has_mod = (mod & (c.SDL_KMOD_CTRL | c.SDL_KMOD_ALT | c.SDL_KMOD_GUI)) != 0;
- if (!has_mod) break :blk 0;
- if (sym >= 'a' and sym <= 'z') break :blk @intCast(sym);
- if (sym >= '0' and sym <= '9') break :blk @intCast(sym);
- if (sym < 128) break :blk @intCast(sym);
- break :blk 0;
- },
- };
- if (cp == 0) return null;
- _ = self;
- return EventType{ .key_press = .{ .codepoint = cp, .mods = mods } };
- }
-
- // ---- test-mode stdin reader ----
- // Parses terminal escape sequences from fd 0 (the pty slave) into the
- // app's Event union and posts them to the queue. This mirrors vaxis's
- // ttyRun while also recognizing private PARDES_TEST SDL event OSCs.
- fn stdinRun(self: *Self) void {
- self.stdinRunTyped() catch {};
- }
-
- fn stdinRunTyped(self: *Self) !void {
- // post the initial winsize (read from stdin)
- self.postWinsize();
-
- var parser: vaxis.Parser = .{};
- var cache: vaxis.GraphemeCache = .{};
- var buf: [1024]u8 = undefined;
- var read_start: usize = 0;
-
- while (!self.should_quit) {
- // check for a pending SIGWINCH (flag set by the signal handler)
- if (g_winsize_dirty) {
- g_winsize_dirty = false;
- self.postWinsize();
- }
-
- var fds = [_]posix.pollfd{.{ .fd = 0, .events = posix.POLL.IN, .revents = 0 }};
- _ = posix.poll(&fds, 50) catch continue;
- if ((fds[0].revents & posix.POLL.IN) == 0) continue;
-
- const n = readStdin(buf[read_start..]) catch break;
- if (n == 0) break;
- const len: usize = read_start + n;
- var seq_start: usize = 0;
- while (seq_start < len) {
- switch (try self.parseTestSdlEvent(buf[seq_start..len])) {
- .no_match => {},
- .need_more => {
- const initial = seq_start;
- while (seq_start < len) : (seq_start += 1) {
- buf[seq_start - initial] = buf[seq_start];
- }
- read_start = len - initial;
- break;
- },
- .consumed => |consumed| {
- read_start = 0;
- seq_start += consumed;
- continue;
- },
- }
-
- const result = parser.parse(buf[seq_start..len], null) catch break;
- if (result.n == 0) {
- // partial sequence — shift to front of buffer
- const initial = seq_start;
- while (seq_start < len) : (seq_start += 1) {
- buf[seq_start - initial] = buf[seq_start];
- }
- read_start = len - initial;
- break;
- }
- read_start = 0;
- seq_start += result.n;
- const event = result.event orelse continue;
- // convert vaxis.Event → app EventType and post
- switch (event) {
- .key_press => |key| {
- var mut_key = key;
- if (key.text) |text| mut_key.text = cache.put(text);
- try self.postEvent(.{ .key_press = mut_key });
- },
- .mouse => |mouse| try self.postEvent(.{ .mouse = mouse }),
- .winsize => |ws| try self.postEvent(.{ .winsize = ws }),
- .paste => |text| {
- try self.postEvent(.{ .paste = text });
- },
- else => {},
- }
- }
- }
- }
-
- const TestSdlParse = union(enum) {
- no_match,
- need_more,
- consumed: usize,
- };
-
- // Private OSC used only by PARDES_TEST:
- // ESC ] 777;finger;SDL_EVENT_FINGER_DOWN;<id>;<x>;<y>;<pressure> BEL
- // x/y/pressure use SDL's normalized 0..1 touch coordinates.
- fn parseTestSdlEvent(self: *Self, bytes: []const u8) !TestSdlParse {
- const prefix = "\x1b]777;finger;";
- if (!std.mem.startsWith(u8, bytes, prefix)) {
- if (bytes.len >= 2 and bytes.len < prefix.len and std.mem.eql(u8, bytes, prefix[0..bytes.len])) {
- return .need_more;
- }
- return .no_match;
- }
-
- const body = bytes[prefix.len..];
- const end = std.mem.indexOfScalar(u8, body, '\x07') orelse return .need_more;
- try self.postTestFingerEvent(body[0..end]);
- return .{ .consumed = prefix.len + end + 1 };
- }
-
- fn postTestFingerEvent(self: *Self, payload: []const u8) !void {
- var parts = std.mem.splitScalar(u8, payload, ';');
- const type_name = parts.next() orelse return error.BadTestSdlEvent;
- const finger_id_s = parts.next() orelse return error.BadTestSdlEvent;
- const x_s = parts.next() orelse return error.BadTestSdlEvent;
- const y_s = parts.next() orelse return error.BadTestSdlEvent;
- const pressure_s = parts.next() orelse return error.BadTestSdlEvent;
- if (parts.next() != null) return error.BadTestSdlEvent;
-
- const event_type = testFingerEventType(type_name) orelse return error.BadTestSdlEvent;
- const finger_id = try std.fmt.parseInt(u64, finger_id_s, 10);
- const x = try std.fmt.parseFloat(f32, x_s);
- const y = try std.fmt.parseFloat(f32, y_s);
- const pressure = try std.fmt.parseFloat(f32, pressure_s);
-
- var sev = std.mem.zeroes(c.SDL_Event);
- sev.type = event_type;
- sev.tfinger = .{
- .type = event_type,
- .reserved = 0,
- .timestamp = 0,
- .touchID = 1,
- .fingerID = finger_id,
- .x = x,
- .y = y,
- .dx = 0,
- .dy = 0,
- .pressure = pressure,
- .windowID = c.SDL_GetWindowID(self.vx.sdl_window),
- };
- if (try self.dispatch(&sev)) |event| try self.postEvent(event);
- }
-
- fn testFingerEventType(name: []const u8) ?c.SDL_EventType {
- if (std.mem.eql(u8, name, "SDL_EVENT_FINGER_DOWN") or std.mem.eql(u8, name, "down")) return c.SDL_EVENT_FINGER_DOWN;
- if (std.mem.eql(u8, name, "SDL_EVENT_FINGER_MOTION") or std.mem.eql(u8, name, "motion")) return c.SDL_EVENT_FINGER_MOTION;
- if (std.mem.eql(u8, name, "SDL_EVENT_FINGER_UP") or std.mem.eql(u8, name, "up")) return c.SDL_EVENT_FINGER_UP;
- if (std.mem.eql(u8, name, "SDL_EVENT_FINGER_CANCELED") or std.mem.eql(u8, name, "cancel")) return c.SDL_EVENT_FINGER_CANCELED;
- return null;
- }
- };
-}
-
-fn setStdinRaw() !void {
- var term = try posix.tcgetattr(0);
- term.iflag.BRKINT = false;
- term.iflag.ICRNL = false;
- term.iflag.INPCK = false;
- term.iflag.ISTRIP = false;
- term.iflag.IXON = false;
- term.oflag.OPOST = false;
- term.lflag.ECHO = false;
- term.lflag.ICANON = false;
- term.lflag.IEXTEN = false;
- term.lflag.ISIG = false;
- term.cc[@intFromEnum(posix.V.MIN)] = 1;
- term.cc[@intFromEnum(posix.V.TIME)] = 0;
- try posix.tcsetattr(0, .NOW, term);
-}
-
-fn ioctlReq(bits: u32) c_int {
- return @bitCast(bits);
-}
-
-fn tiocgwinszReq() c_int {
- return switch (os_tag) {
- .linux => @intCast(posix.T.IOCGWINSZ),
- .macos, .freebsd, .openbsd, .netbsd, .dragonfly => ioctlReq(0x40087468),
- else => @intCast(posix.T.IOCGWINSZ),
- };
-}
-
-fn readStdin(buf: []u8) !usize {
- switch (os_tag) {
- .linux => {
- while (true) {
- const rc = linux.read(0, buf.ptr, buf.len);
- const n: isize = @bitCast(rc);
- if (n < 0) {
- if (n == -@as(isize, @intFromEnum(linux.E.INTR))) continue;
- return error.ReadFailed;
- }
- return @intCast(n);
- }
- },
- else => return posix.read(0, buf),
- }
-}
-
-// ---- test-mode SIGWINCH handler ----
-// Signal handlers can't take closures, so we just set a flag; the stdin reader
-// (SdlLoop.stdinRunTyped) checks it and reads + posts the new winsize.
-var g_winsize_dirty: bool = false;
-
-fn winchHandler(_: posix.SIG) callconv(.c) void {
- g_winsize_dirty = true;
-}
-
-fn sdlMouseMods(km: vaxis.Key.Modifiers) vaxis.Mouse.Modifiers {
- return .{ .shift = km.shift, .alt = km.alt, .ctrl = km.ctrl };
-}
-
-fn sdlMods(mod: c.SDL_Keymod) vaxis.Key.Modifiers {
- return .{
- .shift = (mod & c.SDL_KMOD_SHIFT) != 0,
- .ctrl = (mod & c.SDL_KMOD_CTRL) != 0,
- .alt = (mod & c.SDL_KMOD_ALT) != 0,
- .super = (mod & c.SDL_KMOD_GUI) != 0,
- };
-}
-
-fn mouseButton(b: u8) ?vaxis.Mouse.Button {
- return switch (b) {
- 1 => .left,
- 2 => .middle,
- 3 => .right,
- else => null,
- };
-}
-
-// the SDL mouse button bitmask (`state` in a motion event) → the single button
-// being dragged with, if any. Left wins on a chord, matching the app's
-// "later buttons win" only loosely — good enough for selection drags.
-fn heldButton(state: u32) ?vaxis.Mouse.Button {
- if ((state & c.SDL_BUTTON_LMASK) != 0) return .left;
- if ((state & c.SDL_BUTTON_MMASK) != 0) return .middle;
- if ((state & c.SDL_BUTTON_RMASK) != 0) return .right;
- return null;
-}