summaryrefslogtreecommitdiff
path: root/src/pdf_view.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pdf_view.zig')
-rw-r--r--src/pdf_view.zig175
1 files changed, 168 insertions, 7 deletions
diff --git a/src/pdf_view.zig b/src/pdf_view.zig
index 6c137202..09c06356 100644
--- a/src/pdf_view.zig
+++ b/src/pdf_view.zig
@@ -39,6 +39,20 @@ const raster_max = 256;
const raster_spare = 4;
pub const page_gap_px: u32 = 8;
const band_grain: usize = 64;
+/// The tallest raster one buffer carries: within every GPU's texture size
+/// and Kitty's 10000-pixel image limit (kitty and ghostty both). A page
+/// taller than this, or than `pdf.max_owned_raster_bytes` allows, is BANDED:
+/// it is held as the rows around what is on screen and rendered in chunks.
+const max_band_rows: usize = 8192;
+/// A banded page is rendered in chunks of this many rows, each from its own
+/// render of the chunk and `chunk_overlap` rows of the page beyond both of
+/// its edges, which are thrown away. A row of the page therefore always comes
+/// from the same render, however the reader scrolled to it, and never from
+/// the edge of one: MuPDF's rows at the edge of a clip can differ from the
+/// same rows rendered whole (see `pardes_pdf_render_into`), which, butted
+/// against the next chunk, would be a seam.
+const chunk_rows: usize = 512;
+const chunk_overlap: usize = 32;
pub const FitMode = if (enabled) enum { width, height } else void;
pub const TintMode = if (enabled) pdf.TintMode else void;
@@ -802,13 +816,18 @@ const VisibleRows = struct {
pixel_offset_y: f32,
};
-pub fn renderRequest(viewport: Viewport, policy: RasterPolicy) RenderRequest {
+/// The raster a pane asks for. The page is shown `viewport.pixel_w` wide
+/// (fit width) or `pixel_h` tall (fit height), and the raster is never
+/// smaller than that, however tall the page: see `pdf.RenderRequest`.
+pub fn renderRequest(viewport: Viewport, policy: RasterPolicy, fit: FitMode) RenderRequest {
if (comptime !enabled) return;
return .{
.dpi = policy.dpi,
.minimum_width = if (policy.match_viewport) viewport.pixel_w else 0,
.minimum_height = if (policy.match_viewport) viewport.pixel_h else 0,
.max_dimension = policy.max_dimension,
+ .display_width = if (fit == .width) viewport.pixel_w else 0,
+ .display_height = if (fit == .height) viewport.pixel_h else 0,
};
}
@@ -1052,13 +1071,34 @@ fn flinging(state: *const State, viewport: Viewport) bool {
return state.scroll_travel >= @as(f64, @floatFromInt(viewport.pixel_h));
}
+/// The most rows one buffer of this page holds.
+fn bandCap(shape: pdf.Raster) usize {
+ return @min(max_band_rows, pdf.max_owned_raster_bytes / @max(shape.stride, 1));
+}
+
+/// A page too tall for one buffer: see `max_band_rows`.
+pub fn isBanded(shape: pdf.Raster) bool {
+ if (comptime !enabled) return false;
+ return shape.height > bandCap(shape);
+}
+
+/// What a banded page's buffer is asked to hold. `target` is what a render
+/// covers: the rows on screen and a screenful either side, so scrolling finds
+/// the next rows ready (none while flinging, which only wants this frame's).
+/// `keep` is what the buffer must still cover to be kept: half a screenful
+/// either side, so a render happens once per half screenful scrolled, not on
+/// every frame.
+const Reach = enum { target, keep };
+
fn wantedBand(
state: *const State,
viewport: Viewport,
page: usize,
shape: pdf.Raster,
is_flinging: bool,
+ reach: Reach,
) pdf.Raster.Band {
+ if (isBanded(shape)) return chunkBand(state, viewport, page, shape, is_flinging, reach);
if (!is_flinging) return shape.wholePage();
const rows = visibleRows(state, viewport, page, shape.width, shape.height) orelse
return shape.wholePage();
@@ -1068,6 +1108,117 @@ fn wantedBand(
return shape.band(first, last * band_grain - first);
}
+fn chunkBand(
+ state: *const State,
+ viewport: Viewport,
+ page: usize,
+ shape: pdf.Raster,
+ is_flinging: bool,
+ reach: Reach,
+) pdf.Raster.Band {
+ const rows = visibleRows(state, viewport, page, shape.width, shape.height) orelse
+ return shape.band(0, chunk_rows);
+ const seen: usize = rows.y1 - rows.y0;
+ const margin: usize = if (is_flinging) 0 else switch (reach) {
+ .target => seen,
+ .keep => seen / 2,
+ };
+ const top = @as(usize, rows.y0) -| margin;
+ const bottom = @min(shape.height, @as(usize, rows.y1) + margin);
+ if (reach == .keep) return shape.band(top, bottom - top);
+ var first = top / chunk_rows * chunk_rows;
+ var last = @min(shape.height, (bottom + chunk_rows - 1) / chunk_rows * chunk_rows);
+ const cap = bandCap(shape) / chunk_rows * chunk_rows;
+ if (last - first > cap) {
+ // The margins give way: what is on screen always fits.
+ first = @as(usize, rows.y0) / chunk_rows * chunk_rows;
+ last = @min(shape.height, first + cap);
+ }
+ return shape.band(first, last - first);
+}
+
+/// Fill `fresh` with the rows `want` of a banded page. Chunks the slot
+/// already holds, under this request and tint and with these highlights
+/// (`reuse`), are copied; the rest are rendered (`renderChunk`), then
+/// highlighted and tinted as `reconcile` does a whole page. `slot.clean`
+/// becomes the new rows' clean copy when `keep_clean`.
+fn fillBanded(
+ state: *State,
+ gpa: std.mem.Allocator,
+ slot: *const Raster,
+ request: RenderRequest,
+ shape: pdf.Raster,
+ want: pdf.Raster.Band,
+ highlights: []const Highlight,
+ tint_key: TintKey,
+ fresh: []u8,
+ keep_clean: bool,
+ reuse: bool,
+ clean_out: *[]u8,
+) bool {
+ const tz = tracy.zone(@src(), "pdf.fill_banded");
+ defer tz.end();
+ const stride = shape.stride;
+ const clean: []u8 = if (keep_clean) gpa.alloc(u8, fresh.len) catch return false else &.{};
+ var filled = false;
+ defer if (!filled and clean.len > 0) gpa.free(clean);
+ const held = reuse and slot.rgba.len == slot.band_h * stride and
+ (!keep_clean or slot.clean.len == slot.rgba.len);
+ var scratch: []u8 = &.{};
+ defer if (scratch.len > 0) gpa.free(scratch);
+ const end = want.y + want.height;
+ var y = want.y;
+ while (y < end) {
+ const rows = @min(chunk_rows - y % chunk_rows, end - y);
+ const at = (y - want.y) * stride;
+ const out = fresh[at..][0 .. rows * stride];
+ if (held and slot.band_y <= y and y + rows <= slot.band_y + slot.band_h) {
+ const from = (y - slot.band_y) * stride;
+ @memcpy(out, slot.rgba[from..][0..out.len]);
+ if (keep_clean) @memcpy(clean[at..][0..out.len], slot.clean[from..][0..out.len]);
+ } else {
+ if (scratch.len == 0)
+ scratch = gpa.alloc(u8, (chunk_rows + 2 * chunk_overlap) * stride) catch return false;
+ const target = if (keep_clean) clean[at..][0..out.len] else out;
+ renderChunk(state, request, shape, slot.page, y, if (keep_clean) &.{} else highlights, target, scratch) catch
+ return false;
+ if (keep_clean) {
+ @memcpy(out, target);
+ state.document.paintHighlightsAt(slot.page, request, shape, shape.band(y, rows), highlights, out) catch
+ return false;
+ }
+ pdf.tintRgba(out, tint_key.mode, tint_key.colors) catch return false;
+ }
+ y += rows;
+ }
+ filled = true;
+ clean_out.* = clean;
+ return true;
+}
+
+/// The `out.len / stride` rows of a banded page from row `y` (a chunk or
+/// its end), cut from a render reaching `chunk_overlap` rows past both.
+fn renderChunk(
+ state: *State,
+ request: RenderRequest,
+ shape: pdf.Raster,
+ page: usize,
+ y: usize,
+ highlights: []const Highlight,
+ out: []u8,
+ scratch: []u8,
+) !void {
+ const tz = tracy.zone(@src(), "pdf.render_chunk");
+ defer tz.end();
+ const rows = out.len / shape.stride;
+ const top = y -| chunk_overlap;
+ const bottom = @min(shape.height, y + rows + chunk_overlap);
+ const band = shape.band(top, bottom - top);
+ const buf = scratch[0..band.len];
+ try state.document.renderIntoAtWithPaper(page, request, shape, band, highlights, buf, pardes.platform == .macos);
+ @memcpy(out, buf[(y - top) * shape.stride ..][0..out.len]);
+}
+
fn reconcile(
state: *State,
gpa: std.mem.Allocator,
@@ -1113,10 +1264,13 @@ fn reconcile(
slot.tint_key == null or !slot.tint_key.?.eql(tint_key) or
slot.rgba.len == 0 or slot.band_h == 0;
const uncovered = !base_stale and uncovered: {
- const want = wantedBand(state, viewport, page, slotShape(slot), is_flinging);
+ const want = wantedBand(state, viewport, page, slotShape(slot), is_flinging, .keep);
break :uncovered slot.band_y > want.y or
slot.band_y + slot.band_h < want.y + want.height;
};
+ // A banded page moving on keeps the rows it has, if they are exactly
+ // what a render would make now.
+ const reuse = !base_stale and slot.baked_valid and sameHighlights(slot.baked, page_highlights);
if (!base_stale and !uncovered) {
if (slot.tried and slot.decorated == decorated and
!(highlights.live and !sameHighlights(slot.baked, page_highlights))) continue;
@@ -1140,14 +1294,21 @@ fn reconcile(
break :shape state.document.measureRenderAt(page, request) catch null;
};
const shape = shape_or_null orelse continue;
- const want = wantedBand(state, viewport, page, shape, is_flinging);
+ const want = wantedBand(state, viewport, page, shape, is_flinging, .target);
const fresh = state.claimRgba(gpa, want.len) orelse continue;
- const filled = filled: {
+ // A highlighted page keeps its clean rows for repaints.
+ const keep_clean = pardes.platform != .macos and (decorated or slot.clean.len > 0);
+ const filled = if (isBanded(shape)) banded: {
+ var clean: []u8 = &.{};
+ if (!fillBanded(state, gpa, slot, request, shape, want, page_highlights, tint_key, fresh, keep_clean, reuse, &clean))
+ break :banded false;
+ if (slot.clean.len > 0) gpa.free(slot.clean);
+ slot.clean = clean;
+ break :banded true;
+ } else filled: {
{
const tz_render = tracy.zone(@src(), "pdf.render_into");
defer tz_render.end();
- // A highlighted page keeps its clean rows for repaints.
- const keep_clean = pardes.platform != .macos and (decorated or slot.clean.len > 0);
state.document.renderIntoAtWithPaper(
page,
request,
@@ -1330,7 +1491,7 @@ pub fn renderFrame(
ensureLayout(state, viewport);
state.resolveSearch(gpa);
const highlights = buildHighlights(state, arena, highlight_input) catch Highlights{};
- const request = renderRequest(viewport, policy);
+ const request = renderRequest(viewport, policy, state.fit);
var visible = visiblePages(state, viewport);
reconcile(state, gpa, request, tint_key, highlights, visible, viewport);