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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user