From ec19caddb60e748115adb562b46d007598dcdb95 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sun, 2 Aug 2026 21:28:49 -0300 Subject: render PDFs as a continuous vertical page strip --- src/gui/gui.zig | 174 +++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 127 insertions(+), 47 deletions(-) (limited to 'src/gui/gui.zig') diff --git a/src/gui/gui.zig b/src/gui/gui.zig index efba7bda..5744b3f2 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -191,10 +191,7 @@ const ImageInstance = extern struct { v1: f32, }; -const NativeImage = struct { - key: pardes.ImageCacheKey, - texture: *c.SDL_GPUTexture, -}; +const initial_image_capacity: u32 = pardes.MAX_PANES; fn updateCoreResize(core: *pardes.Pardes, cols: u16, rows: u16, cell_w: u32, cell_h: u32) bool { if (comptime pardes.pdf_enabled) { @@ -855,7 +852,8 @@ const Gui = struct { overlay_vertices: []OverlayVertex, image_vbuf: *c.SDL_GPUBuffer, image_vxfer: *c.SDL_GPUTransferBuffer, - native_images: [pardes.MAX_PANES]?NativeImage = @splat(null), + image_capacity: u32 = 0, + native_images: std.AutoHashMapUnmanaged(pardes.ImageCacheKey, *c.SDL_GPUTexture) = .empty, font: *c.UIFont, /// the file behind `font`, when it is one the Font builtin loaded. Empty @@ -1095,7 +1093,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void { 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 image_buffer_size: u32 = @intCast(pardes.MAX_PANES * @sizeOf(ImageInstance)); + const image_buffer_size: u32 = initial_image_capacity * @sizeOf(ImageInstance); var ivb_info = c.SDL_GPUBufferCreateInfo{ .usage = c.SDL_GPU_BUFFERUSAGE_VERTEX, .size = image_buffer_size, .props = 0 }; const image_vbuf = c.SDL_CreateGPUBuffer(device, &ivb_info) orelse return error.GpuCreate; var ivx_info = c.SDL_GPUTransferBufferCreateInfo{ .usage = c.SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD, .size = image_buffer_size, .props = 0 }; @@ -1123,6 +1121,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void { .overlay_vertices = overlay_vertices, .image_vbuf = image_vbuf, .image_vxfer = image_vxfer, + .image_capacity = initial_image_capacity, .font = font, .px = px, .scale = scale, @@ -1135,9 +1134,12 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void { .capture_dir = capture_dir orelse "", }; defer g.glyphs.deinit(); - defer clearNativeImages(&g); - defer c.SDL_ReleaseGPUTransferBuffer(device, image_vxfer); - defer c.SDL_ReleaseGPUBuffer(device, image_vbuf); + defer { + clearNativeImages(&g); + g.native_images.deinit(gpa); + } + defer c.SDL_ReleaseGPUTransferBuffer(device, g.image_vxfer); + defer c.SDL_ReleaseGPUBuffer(device, g.image_vbuf); defer c.SDL_ReleaseGPUGraphicsPipeline(device, image_pipeline); defer if (g.font_bytes.len != 0) gpa.free(g.font_bytes); // set by Font, if it ran defer gpa.free(g.scroll_edge); // grown on demand by stepScroll @@ -2102,10 +2104,25 @@ fn dispatch(g: *Gui, core: *pardes.Pardes, sev: *const c.SDL_Event) void { resetScroll(g); }; if (hit) |id| { - g.scroll_pane = id; - g.scroll_col = mc.col; - g.scroll_row = mc.row; - g.scroll_delta = accumulateWheelDelta(g.scroll_delta, w.y); + const pdf_target = if (comptime pardes.pdf_enabled) + core.native_images and core.panes[id].?.pdfPage() != null + else + false; + if (pdf_target) { + // PDF placements live in physical document space, so + // preserve SDL's raw magnitude directly instead of + // quantizing through synthetic wheel buttons/rows. + resetScroll(g); + core.update(.{ .pdf_scroll = .{ + .pane = @intCast(id), + .delta_pixels = -w.y * @as(f32, @floatFromInt(g.cell_h)), + } }); + } else { + g.scroll_pane = id; + g.scroll_col = mc.col; + g.scroll_row = mc.row; + g.scroll_delta = accumulateWheelDelta(g.scroll_delta, w.y); + } } } if (w.x != 0) core.update(.{ .mouse = .{ @@ -2798,15 +2815,56 @@ fn scrollScissor(g: *const Gui, layout: CellLayout, sw: u32, sh: u32) c.SDL_Rect // render: Surface → instanced quads → SDL GPU // ===================================================================== -fn releaseNativeImage(g: *Gui, pane: usize) void { - if (g.native_images[pane]) |cached| { - c.SDL_ReleaseGPUTexture(g.device, cached.texture); - g.native_images[pane] = null; - } +fn releaseNativeImage(g: *Gui, key: pardes.ImageCacheKey) void { + if (g.native_images.fetchRemove(key)) |removed| + c.SDL_ReleaseGPUTexture(g.device, removed.value); } fn clearNativeImages(g: *Gui) void { - for (0..g.native_images.len) |pane| releaseNativeImage(g, pane); + var iterator = g.native_images.valueIterator(); + while (iterator.next()) |texture| c.SDL_ReleaseGPUTexture(g.device, texture.*); + g.native_images.clearRetainingCapacity(); +} + +fn surfaceHasNativeKey(surface: *const pardes.Surface, key: pardes.ImageCacheKey) bool { + for (surface.images[0..surface.nimages]) |maybe| { + const place = maybe orelse continue; + if (validImageBytes(place) != null and place.cacheKey().eql(key)) return true; + } + return false; +} + +fn ensureImageBuffers(g: *Gui, needed: u32) bool { + if (needed <= g.image_capacity) return true; + var capacity = @max(@as(u32, 1), g.image_capacity); + while (capacity < needed) + capacity = std.math.mul(u32, capacity, 2) catch return false; + const buffer_size = std.math.mul( + u32, + capacity, + @as(u32, @intCast(@sizeOf(ImageInstance))), + ) catch return false; + var vb_info = c.SDL_GPUBufferCreateInfo{ + .usage = c.SDL_GPU_BUFFERUSAGE_VERTEX, + .size = buffer_size, + .props = 0, + }; + const vbuf = c.SDL_CreateGPUBuffer(g.device, &vb_info) orelse return false; + var vx_info = c.SDL_GPUTransferBufferCreateInfo{ + .usage = c.SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD, + .size = buffer_size, + .props = 0, + }; + const vxfer = c.SDL_CreateGPUTransferBuffer(g.device, &vx_info) orelse { + c.SDL_ReleaseGPUBuffer(g.device, vbuf); + return false; + }; + c.SDL_ReleaseGPUTransferBuffer(g.device, g.image_vxfer); + c.SDL_ReleaseGPUBuffer(g.device, g.image_vbuf); + g.image_vbuf = vbuf; + g.image_vxfer = vxfer; + g.image_capacity = capacity; + return true; } fn validImageBytes(place: pardes.ImagePlace) ?u32 { @@ -2828,6 +2886,7 @@ fn nativeBounds(g: *const Gui, place: pardes.ImagePlace) ?NativeBounds { fn nativePlaceGeometry(g: *const Gui, place: pardes.ImagePlace) ?pardes.image.NativeGeometry { if (comptime pardes.pdf_enabled) { + if (place.native.geometry) |geometry| return geometry; const bounds = nativeBounds(g, place) orelse return null; return pardes.image.nativeGeometry( place.iw, @@ -2849,13 +2908,15 @@ fn nativePlaceDrawable(g: *const Gui, place: pardes.ImagePlace) bool { return fit.w != 0 and fit.h != 0; } -fn uploadNativeTexture(g: *Gui, cmd: *c.SDL_GPUCommandBuffer, place: pardes.ImagePlace) !void { +fn uploadNativeTexture( + g: *Gui, + gpa: std.mem.Allocator, + 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.key.eql(place.cacheKey())) return; - releaseNativeImage(g, pane); - } + const key = place.cacheKey(); + if (g.native_images.contains(key)) return; var tex_info = std.mem.zeroes(c.SDL_GPUTextureCreateInfo); tex_info.type = c.SDL_GPU_TEXTURETYPE_2D; @@ -2875,7 +2936,7 @@ fn uploadNativeTexture(g: *Gui, cmd: *c.SDL_GPUCommandBuffer, place: pardes.Imag .props = 0, }; const transfer = c.SDL_CreateGPUTransferBuffer(g.device, &xf_info) orelse return error.GpuCreate; - errdefer c.SDL_ReleaseGPUTransferBuffer(g.device, transfer); + defer c.SDL_ReleaseGPUTransferBuffer(g.device, transfer); const mapped: [*]u8 = @ptrCast(c.SDL_MapGPUTransferBuffer(g.device, transfer, false) orelse return error.GpuMap); @memcpy(mapped[0..byte_len], place.rgba); c.SDL_UnmapGPUTransferBuffer(g.device, transfer); @@ -2903,36 +2964,40 @@ fn uploadNativeTexture(g: *Gui, cmd: *c.SDL_GPUCommandBuffer, place: pardes.Imag // SDL defers destruction until the submitted copy is done; the staging // 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] = .{ .key = place.cacheKey(), .texture = texture }; + try g.native_images.put(gpa, key, texture); } /// Upload new pixel generations and the small per-frame placement buffer. /// Returns the number of image instances drawNativeImagesGpu will consume. fn prepareNativeImages( g: *Gui, + gpa: std.mem.Allocator, cmd: *c.SDL_GPUCommandBuffer, surface: *pardes.Surface, sw: u32, sh: u32, ) u32 { - var seen: [pardes.MAX_PANES]bool = @splat(false); for (surface.images[0..surface.nimages]) |maybe| { const place = maybe orelse continue; if (validImageBytes(place) == null) continue; - seen[place.pane] = true; - uploadNativeTexture(g, cmd, place) catch continue; + uploadNativeTexture(g, gpa, cmd, place) catch continue; } - for (seen, 0..) |is_seen, pane| if (!is_seen) releaseNativeImage(g, pane); + + var stale: std.ArrayList(pardes.ImageCacheKey) = .empty; + defer stale.deinit(gpa); + var iterator = g.native_images.iterator(); + while (iterator.next()) |entry| + if (!surfaceHasNativeKey(surface, entry.key_ptr.*)) + stale.append(gpa, entry.key_ptr.*) catch {}; + for (stale.items) |key| releaseNativeImage(g, key); var count: u32 = 0; for (surface.images[0..surface.nimages]) |maybe| { const place = maybe orelse continue; - if (g.native_images[place.pane]) |cached| { - if (cached.key.eql(place.cacheKey()) and nativePlaceDrawable(g, place)) count += 1; - } + if (g.native_images.contains(place.cacheKey()) and nativePlaceDrawable(g, place)) count += 1; } if (count == 0) return 0; + if (!ensureImageBuffers(g, count)) return 0; const ptr: [*]u8 = @ptrCast(c.SDL_MapGPUTransferBuffer(g.device, g.image_vxfer, false) orelse return 0); const instances: [*]ImageInstance = @ptrCast(@alignCast(ptr)); @@ -2941,22 +3006,24 @@ fn prepareNativeImages( var idx: u32 = 0; for (surface.images[0..surface.nimages]) |maybe| { const place = maybe orelse continue; - const cached = g.native_images[place.pane] orelse continue; - if (!cached.key.eql(place.cacheKey())) continue; + if (!g.native_images.contains(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 px0 = @as(f32, @floatFromInt(body_x + geometry.dst.x)); + const py0 = @as(f32, @floatFromInt(body_y + geometry.dst.y)) + + place.native.pixel_offset_y; + const px1 = px0 + @as(f32, @floatFromInt(geometry.dst.w)); + const py1 = py0 + @as(f32, @floatFromInt(geometry.dst.h)); 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, + .x0 = (px0 / win_w) * 2.0 - 1.0, + .y0 = 1.0 - (py0 / win_h) * 2.0, + .x1 = (px1 / win_w) * 2.0 - 1.0, + .y1 = 1.0 - (py1 / 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, @@ -2996,16 +3063,29 @@ fn drawNativeImagesGpu(g: *Gui, rp: ?*c.SDL_GPURenderPass, surface: *pardes.Surf var idx: u32 = 0; for (surface.images[0..surface.nimages]) |maybe| { const place = maybe orelse continue; - const cached = g.native_images[place.pane] orelse continue; - if (!cached.key.eql(place.cacheKey())) continue; + const texture = g.native_images.get(place.cacheKey()) orelse continue; if (!nativePlaceDrawable(g, place)) continue; - const sampler = c.SDL_GPUTextureSamplerBinding{ .texture = cached.texture, .sampler = g.atlas_sampler }; + const clip = c.SDL_Rect{ + .x = @intCast(@as(u32, place.x) * g.cell_w), + .y = @intCast(@as(u32, place.y) * g.cell_h), + .w = @intCast(@as(u32, place.w) * g.cell_w), + .h = @intCast(@as(u32, place.h) * g.cell_h), + }; + c.SDL_SetGPUScissor(pass, &clip); + const sampler = c.SDL_GPUTextureSamplerBinding{ .texture = 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) }; c.SDL_BindGPUVertexBuffers(pass, 0, &binding, 1); c.SDL_DrawGPUPrimitives(pass, 6, 1, 0, 0); idx += 1; } + const whole = c.SDL_Rect{ + .x = 0, + .y = 0, + .w = @intCast(@as(u32, surface.cols) * g.cell_w), + .h = @intCast(@as(u32, surface.rows) * g.cell_h), + }; + c.SDL_SetGPUScissor(pass, &whole); } fn renderFrame(g: *Gui, gpa: std.mem.Allocator, surface: *pardes.Surface, crt_on: bool, debug_on: bool) !void { @@ -3044,7 +3124,7 @@ fn renderFrame(g: *Gui, gpa: std.mem.Allocator, surface: *pardes.Surface, crt_on const overlay_count = buildTouchOverlay(g, sw, sh, debug_on); if (overlay_count != 0) uploadOverlayGpu(g, cmd, overlay_count); - const image_count = prepareNativeImages(g, cmd, surface, sw, sh); + const image_count = prepareNativeImages(g, gpa, cmd, surface, sw, sh); const cells: u32 = @as(u32, surface.cols) * surface.rows; // ponytail: ×2, because the fractional-scroll pane is drawn a second time, -- cgit v1.3