summaryrefslogtreecommitdiff
path: root/src/gui
diff options
context:
space:
mode:
Diffstat (limited to 'src/gui')
-rw-r--r--src/gui/gui.zig174
1 files changed, 127 insertions, 47 deletions
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,