From db5ee5d20daf8855de3a3b2fa9349eced70946f0 Mon Sep 17 00:00:00 2001 From: Gregor Lohaus Date: Sun, 31 May 2026 21:36:21 +0200 Subject: [PATCH] reuse scratch buffers when marshalling render cells Each per-field cell getter wrapped its own Arena.ofConfined() (a native alloc/free plus session bookkeeping) to hold a few output bytes. With ~6 downcalls per cell that is tens of thousands of confined arenas per frame on a full screen, which profiling in jprototerm pinned as ~6-7ms/frame and the dominant render cost. Allocate the scalar out-segments (int, bool, color, style) plus a growable graphemes buffer once per snapshot in a single Scratch, confined to the single marshalling thread, and thread it through the cell getters. Also drop a redundant List.copyOf of the per-row cell list (RenderRow's constructor already makes the immutable copy) and reuse a shared empty-codepoints array. Co-Authored-By: Claude Opus 4.8 --- .../jlibghostty/internal/GhosttyLibrary.java | 109 ++++++++++++------ 1 file changed, 73 insertions(+), 36 deletions(-) diff --git a/src/main/java/dev/jlibghostty/internal/GhosttyLibrary.java b/src/main/java/dev/jlibghostty/internal/GhosttyLibrary.java index 6cc17ad..dba772e 100644 --- a/src/main/java/dev/jlibghostty/internal/GhosttyLibrary.java +++ b/src/main/java/dev/jlibghostty/internal/GhosttyLibrary.java @@ -898,6 +898,43 @@ public final class GhosttyLibrary { } } + private static final int[] EMPTY_CODEPOINTS = new int[0]; + + /** + * Per-snapshot scratch buffers. A snapshot makes ~6 native downcalls per cell, and each + * field getter used to wrap its own {@link Arena#ofConfined()} (a native alloc/free plus + * session bookkeeping) just to hold a few output bytes — tens of thousands of arenas per + * frame on a full screen. These buffers are allocated once per snapshot and reused for + * every cell. Safe because a snapshot is marshalled on a single thread. + */ + private static final class Scratch { + private final Arena arena; + private final MemorySegment outInt; + private final MemorySegment outBool; + private final MemorySegment outColor; + private final MemorySegment outStyle; + private MemorySegment graphemes; + private int graphemesCapacity; + + private Scratch(Arena arena) { + this.arena = arena; + this.outInt = arena.allocate(C_INT); + this.outBool = arena.allocate(C_BOOL); + this.outColor = arena.allocate(3, 1); + this.outStyle = arena.allocate(GHOSTTY_STYLE); + this.graphemesCapacity = 8; + this.graphemes = arena.allocate(C_INT, graphemesCapacity); + } + + private MemorySegment graphemes(int len) { + if (len > graphemesCapacity) { + graphemesCapacity = Math.max(len, graphemesCapacity * 2); + graphemes = arena.allocate(C_INT, graphemesCapacity); + } + return graphemes; + } + } + public List renderStateRows(MemorySegment state) { return renderStateRows(state, false); } @@ -910,15 +947,16 @@ public final class GhosttyLibrary { */ public List renderStateRows(MemorySegment state, boolean dirtyRowsOnly) { MemorySegment iterator = renderStateRowIteratorNew(); - try { + try (Arena scratchArena = Arena.ofConfined()) { + Scratch scratch = new Scratch(scratchArena); renderStatePopulateRowIterator(state, iterator); List rows = new ArrayList<>(); int rowIndex = 0; while (renderStateRowIteratorNext(iterator)) { - boolean dirty = renderStateRowGetBoolean(iterator, RENDER_STATE_ROW_DATA_DIRTY); + boolean dirty = renderStateRowGetBoolean(iterator, RENDER_STATE_ROW_DATA_DIRTY, scratch); List cells = (dirtyRowsOnly && !dirty) ? List.of() - : renderStateRowCells(iterator); + : renderStateRowCells(iterator, scratch); rows.add(new RenderRow(rowIndex, dirty, cells)); rowIndex++; } @@ -1006,37 +1044,38 @@ public final class GhosttyLibrary { } } - private boolean renderStateRowGetBoolean(MemorySegment iterator, int key) { - try (Arena arena = Arena.ofConfined()) { - MemorySegment out = arena.allocate(C_BOOL); - int result = (int) renderStateRowGet.invoke(iterator, key, out); + private boolean renderStateRowGetBoolean(MemorySegment iterator, int key, Scratch scratch) { + try { + int result = (int) renderStateRowGet.invoke(iterator, key, scratch.outBool); checkResult("ghostty_render_state_row_get", result); - return out.get(C_BOOL, 0); + return scratch.outBool.get(C_BOOL, 0); } catch (Throwable t) { return rethrow(t); } } - private List renderStateRowCells(MemorySegment rowIterator) { + private List renderStateRowCells(MemorySegment rowIterator, Scratch scratch) { MemorySegment cells = renderStateRowCellsNew(); try { renderStatePopulateRowCells(rowIterator, cells); + // Returned raw: RenderRow's constructor already wraps this in an immutable copy, + // so a List.copyOf here would copy the per-cell list a second time every row. List result = new ArrayList<>(); int column = 0; while (renderStateRowCellsNext(cells)) { - int[] codepoints = renderStateRowCellGraphemes(cells); + int[] codepoints = renderStateRowCellGraphemes(cells, scratch); result.add(new RenderCell( column, codepoints, - renderStateRowCellColor(cells, RENDER_STATE_ROW_CELLS_DATA_FG_COLOR), - renderStateRowCellColor(cells, RENDER_STATE_ROW_CELLS_DATA_BG_COLOR), + renderStateRowCellColor(cells, RENDER_STATE_ROW_CELLS_DATA_FG_COLOR, scratch), + renderStateRowCellColor(cells, RENDER_STATE_ROW_CELLS_DATA_BG_COLOR, scratch), renderStateRowCellKittyPlaceholder(cells, codepoints), - renderStateRowCellsGetBoolean(cells, RENDER_STATE_ROW_CELLS_DATA_SELECTED), - renderStateRowCellInverse(cells) + renderStateRowCellsGetBoolean(cells, RENDER_STATE_ROW_CELLS_DATA_SELECTED, scratch), + renderStateRowCellInverse(cells, scratch) )); column++; } - return List.copyOf(result); + return result; } finally { renderStateRowCellsFree(cells); } @@ -1085,14 +1124,14 @@ public final class GhosttyLibrary { } } - private int[] renderStateRowCellGraphemes(MemorySegment cells) { - int len = renderStateRowCellsGetI32(cells, RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN); + private int[] renderStateRowCellGraphemes(MemorySegment cells, Scratch scratch) { + int len = renderStateRowCellsGetI32(cells, RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN, scratch); if (len <= 0) { - return new int[0]; + return EMPTY_CODEPOINTS; } - try (Arena arena = Arena.ofConfined()) { - MemorySegment out = arena.allocate(C_INT, len); + try { + MemorySegment out = scratch.graphemes(len); int result = (int) renderStateRowCellsGet.invoke(cells, RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF, out); checkResult("ghostty_render_state_row_cells_get", result); @@ -1106,23 +1145,21 @@ public final class GhosttyLibrary { } } - private int renderStateRowCellsGetI32(MemorySegment cells, int key) { - try (Arena arena = Arena.ofConfined()) { - MemorySegment out = arena.allocate(C_INT); - int result = (int) renderStateRowCellsGet.invoke(cells, key, out); + private int renderStateRowCellsGetI32(MemorySegment cells, int key, Scratch scratch) { + try { + int result = (int) renderStateRowCellsGet.invoke(cells, key, scratch.outInt); checkResult("ghostty_render_state_row_cells_get", result); - return out.get(C_INT, 0); + return scratch.outInt.get(C_INT, 0); } catch (Throwable t) { return rethrow(t); } } - private boolean renderStateRowCellsGetBoolean(MemorySegment cells, int key) { - try (Arena arena = Arena.ofConfined()) { - MemorySegment out = arena.allocate(C_BOOL); - int result = (int) renderStateRowCellsGet.invoke(cells, key, out); + private boolean renderStateRowCellsGetBoolean(MemorySegment cells, int key, Scratch scratch) { + try { + int result = (int) renderStateRowCellsGet.invoke(cells, key, scratch.outBool); checkResult("ghostty_render_state_row_cells_get", result); - return out.get(C_BOOL, 0); + return scratch.outBool.get(C_BOOL, 0); } catch (Throwable t) { return rethrow(t); } @@ -1251,9 +1288,9 @@ public final class GhosttyLibrary { } } - private Optional renderStateRowCellColor(MemorySegment cells, int key) { - try (Arena arena = Arena.ofConfined()) { - MemorySegment out = arena.allocate(3, 1); + private Optional renderStateRowCellColor(MemorySegment cells, int key, Scratch scratch) { + try { + MemorySegment out = scratch.outColor; int result = (int) renderStateRowCellsGet.invoke(cells, key, out); if (result == GHOSTTY_INVALID_VALUE) { return Optional.empty(); @@ -1271,9 +1308,9 @@ public final class GhosttyLibrary { // Reads the cell's reverse/inverse flag from its GhosttyStyle. The resolved fg/bg // colors do NOT account for inverse, so a renderer must read this and swap fg/bg. - private boolean renderStateRowCellInverse(MemorySegment cells) { - try (Arena arena = Arena.ofConfined()) { - MemorySegment out = arena.allocate(GHOSTTY_STYLE); + private boolean renderStateRowCellInverse(MemorySegment cells, Scratch scratch) { + try { + MemorySegment out = scratch.outStyle; out.set(C_SIZE_T, 0, GHOSTTY_STYLE.byteSize()); int result = (int) renderStateRowCellsGet.invoke(cells, RENDER_STATE_ROW_CELLS_DATA_STYLE, out); if (result == GHOSTTY_INVALID_VALUE) {