expose dirty on render state for incremental rendering

This commit is contained in:
Gregor Lohaus
2026-05-29 13:33:58 +02:00
parent d558d554b3
commit a534914a9b
2 changed files with 83 additions and 3 deletions

View File

@@ -29,10 +29,17 @@ public final class RenderState implements AutoCloseable {
GhosttyLibrary.RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE GhosttyLibrary.RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE
); );
// When only some rows changed (PARTIAL), skip marshalling cells for clean rows;
// FULL means the whole frame must be redrawn, so every row's cells are needed. A
// throwaway render state (a single update) always reports FULL, so callers that do
// not keep the state around keep getting fully-populated rows exactly as before.
int dirty = library.renderStateGetI32(handle, GhosttyLibrary.RENDER_STATE_DATA_DIRTY);
boolean dirtyRowsOnly = dirty != GhosttyLibrary.RENDER_STATE_DIRTY_FULL;
return new RenderStateSnapshot( return new RenderStateSnapshot(
library.renderStateGetU16(handle, GhosttyLibrary.RENDER_STATE_DATA_COLS), library.renderStateGetU16(handle, GhosttyLibrary.RENDER_STATE_DATA_COLS),
library.renderStateGetU16(handle, GhosttyLibrary.RENDER_STATE_DATA_ROWS), library.renderStateGetU16(handle, GhosttyLibrary.RENDER_STATE_DATA_ROWS),
library.renderStateGetI32(handle, GhosttyLibrary.RENDER_STATE_DATA_DIRTY), dirty,
RenderCursorStyle.fromNative( RenderCursorStyle.fromNative(
library.renderStateGetI32(handle, GhosttyLibrary.RENDER_STATE_DATA_CURSOR_VISUAL_STYLE) library.renderStateGetI32(handle, GhosttyLibrary.RENDER_STATE_DATA_CURSOR_VISUAL_STYLE)
), ),
@@ -48,7 +55,7 @@ public final class RenderState implements AutoCloseable {
: -1, : -1,
cursorViewportHasValue cursorViewportHasValue
&& library.renderStateGetBoolean(handle, GhosttyLibrary.RENDER_STATE_DATA_CURSOR_VIEWPORT_WIDE_TAIL), && library.renderStateGetBoolean(handle, GhosttyLibrary.RENDER_STATE_DATA_CURSOR_VIEWPORT_WIDE_TAIL),
rows() library.renderStateRows(handle, dirtyRowsOnly)
); );
} }
@@ -57,6 +64,16 @@ public final class RenderState implements AutoCloseable {
return library.renderStateRows(handle); return library.renderStateRows(handle);
} }
/**
* Clears the global and per-row dirty flags. Call after rendering a frame so the next
* {@link #update} reports only the rows that change after this point. Required for
* incremental rendering with a render state reused across frames.
*/
public void resetDirty() {
ensureOpen();
library.renderStateResetDirty(handle);
}
@Override @Override
public void close() { public void close() {
if (closed.compareAndSet(false, true)) { if (closed.compareAndSet(false, true)) {

View File

@@ -94,6 +94,15 @@ public final class GhosttyLibrary {
public static final int RENDER_STATE_ROW_DATA_DIRTY = 1; public static final int RENDER_STATE_ROW_DATA_DIRTY = 1;
public static final int RENDER_STATE_ROW_DATA_CELLS = 3; public static final int RENDER_STATE_ROW_DATA_CELLS = 3;
// ghostty_render_state_set / _row_set option selectors (both DIRTY == 0).
public static final int RENDER_STATE_OPTION_DIRTY = 0;
public static final int RENDER_STATE_ROW_OPTION_DIRTY = 0;
// GhosttyRenderStateDirty values returned by RENDER_STATE_DATA_DIRTY.
public static final int RENDER_STATE_DIRTY_FALSE = 0;
public static final int RENDER_STATE_DIRTY_PARTIAL = 1;
public static final int RENDER_STATE_DIRTY_FULL = 2;
public static final int RENDER_STATE_ROW_CELLS_DATA_STYLE = 2; public static final int RENDER_STATE_ROW_CELLS_DATA_STYLE = 2;
public static final int RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN = 3; public static final int RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN = 3;
public static final int RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF = 4; public static final int RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF = 4;
@@ -330,6 +339,8 @@ public final class GhosttyLibrary {
private final MethodHandle renderStateFree; private final MethodHandle renderStateFree;
private final MethodHandle renderStateUpdate; private final MethodHandle renderStateUpdate;
private final MethodHandle renderStateGet; private final MethodHandle renderStateGet;
private final MethodHandle renderStateSet;
private final MethodHandle renderStateRowSet;
private final MethodHandle renderStateRowIteratorNew; private final MethodHandle renderStateRowIteratorNew;
private final MethodHandle renderStateRowIteratorFree; private final MethodHandle renderStateRowIteratorFree;
private final MethodHandle renderStateRowIteratorNext; private final MethodHandle renderStateRowIteratorNext;
@@ -409,6 +420,10 @@ public final class GhosttyLibrary {
FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER)); FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER));
renderStateGet = downcall(symbols, "ghostty_render_state_get", renderStateGet = downcall(symbols, "ghostty_render_state_get",
FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_POINTER)); FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_POINTER));
renderStateSet = downcall(symbols, "ghostty_render_state_set",
FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_POINTER));
renderStateRowSet = downcall(symbols, "ghostty_render_state_row_set",
FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_POINTER));
renderStateRowIteratorNew = downcall(symbols, "ghostty_render_state_row_iterator_new", renderStateRowIteratorNew = downcall(symbols, "ghostty_render_state_row_iterator_new",
FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER)); FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER));
renderStateRowIteratorFree = downcall(symbols, "ghostty_render_state_row_iterator_free", renderStateRowIteratorFree = downcall(symbols, "ghostty_render_state_row_iterator_free",
@@ -855,6 +870,16 @@ public final class GhosttyLibrary {
} }
public List<RenderRow> renderStateRows(MemorySegment state) { public List<RenderRow> renderStateRows(MemorySegment state) {
return renderStateRows(state, false);
}
/**
* Builds the row list. When {@code dirtyRowsOnly} is true, cells are marshalled only
* for rows ghostty flagged dirty; clean rows get an empty cell list. This lets callers
* that keep a persistent render state skip the (expensive) per-cell marshalling for
* rows that have not changed since the last {@link #renderStateResetDirty}.
*/
public List<RenderRow> renderStateRows(MemorySegment state, boolean dirtyRowsOnly) {
MemorySegment iterator = renderStateRowIteratorNew(); MemorySegment iterator = renderStateRowIteratorNew();
try { try {
renderStatePopulateRowIterator(state, iterator); renderStatePopulateRowIterator(state, iterator);
@@ -862,7 +887,10 @@ public final class GhosttyLibrary {
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);
rows.add(new RenderRow(rowIndex, dirty, renderStateRowCells(iterator))); List<RenderCell> cells = (dirtyRowsOnly && !dirty)
? List.of()
: renderStateRowCells(iterator);
rows.add(new RenderRow(rowIndex, dirty, cells));
rowIndex++; rowIndex++;
} }
return List.copyOf(rows); return List.copyOf(rows);
@@ -871,6 +899,41 @@ public final class GhosttyLibrary {
} }
} }
public void renderStateSetDirty(MemorySegment state, int dirtyValue) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment value = arena.allocate(C_INT);
value.set(C_INT, 0, dirtyValue);
int result = (int) renderStateSet.invoke(state, RENDER_STATE_OPTION_DIRTY, value);
checkResult("ghostty_render_state_set", result);
} catch (Throwable t) {
rethrow(t);
}
}
/**
* Clears the global dirty flag and every per-row dirty flag. The {@code update} call
* does not reset dirty state, so a caller reusing a render state across frames must
* call this after consuming a frame; the next {@code update} then reports only what
* changed since now.
*/
public void renderStateResetDirty(MemorySegment state) {
MemorySegment iterator = renderStateRowIteratorNew();
try (Arena arena = Arena.ofConfined()) {
renderStatePopulateRowIterator(state, iterator);
MemorySegment falseValue = arena.allocate(C_BOOL);
falseValue.set(C_BOOL, 0, false);
while (renderStateRowIteratorNext(iterator)) {
int result = (int) renderStateRowSet.invoke(iterator, RENDER_STATE_ROW_OPTION_DIRTY, falseValue);
checkResult("ghostty_render_state_row_set", result);
}
} catch (Throwable t) {
rethrow(t);
} finally {
renderStateRowIteratorFree(iterator);
}
renderStateSetDirty(state, RENDER_STATE_DIRTY_FALSE);
}
private MemorySegment renderStateRowIteratorNew() { private MemorySegment renderStateRowIteratorNew() {
try (Arena arena = Arena.ofConfined()) { try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_POINTER); MemorySegment out = arena.allocate(C_POINTER);