From 5961587b227e5fa097fb033e32c29da08a23fb18 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sun, 2 Aug 2026 17:17:35 -0300 Subject: tty/image: big harness + golden coverage pass --- src/gui/gui.zig | 127 +++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 93 insertions(+), 34 deletions(-) (limited to 'src/gui/gui.zig') diff --git a/src/gui/gui.zig b/src/gui/gui.zig index c08c27fe..ef2422c5 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -198,10 +198,27 @@ const ImageInstance = extern struct { }; const NativeImage = struct { - serial: u32, + key: pardes.ImageCacheKey, texture: *c.SDL_GPUTexture, }; +fn updateCoreResize(core: *pardes.Pardes, cols: u16, rows: u16, cell_w: u32, cell_h: u32) void { + if (comptime pardes.pdf_enabled) { + const cell_px_w: u16 = @intCast(@min(@max(1, cell_w), std.math.maxInt(u16))); + const cell_px_h: u16 = @intCast(@min(@max(1, cell_h), std.math.maxInt(u16))); + if (cols == core.screen_w and rows == core.screen_h and + cell_px_w == core.cell_pixels.w and cell_px_h == core.cell_pixels.h) return; + core.update(.{ .resize = .{ + .cols = cols, + .rows = rows, + .cell_pixels = .{ .w = cell_px_w, .h = cell_px_h }, + } }); + } else { + if (cols == core.screen_w and rows == core.screen_h) return; + core.update(.{ .resize = .{ .cols = cols, .rows = rows } }); + } +} + // ---- touch: per-finger tracking + shared scroll/tap machines ---- const TouchSample = struct { x: f32 = 0, y: f32 = 0, pressure: f32 = 1 }; @@ -1303,8 +1320,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void { _ = c.SDL_GetWindowSizeInPixels(window, &pw, &ph); const cols: u16 = @intCast(@max(1, @divTrunc(@as(u32, @intCast(@max(pw, 1))), g.cell_w))); const rows: u16 = @intCast(@max(1, @divTrunc(@as(u32, @intCast(@max(ph, 1))), g.cell_h))); - if (cols != core.screen_w or rows != core.screen_h) - core.update(.{ .resize = .{ .cols = cols, .rows = rows } }); + updateCoreResize(core, cols, rows, g.cell_w, g.cell_h); // 7. render — the scroll animation steps LAST before it, while // core.surface still holds the frame the last pass drew stepScroll(&g, core, gpa); @@ -1588,8 +1604,7 @@ fn webFrame(arg: ?*anyopaque) callconv(.c) void { _ = c.SDL_GetWindowSizeInPixels(g.window, &pw, &ph); const cols: u16 = @intCast(@max(1, @divTrunc(@as(u32, @intCast(@max(pw, 1))), g.cell_w))); const rows: u16 = @intCast(@max(1, @divTrunc(@as(u32, @intCast(@max(ph, 1))), g.cell_h))); - if (cols != core.screen_w or rows != core.screen_h) - core.update(.{ .resize = .{ .cols = cols, .rows = rows } }); + updateCoreResize(core, cols, rows, g.cell_w, g.cell_h); // 4. render — scroll animation last before it, see runNative stepScroll(g, core, st.gpa); _ = st.frame_arena.reset(.retain_capacity); @@ -2762,11 +2777,42 @@ fn validImageBytes(place: pardes.ImagePlace) ?u32 { return @intCast(bytes); } +const NativeBounds = struct { w: u32, h: u32 }; + +fn nativeBounds(g: *const Gui, place: pardes.ImagePlace) ?NativeBounds { + const bound_w = std.math.mul(u32, place.w, g.cell_w) catch return null; + const bound_h = std.math.mul(u32, place.h, g.cell_h) catch return null; + return .{ .w = bound_w, .h = bound_h }; +} + +fn nativePlaceGeometry(g: *const Gui, place: pardes.ImagePlace) ?pardes.image.NativeGeometry { + if (comptime pardes.pdf_enabled) { + const bounds = nativeBounds(g, place) orelse return null; + return pardes.image.nativeGeometry( + place.iw, + place.ih, + bounds.w, + bounds.h, + place.native.fit, + place.native.pan_x, + place.native.pan_y, + ); + } + return null; +} + +fn nativePlaceDrawable(g: *const Gui, place: pardes.ImagePlace) bool { + if (comptime pardes.pdf_enabled) return nativePlaceGeometry(g, place) != null; + const bounds = nativeBounds(g, place) orelse return false; + const fit = pardes.image.contain(place.iw, place.ih, bounds.w, bounds.h); + return fit.w != 0 and fit.h != 0; +} + fn uploadNativeTexture(g: *Gui, cmd: *c.SDL_GPUCommandBuffer, place: pardes.ImagePlace) !void { const byte_len = validImageBytes(place) orelse return error.BadImage; const pane: usize = place.pane; if (g.native_images[pane]) |cached| { - if (cached.serial == place.serial) return; + if (cached.key.eql(place.cacheKey())) return; releaseNativeImage(g, pane); } @@ -2817,10 +2863,10 @@ fn uploadNativeTexture(g: *Gui, cmd: *c.SDL_GPUCommandBuffer, place: pardes.Imag // allocation is never needed again, so do not retain a second full image // beside the texture for the life of the pane. c.SDL_ReleaseGPUTransferBuffer(g.device, transfer); - g.native_images[pane] = .{ .serial = place.serial, .texture = texture }; + g.native_images[pane] = .{ .key = place.cacheKey(), .texture = texture }; } -/// Upload new pane generations and the small per-frame placement buffer. +/// Upload new pixel generations and the small per-frame placement buffer. /// Returns the number of image instances drawNativeImagesGpu will consume. fn prepareNativeImages( g: *Gui, @@ -2842,7 +2888,7 @@ fn prepareNativeImages( for (surface.images[0..surface.nimages]) |maybe| { const place = maybe orelse continue; if (g.native_images[place.pane]) |cached| { - if (cached.serial == place.serial) count += 1; + if (cached.key.eql(place.cacheKey()) and nativePlaceDrawable(g, place)) count += 1; } } if (count == 0) return 0; @@ -2855,28 +2901,41 @@ fn prepareNativeImages( for (surface.images[0..surface.nimages]) |maybe| { const place = maybe orelse continue; const cached = g.native_images[place.pane] orelse continue; - if (cached.serial != place.serial) continue; - - const bound_w = @as(u32, place.w) * g.cell_w; - const bound_h = @as(u32, place.h) * g.cell_h; - const fit = pardes.image.contain(place.iw, place.ih, bound_w, bound_h); - if (fit.w == 0 or fit.h == 0) continue; - const px0 = @as(u32, place.x) * g.cell_w; - const py0 = @as(u32, place.y) * g.cell_h; - const x0 = (@as(f32, @floatFromInt(px0)) / win_w) * 2.0 - 1.0; - const x1 = (@as(f32, @floatFromInt(px0 + fit.w)) / win_w) * 2.0 - 1.0; - const y0 = 1.0 - (@as(f32, @floatFromInt(py0)) / win_h) * 2.0; - const y1 = 1.0 - (@as(f32, @floatFromInt(py0 + fit.h)) / win_h) * 2.0; - instances[idx] = .{ - .x0 = x0, - .y0 = y0, - .x1 = x1, - .y1 = y1, - .u0 = 0, - .v0 = 0, - .u1 = 1, - .v1 = 1, - }; + if (!cached.key.eql(place.cacheKey())) continue; + + const body_x = @as(u32, place.x) * g.cell_w; + const body_y = @as(u32, place.y) * g.cell_h; + if (comptime pardes.pdf_enabled) { + const geometry = nativePlaceGeometry(g, place) orelse continue; + const px0 = body_x + geometry.dst.x; + const py0 = body_y + geometry.dst.y; + const image_w: f32 = @floatFromInt(place.iw); + const image_h: f32 = @floatFromInt(place.ih); + instances[idx] = .{ + .x0 = (@as(f32, @floatFromInt(px0)) / win_w) * 2.0 - 1.0, + .y0 = 1.0 - (@as(f32, @floatFromInt(py0)) / win_h) * 2.0, + .x1 = (@as(f32, @floatFromInt(px0 + geometry.dst.w)) / win_w) * 2.0 - 1.0, + .y1 = 1.0 - (@as(f32, @floatFromInt(py0 + geometry.dst.h)) / win_h) * 2.0, + .u0 = @as(f32, @floatFromInt(geometry.src.x)) / image_w, + .v0 = @as(f32, @floatFromInt(geometry.src.y)) / image_h, + .u1 = @as(f32, @floatFromInt(geometry.src.x + geometry.src.w)) / image_w, + .v1 = @as(f32, @floatFromInt(geometry.src.y + geometry.src.h)) / image_h, + }; + } else { + const bounds = nativeBounds(g, place) orelse continue; + const fit = pardes.image.contain(place.iw, place.ih, bounds.w, bounds.h); + if (fit.w == 0 or fit.h == 0) continue; + instances[idx] = .{ + .x0 = (@as(f32, @floatFromInt(body_x)) / win_w) * 2.0 - 1.0, + .y0 = 1.0 - (@as(f32, @floatFromInt(body_y)) / win_h) * 2.0, + .x1 = (@as(f32, @floatFromInt(body_x + fit.w)) / win_w) * 2.0 - 1.0, + .y1 = 1.0 - (@as(f32, @floatFromInt(body_y + fit.h)) / win_h) * 2.0, + .u0 = 0, + .v0 = 0, + .u1 = 1, + .v1 = 1, + }; + } idx += 1; } c.SDL_UnmapGPUTransferBuffer(g.device, g.image_vxfer); @@ -2897,7 +2956,8 @@ fn drawNativeImagesGpu(g: *Gui, rp: ?*c.SDL_GPURenderPass, surface: *pardes.Surf for (surface.images[0..surface.nimages]) |maybe| { const place = maybe orelse continue; const cached = g.native_images[place.pane] orelse continue; - if (cached.serial != place.serial) continue; + if (!cached.key.eql(place.cacheKey())) continue; + if (!nativePlaceDrawable(g, place)) continue; const sampler = c.SDL_GPUTextureSamplerBinding{ .texture = cached.texture, .sampler = g.atlas_sampler }; c.SDL_BindGPUFragmentSamplers(pass, 0, &sampler, 1); const binding = c.SDL_GPUBufferBinding{ .buffer = g.image_vbuf, .offset = idx * @sizeOf(ImageInstance) }; @@ -3190,8 +3250,7 @@ fn refitFont(g: *Gui, core: *pardes.Pardes) void { _ = c.SDL_GetWindowSizeInPixels(g.window, &pw, &ph); const cols: u16 = @intCast(@max(1, @divTrunc(@as(u32, @intCast(@max(pw, 1))), g.cell_w))); const rows: u16 = @intCast(@max(1, @divTrunc(@as(u32, @intCast(@max(ph, 1))), g.cell_h))); - if (cols != core.screen_w or rows != core.screen_h) - core.update(.{ .resize = .{ .cols = cols, .rows = rows } }); + updateCoreResize(core, cols, rows, g.cell_w, g.cell_h); // ...and a smooth scroll in flight is measured in the OLD grid: scroll_rect // is a rect of the pane the last frame drew, and scroll_edge is a saved row -- cgit v1.3