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) {
return renderStateRows(state, false);
}
@@ -910,15 +947,16 @@ public final class GhosttyLibrary {
*/
public List<RenderRow> renderStateRows(MemorySegment state, boolean dirtyRowsOnly) {
MemorySegment iterator = renderStateRowIteratorNew();
try {
try (Arena scratchArena = Arena.ofConfined()) {
Scratch scratch = new Scratch(scratchArena);
renderStatePopulateRowIterator(state, iterator);
List<RenderRow> 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<RenderCell> 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<RenderCell> renderStateRowCells(MemorySegment rowIterator) {
private List<RenderCell> 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<RenderCell> 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<RenderColor> renderStateRowCellColor(MemorySegment cells, int key) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(3, 1);
private Optional<RenderColor> 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) {