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