diff --git a/src/main/java/dev/jlibghostty/RenderState.java b/src/main/java/dev/jlibghostty/RenderState.java index 41c6331..1429d22 100644 --- a/src/main/java/dev/jlibghostty/RenderState.java +++ b/src/main/java/dev/jlibghostty/RenderState.java @@ -29,10 +29,17 @@ public final class RenderState implements AutoCloseable { 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( library.renderStateGetU16(handle, GhosttyLibrary.RENDER_STATE_DATA_COLS), library.renderStateGetU16(handle, GhosttyLibrary.RENDER_STATE_DATA_ROWS), - library.renderStateGetI32(handle, GhosttyLibrary.RENDER_STATE_DATA_DIRTY), + dirty, RenderCursorStyle.fromNative( library.renderStateGetI32(handle, GhosttyLibrary.RENDER_STATE_DATA_CURSOR_VISUAL_STYLE) ), @@ -48,7 +55,7 @@ public final class RenderState implements AutoCloseable { : -1, cursorViewportHasValue && 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); } + /** + * 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 public void close() { if (closed.compareAndSet(false, true)) { diff --git a/src/main/java/dev/jlibghostty/internal/GhosttyLibrary.java b/src/main/java/dev/jlibghostty/internal/GhosttyLibrary.java index 9bea9be..7363f04 100644 --- a/src/main/java/dev/jlibghostty/internal/GhosttyLibrary.java +++ b/src/main/java/dev/jlibghostty/internal/GhosttyLibrary.java @@ -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_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_GRAPHEMES_LEN = 3; 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 renderStateUpdate; private final MethodHandle renderStateGet; + private final MethodHandle renderStateSet; + private final MethodHandle renderStateRowSet; private final MethodHandle renderStateRowIteratorNew; private final MethodHandle renderStateRowIteratorFree; private final MethodHandle renderStateRowIteratorNext; @@ -409,6 +420,10 @@ public final class GhosttyLibrary { FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER)); renderStateGet = downcall(symbols, "ghostty_render_state_get", 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", FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER)); renderStateRowIteratorFree = downcall(symbols, "ghostty_render_state_row_iterator_free", @@ -855,6 +870,16 @@ public final class GhosttyLibrary { } public List 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 renderStateRows(MemorySegment state, boolean dirtyRowsOnly) { MemorySegment iterator = renderStateRowIteratorNew(); try { renderStatePopulateRowIterator(state, iterator); @@ -862,7 +887,10 @@ public final class GhosttyLibrary { int rowIndex = 0; while (renderStateRowIteratorNext(iterator)) { boolean dirty = renderStateRowGetBoolean(iterator, RENDER_STATE_ROW_DATA_DIRTY); - rows.add(new RenderRow(rowIndex, dirty, renderStateRowCells(iterator))); + List cells = (dirtyRowsOnly && !dirty) + ? List.of() + : renderStateRowCells(iterator); + rows.add(new RenderRow(rowIndex, dirty, cells)); rowIndex++; } 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() { try (Arena arena = Arena.ofConfined()) { MemorySegment out = arena.allocate(C_POINTER);