Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: LRU cache #48

Merged
merged 12 commits into from
Feb 23, 2025
Merged
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ apt install \
zlib1g-dev
```

> [!IMPORTANT]
> [!NOTE]
> On some Linux distributions (e.g., Fedora, Arch), replace `mupdf-third` with `mupdf` in `build.zig` to compile successfully.

### Build
Expand Down
7 changes: 7 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,10 @@ Configure the information bar at screen bottom:
- `style`: Status bar appearance
- `bg`: Array of 3 `u8` values [r, g, b] - Text color (0-255)
- `fg`: Array of 3 `u8` values [r, g, b] - Status bar color (0-255)

## Config

Configure the LRU cache

- `enabled`: `bool` - Allow/disallow caching
- `max_size`: ìnteger` - Maximum amount of pages cached
109 changes: 109 additions & 0 deletions src/Cache.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
const Self = @This();
const std = @import("std");
const Config = @import("config/Config.zig");

pub const Key = struct { colorize: bool, page: u16 };
pub const EncodedImage = struct { base64: []const u8, width: u16, height: u16, cached: bool };

const Node = struct {
key: Key,
value: EncodedImage,
prev: ?*Node,
next: ?*Node,
};

allocator: std.mem.Allocator,
map: std.AutoHashMap(Key, *Node),
head: ?*Node,
tail: ?*Node,
config: Config,
max_pages: usize,

pub fn init(allocator: std.mem.Allocator, config: Config) Self {
return .{
.allocator = allocator,
.map = std.AutoHashMap(Key, *Node).init(allocator),
.head = null,
.tail = null,
.config = config,
.max_pages = config.cache.max_pages,
};
}

pub fn deinit(self: *Self) void {
var current = self.head;
while (current) |node| {
const next = node.next;
self.allocator.free(node.value.base64);
self.allocator.destroy(node);
current = next;
}
self.map.deinit();
}

pub fn get(self: *Self, key: Key) ?EncodedImage {
const node = self.map.get(key) orelse return null;
self.moveToFront(node);
return node.value;
}

pub fn put(self: *Self, key: Key, image: EncodedImage) !bool {
if (self.map.get(key)) |node| {
self.moveToFront(node);
return false;
}

const new_node = try self.allocator.create(Node);
new_node.* = .{
.key = key,
.value = image,
.prev = null,
.next = null,
};

try self.map.put(key, new_node);
self.addToFront(new_node);

if (self.map.count() > self.max_pages) {
const tail_node = self.tail orelse unreachable;
_ = self.map.remove(tail_node.key);
self.removeNode(tail_node);
self.allocator.free(tail_node.value.base64);
self.allocator.destroy(tail_node);
}

return true;
}

fn addToFront(self: *Self, node: *Node) void {
node.next = self.head;
node.prev = null;

if (self.head) |head| {
head.prev = node;
} else {
self.tail = node;
}

self.head = node;
}

fn removeNode(self: *Self, node: *Node) void {
if (node.prev) |prev| {
prev.next = node.next;
} else {
self.head = node.next;
}

if (node.next) |next| {
next.prev = node.prev;
} else {
self.tail = node.prev;
}
}

fn moveToFront(self: *Self, node: *Node) void {
if (self.head == node) return;
self.removeNode(node);
self.addToFront(node);
}
37 changes: 18 additions & 19 deletions src/Context.zig
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
const std = @import("std");
const vaxis = @import("vaxis");
const ViewState = @import("state/ViewState.zig");
const CommandState = @import("state/CommandState.zig");
const ViewState = @import("states/ViewState.zig");
const CommandState = @import("states/CommandState.zig");
const fzwatch = @import("fzwatch");
const Config = @import("config/Config.zig");
const PdfHelper = @import("./helpers/PdfHelper.zig");
const PdfHandler = @import("./PdfHandler.zig");

pub const panic = vaxis.panic_handler;

