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 <noreply@anthropic.com>
This commit is contained in:
Gregor Lohaus
2026-05-31 21:36:21 +02:00
parent 68121d50b5
commit db5ee5d20d

View File

@@ -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<RenderRow> renderStateRows(MemorySegment state) { public List<RenderRow> renderStateRows(MemorySegment state) {
return renderStateRows(state, false); return renderStateRows(state, false);
} }
@@ -910,15 +947,16 @@ public final class GhosttyLibrary {
*/ */
public List<RenderRow> renderStateRows(MemorySegment state, boolean dirtyRowsOnly) { public List<RenderRow> renderStateRows(MemorySegment state, boolean dirtyRowsOnly) {
MemorySegment iterator = renderStateRowIteratorNew(); MemorySegment iterator = renderStateRowIteratorNew();
try { try (Arena scratchArena = Arena.ofConfined()) {
Scratch scratch = new Scratch(scratchArena);
renderStatePopulateRowIterator(state, iterator); renderStatePopulateRowIterator(state, iterator);
List<RenderRow> rows = new ArrayList<>(); List<RenderRow> rows = new ArrayList<>();
int rowIndex = 0; int rowIndex = 0;
while (renderStateRowIteratorNext(iterator)) { while (renderStateRowIteratorNext(iterator)) {
boolean dirty = renderStateRowGetBoolean(iterator, RENDER_STATE_ROW_DATA_DIRTY); boolean dirty = renderStateRowGetBoolean(iterator, RENDER_STATE_ROW_DATA_DIRTY, scratch);
List<RenderCell> cells = (dirtyRowsOnly && !dirty) List<RenderCell> cells = (dirtyRowsOnly && !dirty)
? List.of() ? List.of()
: renderStateRowCells(iterator); : renderStateRowCells(iterator, scratch);
rows.add(new RenderRow(rowIndex, dirty, cells)); rows.add(new RenderRow(rowIndex, dirty, cells));
rowIndex++; rowIndex++;
} }
@@ -1006,37 +1044,38 @@ public final class GhosttyLibrary {
} }
} }
private boolean renderStateRowGetBoolean(MemorySegment iterator, int key) { private boolean renderStateRowGetBoolean(MemorySegment iterator, int key, Scratch scratch) {
try (Arena arena = Arena.ofConfined()) { try {
MemorySegment out = arena.allocate(C_BOOL); int result = (int) renderStateRowGet.invoke(iterator, key, scratch.outBool);
int result = (int) renderStateRowGet.invoke(iterator, key, out);
checkResult("ghostty_render_state_row_get", result); checkResult("ghostty_render_state_row_get", result);
return out.get(C_BOOL, 0); return scratch.outBool.get(C_BOOL, 0);
} catch (Throwable t) { } catch (Throwable t) {
return rethrow(t); return rethrow(t);
} }
} }
private List<RenderCell> renderStateRowCells(MemorySegment rowIterator) { private List<RenderCell> renderStateRowCells(MemorySegment rowIterator, Scratch scratch) {
MemorySegment cells = renderStateRowCellsNew(); MemorySegment cells = renderStateRowCellsNew();
try { try {
renderStatePopulateRowCells(rowIterator, cells); 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<RenderCell> result = new ArrayList<>(); List<RenderCell> result = new ArrayList<>();
int column = 0; int column = 0;
while (renderStateRowCellsNext(cells)) { while (renderStateRowCellsNext(cells)) {
int[] codepoints = renderStateRowCellGraphemes(cells); int[] codepoints = renderStateRowCellGraphemes(cells, scratch);
result.add(new RenderCell( result.add(new RenderCell(
column, column,
codepoints, codepoints,
renderStateRowCellColor(cells, RENDER_STATE_ROW_CELLS_DATA_FG_COLOR), renderStateRowCellColor(cells, RENDER_STATE_ROW_CELLS_DATA_FG_COLOR, scratch),
renderStateRowCellColor(cells, RENDER_STATE_ROW_CELLS_DATA_BG_COLOR), renderStateRowCellColor(cells, RENDER_STATE_ROW_CELLS_DATA_BG_COLOR, scratch),
renderStateRowCellKittyPlaceholder(cells, codepoints), renderStateRowCellKittyPlaceholder(cells, codepoints),
renderStateRowCellsGetBoolean(cells, RENDER_STATE_ROW_CELLS_DATA_SELECTED), renderStateRowCellsGetBoolean(cells, RENDER_STATE_ROW_CELLS_DATA_SELECTED, scratch),
renderStateRowCellInverse(cells) renderStateRowCellInverse(cells, scratch)
)); ));
column++; column++;
} }
return List.copyOf(result); return result;
} finally { } finally {
renderStateRowCellsFree(cells); renderStateRowCellsFree(cells);
} }
@@ -1085,14 +1124,14 @@ public final class GhosttyLibrary {
} }
} }
private int[] renderStateRowCellGraphemes(MemorySegment cells) { private int[] renderStateRowCellGraphemes(MemorySegment cells, Scratch scratch) {
int len = renderStateRowCellsGetI32(cells, RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN); int len = renderStateRowCellsGetI32(cells, RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN, scratch);
if (len <= 0) { if (len <= 0) {
return new int[0]; return EMPTY_CODEPOINTS;
} }
try (Arena arena = Arena.ofConfined()) { try {
MemorySegment out = arena.allocate(C_INT, len); MemorySegment out = scratch.graphemes(len);
int result = (int) renderStateRowCellsGet.invoke(cells, RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF, out); int result = (int) renderStateRowCellsGet.invoke(cells, RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF, out);
checkResult("ghostty_render_state_row_cells_get", result); checkResult("ghostty_render_state_row_cells_get", result);
@@ -1106,23 +1145,21 @@ public final class GhosttyLibrary {
} }
} }
private int renderStateRowCellsGetI32(MemorySegment cells, int key) { private int renderStateRowCellsGetI32(MemorySegment cells, int key, Scratch scratch) {
try (Arena arena = Arena.ofConfined()) { try {
MemorySegment out = arena.allocate(C_INT); int result = (int) renderStateRowCellsGet.invoke(cells, key, scratch.outInt);
int result = (int) renderStateRowCellsGet.invoke(cells, key, out);
checkResult("ghostty_render_state_row_cells_get", result); checkResult("ghostty_render_state_row_cells_get", result);
return out.get(C_INT, 0); return scratch.outInt.get(C_INT, 0);
} catch (Throwable t) { } catch (Throwable t) {
return rethrow(t); return rethrow(t);
} }
} }
private boolean renderStateRowCellsGetBoolean(MemorySegment cells, int key) { private boolean renderStateRowCellsGetBoolean(MemorySegment cells, int key, Scratch scratch) {
try (Arena arena = Arena.ofConfined()) { try {
MemorySegment out = arena.allocate(C_BOOL); int result = (int) renderStateRowCellsGet.invoke(cells, key, scratch.outBool);
int result = (int) renderStateRowCellsGet.invoke(cells, key, out);
checkResult("ghostty_render_state_row_cells_get", result); checkResult("ghostty_render_state_row_cells_get", result);
return out.get(C_BOOL, 0); return scratch.outBool.get(C_BOOL, 0);
} catch (Throwable t) { } catch (Throwable t) {
return rethrow(t); return rethrow(t);
} }
@@ -1251,9 +1288,9 @@ public final class GhosttyLibrary {
} }
} }
private Optional<RenderColor> renderStateRowCellColor(MemorySegment cells, int key) { private Optional<RenderColor> renderStateRowCellColor(MemorySegment cells, int key, Scratch scratch) {
try (Arena arena = Arena.ofConfined()) { try {
MemorySegment out = arena.allocate(3, 1); MemorySegment out = scratch.outColor;
int result = (int) renderStateRowCellsGet.invoke(cells, key, out); int result = (int) renderStateRowCellsGet.invoke(cells, key, out);
if (result == GHOSTTY_INVALID_VALUE) { if (result == GHOSTTY_INVALID_VALUE) {
return Optional.empty(); 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 // 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. // colors do NOT account for inverse, so a renderer must read this and swap fg/bg.
private boolean renderStateRowCellInverse(MemorySegment cells) { private boolean renderStateRowCellInverse(MemorySegment cells, Scratch scratch) {
try (Arena arena = Arena.ofConfined()) { try {
MemorySegment out = arena.allocate(GHOSTTY_STYLE); MemorySegment out = scratch.outStyle;
out.set(C_SIZE_T, 0, GHOSTTY_STYLE.byteSize()); out.set(C_SIZE_T, 0, GHOSTTY_STYLE.byteSize());
int result = (int) renderStateRowCellsGet.invoke(cells, RENDER_STATE_ROW_CELLS_DATA_STYLE, out); int result = (int) renderStateRowCellsGet.invoke(cells, RENDER_STATE_ROW_CELLS_DATA_STYLE, out);
if (result == GHOSTTY_INVALID_VALUE) { if (result == GHOSTTY_INVALID_VALUE) {