diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-05 20:21:50 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:07 -0300 |
| commit | de4def3548a6729b0dfd2120495a61beef8c8c2c (patch) | |
| tree | 12144f0f64dc96bb4741459c1f9db57f44349330 /ui_sdl.zig | |
| parent | 7988d9bc6e31210ff13994f18d5622dd57ba9341 (diff) | |
| download | pardes-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.zig | 2916 |
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; -} |