Expand All @@ -26,7 +26,7 @@ pub const Context = struct {
tty: vaxis.Tty,
vx: vaxis.Vaxis,
mouse: ?vaxis.Mouse,
pdf_helper: PdfHelper,
pdf_handler: PdfHandler,
page_info_text: []u8,
current_page: ?vaxis.Image,
watcher: ?fzwatch.Watcher,
Expand All @@ -44,8 +44,8 @@ pub const Context = struct {

const config = try Config.init(allocator);

var pdf_helper = try PdfHelper.init(allocator, path, initial_page, config);
errdefer pdf_helper.deinit();
var pdf_handler = try PdfHandler.init(allocator, path, initial_page, config);
errdefer pdf_handler.deinit();

var watcher: ?fzwatch.Watcher = null;
if (config.file_monitor.enabled) {
Expand All @@ -58,7 +58,7 @@ pub const Context = struct {
.should_quit = false,
.tty = try vaxis.Tty.init(),
.vx = try vaxis.init(allocator, .{}),
.pdf_helper = pdf_helper,
.pdf_handler = pdf_handler,
.page_info_text = &[_]u8{},
.current_page = null,
.watcher = watcher,
Expand All @@ -81,7 +81,7 @@ pub const Context = struct {
w.deinit();
}
if (self.page_info_text.len > 0) self.allocator.free(self.page_info_text);
self.pdf_helper.deinit();
self.pdf_handler.deinit();
self.vx.deinit(self.allocator, self.tty.anyWriter());
self.tty.deinit();
}
Expand Down Expand Up @@ -152,6 +152,8 @@ pub const Context = struct {
if (self.current_page) |img| {
self.vx.freeImage(self.tty.anyWriter(), img.id);
self.current_page = null;
self.pdf_handler.resetZoomAndScroll();
self.pdf_handler.check_cache = true;
}
}

Expand All @@ -176,18 +178,18 @@ pub const Context = struct {
.mouse => |mouse| self.mouse = mouse,
.winsize => |ws| {
try self.vx.resize(self.allocator, self.tty.anyWriter(), ws);
self.pdf_helper.resetZoomAndScroll();
self.pdf_handler.resetZoomAndScroll();
self.reload_page = true;
},
.file_changed => {
try self.pdf_helper.reloadDocument();
try self.pdf_handler.reloadDocument();
self.reload_page = true;
},
}
}

pub fn drawCurrentPage(self: *Self, win: vaxis.Window) !void {
self.pdf_helper.commitReload();
self.pdf_handler.commitReload();
if (self.current_page == null or self.reload_page) {
const winsize = try vaxis.Tty.getWinsize(self.tty.fd);
const pix_per_col = try std.math.divCeil(u16, win.screen.width_pix, win.screen.width);
Expand All @@ -197,12 +199,9 @@ pub const Context = struct {
if (self.config.status_bar.enabled) {
y_pix -|= 2 * pix_per_row;
}
const encoded_image = try self.pdf_helper.renderPage(
self.allocator,
x_pix,
y_pix,
);
defer self.allocator.free(encoded_image.base64);

const encoded_image = try self.pdf_handler.renderPage(x_pix, y_pix);
defer if (!encoded_image.cached) self.allocator.free(encoded_image.base64);

self.current_page = try self.vx.transmitPreEncodedImage(
self.tty.anyWriter(),
Expand Down Expand Up @@ -243,7 +242,7 @@ pub const Context = struct {
status_bar.fill(vaxis.Cell{ .style = self.config.status_bar.style });

_ = status_bar.print(
&.{.{ .text = self.pdf_helper.path, .style = self.config.status_bar.style }},
&.{.{ .text = self.pdf_handler.path, .style = self.config.status_bar.style }},
.{ .col_offset = 1 },
);

Expand All @@ -254,7 +253,7 @@ pub const Context = struct {
self.page_info_text = try std.fmt.allocPrint(
self.allocator,
"{d}:{d}",
.{ self.pdf_helper.current_page_number + 1, self.pdf_helper.total_pages },
.{ self.pdf_handler.current_page_number + 1, self.pdf_handler.total_pages },
);

_ = status_bar.print(
Expand Down
75 changes: 48 additions & 27 deletions src/helpers/PdfHelper.zig → src/PdfHandler.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ const Self = @This();
const std = @import("std");
const fastb64z = @import("fastb64z");
const vaxis = @import("vaxis");
const Config = @import("../config/Config.zig");
const Config = @import("./config/Config.zig");
const Cache = @import("./Cache.zig");
const c = @cImport({
@cInclude("mupdf/fitz.h");
@cInclude("mupdf/pdf.h");
});

pub const PdfError = error{ FailedToCreateContext, FailedToOpenDocument, InvalidPageNumber };
pub const ScrollDirection = enum { Up, Down, Left, Right };
pub const EncodedImage = struct { base64: []const u8, width: u16, height: u16 };

allocator: std.mem.Allocator,
ctx: [*c]c.fz_context,
Expand All @@ -25,8 +25,16 @@ x_offset: f32,
y_center: f32,
x_center: f32,
config: Config,
cache: Cache,
check_cache: bool,

pub fn init(allocator: std.mem.Allocator, path: []const u8, initial_page: ?u16, config: Config) !Self {
pub fn init(
allocator: std.mem.Allocator,
path: []const u8,
initial_page: ?u16,
// TODO pass pointer everywhere?
config: Config,
) !Self {
const ctx = c.fz_new_context(null, null, c.FZ_STORE_UNLIMITED) orelse {
std.debug.print("Failed to create mupdf context\n", .{});
return PdfError.FailedToCreateContext;
Expand Down Expand Up @@ -66,10 +74,13 @@ pub fn init(allocator: std.mem.Allocator, path: []const u8, initial_page: ?u16,
.y_center = 0,
.x_center = 0,
.config = config,
.cache = Cache.init(allocator, config),
.check_cache = true,
};
}

pub fn deinit(self: *Self) void {
self.cache.deinit();
if (self.temp_doc) |doc| c.fz_drop_document(self.ctx, doc);
c.fz_drop_document(self.ctx, self.doc);
c.fz_drop_context(self.ctx);
Expand Down Expand Up @@ -97,10 +108,19 @@ pub fn commitReload(self: *Self) void {

pub fn renderPage(
self: *Self,
allocator: std.mem.Allocator,
window_width: u32,
window_height: u32,
) !EncodedImage {
) !Cache.EncodedImage {
if (self.config.cache.enabled and self.zoom == 0 and self.x_offset == 0 and self.y_offset == 0 and self.check_cache) {
if (self.cache.get(.{
.colorize = self.config.general.colorize,
.page = self.current_page_number,
})) |cached| {
self.check_cache = false;
return cached;
}
}

const page = c.fz_load_page(self.ctx, self.doc, self.current_page_number);
defer c.fz_drop_page(self.ctx, page);
const bound = c.fz_bound_page(self.ctx, page);
Expand Down Expand Up @@ -149,35 +169,36 @@ pub fn renderPage(

if (self.config.general.colorize) {
c.fz_tint_pixmap(self.ctx, pix, self.config.general.black, self.config.general.white);
} else {
c.fz_tint_pixmap(self.ctx, pix, 0x000000, 0xffffff);
}

const width = bbox.x1;
const height = bbox.y1;
const width = @as(usize, @intCast(@abs(bbox.x1)));
const height = @as(usize, @intCast(@abs(bbox.y1)));
const samples = c.fz_pixmap_samples(self.ctx, pix);

var img = try vaxis.zigimg.Image.fromRawPixels(
allocator,
@intCast(width),
@intCast(height),
samples[0..@intCast(width * height * 3)],
.rgb24,
);
defer img.deinit();

try img.convert(.rgb24);
const buf = img.rawBytes();

const base64Encoder = fastb64z.standard.Encoder;
const b64_buf = try self.allocator.alloc(u8, base64Encoder.calcSize(buf.len));

const encoded = base64Encoder.encode(b64_buf, buf);
const sample_count = width * height * 3;

const b64_buf = try self.allocator.alloc(u8, base64Encoder.calcSize(sample_count));
const encoded = base64Encoder.encode(b64_buf, samples[0..sample_count]);

var cached = false;
if (self.config.cache.enabled) {
cached = try self.cache.put(.{
.colorize = self.config.general.colorize,
.page = self.current_page_number,
}, .{
.base64 = encoded,
.width = @intCast(width),
.height = @intCast(height),
.cached = true,
});
}

return .{
return Cache.EncodedImage{
.base64 = encoded,
.width = @intCast(img.width),
.height = @intCast(img.height),
.width = @intCast(width),
.height = @intCast(height),
.cached = cached,
};
}

Expand Down
Loading