Compare commits
4 Commits
db5ee5d20d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1cd908e5d0 | |||
| 06a9d5d3ec | |||
| 6a3d5aa0b0 | |||
| 5fdae1e7d5 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -6,3 +6,7 @@ result-*
|
|||||||
*.jar
|
*.jar
|
||||||
.devenv
|
.devenv
|
||||||
BUG.md
|
BUG.md
|
||||||
|
.classpath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
bin
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ public final class KittyGraphics {
|
|||||||
return placements(KittyPlacementLayer.ALL);
|
return placements(KittyPlacementLayer.ALL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Whether any placement exists, without materializing the placement list or its per-image
|
||||||
|
// render info. Cheap enough to call every frame to decide the render path.
|
||||||
|
public boolean isEmpty() {
|
||||||
|
try (KittyPlacementIterator iterator = KittyPlacementIterator.open(library, graphics, KittyPlacementLayer.ALL)) {
|
||||||
|
return !iterator.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public List<KittyPlacement> placements(KittyPlacementLayer layer) {
|
public List<KittyPlacement> placements(KittyPlacementLayer layer) {
|
||||||
try (KittyPlacementIterator iterator = KittyPlacementIterator.open(library, graphics, layer)) {
|
try (KittyPlacementIterator iterator = KittyPlacementIterator.open(library, graphics, layer)) {
|
||||||
List<KittyPlacement> placements = new ArrayList<>();
|
List<KittyPlacement> placements = new ArrayList<>();
|
||||||
@@ -53,7 +61,7 @@ public final class KittyGraphics {
|
|||||||
iterator.getU32(GhosttyLibrary.KITTY_GRAPHICS_PLACEMENT_DATA_COLUMNS),
|
iterator.getU32(GhosttyLibrary.KITTY_GRAPHICS_PLACEMENT_DATA_COLUMNS),
|
||||||
iterator.getU32(GhosttyLibrary.KITTY_GRAPHICS_PLACEMENT_DATA_ROWS),
|
iterator.getU32(GhosttyLibrary.KITTY_GRAPHICS_PLACEMENT_DATA_ROWS),
|
||||||
iterator.getI32(GhosttyLibrary.KITTY_GRAPHICS_PLACEMENT_DATA_Z),
|
iterator.getI32(GhosttyLibrary.KITTY_GRAPHICS_PLACEMENT_DATA_Z),
|
||||||
image.map(KittyImage::snapshot),
|
image,
|
||||||
Optional.ofNullable(renderInfo)
|
Optional.ofNullable(renderInfo)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,16 +13,46 @@ public final class KittyImage {
|
|||||||
this.handle = handle;
|
this.handle = handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cheap metadata accessors: a single native field read each, no pixel-buffer copy. Use these
|
||||||
|
// to build cache keys; only call data() once you've decided you actually need the bytes.
|
||||||
|
|
||||||
|
public long id() {
|
||||||
|
return library.kittyImageGetU32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long number() {
|
||||||
|
return library.kittyImageGetU32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_NUMBER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long width() {
|
||||||
|
return library.kittyImageGetU32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_WIDTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long height() {
|
||||||
|
return library.kittyImageGetU32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_HEIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public KittyImageFormat format() {
|
||||||
|
return KittyImageFormat.fromNative(library.kittyImageGetI32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_FORMAT));
|
||||||
|
}
|
||||||
|
|
||||||
|
public KittyImageCompression compression() {
|
||||||
|
return KittyImageCompression.fromNative(library.kittyImageGetI32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_COMPRESSION));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Byte length of the pixel buffer, read without copying it.
|
||||||
|
public long dataLength() {
|
||||||
|
return library.kittyImageDataLength(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copies the pixel buffer out of native memory. Expensive for large images; call only when
|
||||||
|
// the decoded image isn't already cached.
|
||||||
|
public byte[] data() {
|
||||||
|
return library.kittyImageData(handle);
|
||||||
|
}
|
||||||
|
|
||||||
public KittyImageSnapshot snapshot() {
|
public KittyImageSnapshot snapshot() {
|
||||||
return new KittyImageSnapshot(
|
return new KittyImageSnapshot(id(), number(), width(), height(), format(), compression(), data());
|
||||||
library.kittyImageGetU32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_ID),
|
|
||||||
library.kittyImageGetU32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_NUMBER),
|
|
||||||
library.kittyImageGetU32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_WIDTH),
|
|
||||||
library.kittyImageGetU32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_HEIGHT),
|
|
||||||
KittyImageFormat.fromNative(library.kittyImageGetI32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_FORMAT)),
|
|
||||||
KittyImageCompression.fromNative(library.kittyImageGetI32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_COMPRESSION)),
|
|
||||||
library.kittyImageData(handle)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MemorySegment handle() {
|
MemorySegment handle() {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public record KittyPlacement(
|
|||||||
long columns,
|
long columns,
|
||||||
long rows,
|
long rows,
|
||||||
int z,
|
int z,
|
||||||
Optional<KittyImageSnapshot> image,
|
Optional<KittyImage> image,
|
||||||
Optional<KittyRenderInfo> renderInfo
|
Optional<KittyRenderInfo> renderInfo
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ public record RenderCell(
|
|||||||
}
|
}
|
||||||
|
|
||||||
public RenderCell {
|
public RenderCell {
|
||||||
codepoints = codepoints.clone();
|
// Stored by reference: the marshaller hands us freshly allocated (or the shared
|
||||||
|
// empty) arrays it never mutates, so cloning here would double-allocate every
|
||||||
|
// cell and defeat the shared-empty optimization. The codepoints() accessor still
|
||||||
|
// clones on read to protect external callers.
|
||||||
foreground = foreground == null ? Optional.empty() : foreground;
|
foreground = foreground == null ? Optional.empty() : foreground;
|
||||||
background = background == null ? Optional.empty() : background;
|
background = background == null ? Optional.empty() : background;
|
||||||
kittyPlaceholder = kittyPlaceholder == null ? Optional.empty() : kittyPlaceholder;
|
kittyPlaceholder = kittyPlaceholder == null ? Optional.empty() : kittyPlaceholder;
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
public final class Terminal implements AutoCloseable {
|
public final class Terminal implements AutoCloseable {
|
||||||
private final GhosttyLibrary library;
|
private final GhosttyLibrary library;
|
||||||
private final MemorySegment handle;
|
private final MemorySegment handle;
|
||||||
private final Arena callbackArena = Arena.ofShared();
|
|
||||||
private final AtomicBoolean closed = new AtomicBoolean();
|
private final AtomicBoolean closed = new AtomicBoolean();
|
||||||
|
private Arena ptyWriterArena;
|
||||||
|
private Arena deviceAttributesArena;
|
||||||
private MemorySegment ptyWriterStub = MemorySegment.NULL;
|
private MemorySegment ptyWriterStub = MemorySegment.NULL;
|
||||||
private MemorySegment deviceAttributesStub = MemorySegment.NULL;
|
private MemorySegment deviceAttributesStub = MemorySegment.NULL;
|
||||||
private PtyWriter ptyWriter;
|
private PtyWriter ptyWriter;
|
||||||
@@ -40,26 +41,41 @@ public final class Terminal implements AutoCloseable {
|
|||||||
public void setPtyWriter(PtyWriter writer) {
|
public void setPtyWriter(PtyWriter writer) {
|
||||||
ensureOpen();
|
ensureOpen();
|
||||||
ptyWriter = writer;
|
ptyWriter = writer;
|
||||||
|
|
||||||
|
// Each upcall stub lives in its own arena so that reassigning (or clearing) the
|
||||||
|
// writer can free the previous stub. The native pointer is repointed before the old
|
||||||
|
// arena is closed, so native never holds a dangling stub.
|
||||||
|
Arena previous = ptyWriterArena;
|
||||||
if (writer == null) {
|
if (writer == null) {
|
||||||
ptyWriterStub = MemorySegment.NULL;
|
ptyWriterStub = MemorySegment.NULL;
|
||||||
|
ptyWriterArena = null;
|
||||||
library.terminalSetPointer(handle, GhosttyLibrary.TERMINAL_OPT_WRITE_PTY, MemorySegment.NULL);
|
library.terminalSetPointer(handle, GhosttyLibrary.TERMINAL_OPT_WRITE_PTY, MemorySegment.NULL);
|
||||||
return;
|
} else {
|
||||||
|
Arena arena = Arena.ofShared();
|
||||||
|
ptyWriterStub = library.upcallPtyWriter(writer, arena);
|
||||||
|
ptyWriterArena = arena;
|
||||||
|
library.terminalSetPointer(handle, GhosttyLibrary.TERMINAL_OPT_WRITE_PTY, ptyWriterStub);
|
||||||
}
|
}
|
||||||
|
|
||||||
ptyWriterStub = library.upcallPtyWriter(writer, callbackArena);
|
if (previous != null) {
|
||||||
library.terminalSetPointer(handle, GhosttyLibrary.TERMINAL_OPT_WRITE_PTY, ptyWriterStub);
|
previous.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDeviceAttributesProvider(DeviceAttributesProvider provider) {
|
public void setDeviceAttributesProvider(DeviceAttributesProvider provider) {
|
||||||
ensureOpen();
|
ensureOpen();
|
||||||
deviceAttributesProvider = provider;
|
deviceAttributesProvider = provider;
|
||||||
|
|
||||||
|
// See setPtyWriter: a per-stub arena lets reassignment free the previous stub.
|
||||||
|
Arena previous = deviceAttributesArena;
|
||||||
if (provider == null) {
|
if (provider == null) {
|
||||||
deviceAttributesStub = MemorySegment.NULL;
|
deviceAttributesStub = MemorySegment.NULL;
|
||||||
|
deviceAttributesArena = null;
|
||||||
library.terminalSetPointer(handle, GhosttyLibrary.TERMINAL_OPT_DEVICE_ATTRIBUTES, MemorySegment.NULL);
|
library.terminalSetPointer(handle, GhosttyLibrary.TERMINAL_OPT_DEVICE_ATTRIBUTES, MemorySegment.NULL);
|
||||||
return;
|
} else {
|
||||||
}
|
Arena arena = Arena.ofShared();
|
||||||
|
deviceAttributesStub = library.upcallDeviceAttributesProvider(provider, arena);
|
||||||
deviceAttributesStub = library.upcallDeviceAttributesProvider(provider, callbackArena);
|
deviceAttributesArena = arena;
|
||||||
library.terminalSetPointer(
|
library.terminalSetPointer(
|
||||||
handle,
|
handle,
|
||||||
GhosttyLibrary.TERMINAL_OPT_DEVICE_ATTRIBUTES,
|
GhosttyLibrary.TERMINAL_OPT_DEVICE_ATTRIBUTES,
|
||||||
@@ -67,6 +83,11 @@ public final class Terminal implements AutoCloseable {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (previous != null) {
|
||||||
|
previous.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public String text() {
|
public String text() {
|
||||||
return format(TerminalFormat.PLAIN);
|
return format(TerminalFormat.PLAIN);
|
||||||
}
|
}
|
||||||
@@ -156,7 +177,14 @@ public final class Terminal implements AutoCloseable {
|
|||||||
public void close() {
|
public void close() {
|
||||||
if (closed.compareAndSet(false, true)) {
|
if (closed.compareAndSet(false, true)) {
|
||||||
library.terminalFree(handle);
|
library.terminalFree(handle);
|
||||||
callbackArena.close();
|
if (ptyWriterArena != null) {
|
||||||
|
ptyWriterArena.close();
|
||||||
|
ptyWriterArena = null;
|
||||||
|
}
|
||||||
|
if (deviceAttributesArena != null) {
|
||||||
|
deviceAttributesArena.close();
|
||||||
|
deviceAttributesArena = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@@ -950,17 +951,20 @@ public final class GhosttyLibrary {
|
|||||||
try (Arena scratchArena = Arena.ofConfined()) {
|
try (Arena scratchArena = Arena.ofConfined()) {
|
||||||
Scratch scratch = new Scratch(scratchArena);
|
Scratch scratch = new Scratch(scratchArena);
|
||||||
renderStatePopulateRowIterator(state, iterator);
|
renderStatePopulateRowIterator(state, iterator);
|
||||||
List<RenderRow> rows = new ArrayList<>();
|
int cols = renderStateGetU16(state, RENDER_STATE_DATA_COLS);
|
||||||
|
List<RenderRow> rows = new ArrayList<>(renderStateGetU16(state, RENDER_STATE_DATA_ROWS));
|
||||||
int rowIndex = 0;
|
int rowIndex = 0;
|
||||||
while (renderStateRowIteratorNext(iterator)) {
|
while (renderStateRowIteratorNext(iterator)) {
|
||||||
boolean dirty = renderStateRowGetBoolean(iterator, RENDER_STATE_ROW_DATA_DIRTY, scratch);
|
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, scratch);
|
: renderStateRowCells(iterator, scratch, cols);
|
||||||
rows.add(new RenderRow(rowIndex, dirty, cells));
|
rows.add(new RenderRow(rowIndex, dirty, cells));
|
||||||
rowIndex++;
|
rowIndex++;
|
||||||
}
|
}
|
||||||
return List.copyOf(rows);
|
// rows is a fresh local that never escapes, so an unmodifiable wrapper gives
|
||||||
|
// the same immutability as List.copyOf without re-copying every row per frame.
|
||||||
|
return Collections.unmodifiableList(rows);
|
||||||
} finally {
|
} finally {
|
||||||
renderStateRowIteratorFree(iterator);
|
renderStateRowIteratorFree(iterator);
|
||||||
}
|
}
|
||||||
@@ -1054,13 +1058,13 @@ public final class GhosttyLibrary {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<RenderCell> renderStateRowCells(MemorySegment rowIterator, Scratch scratch) {
|
private List<RenderCell> renderStateRowCells(MemorySegment rowIterator, Scratch scratch, int cols) {
|
||||||
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,
|
// 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.
|
// 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<>(cols);
|
||||||
int column = 0;
|
int column = 0;
|
||||||
while (renderStateRowCellsNext(cells)) {
|
while (renderStateRowCellsNext(cells)) {
|
||||||
int[] codepoints = renderStateRowCellGraphemes(cells, scratch);
|
int[] codepoints = renderStateRowCellGraphemes(cells, scratch);
|
||||||
@@ -1430,6 +1434,19 @@ public final class GhosttyLibrary {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Length of the image's pixel buffer without copying it out of native memory. Lets callers
|
||||||
|
// build a cache key cheaply and only pull the bytes (kittyImageData) on a cache miss.
|
||||||
|
public long kittyImageDataLength(MemorySegment image) {
|
||||||
|
try (Arena arena = Arena.ofConfined()) {
|
||||||
|
MemorySegment outLen = arena.allocate(C_SIZE_T);
|
||||||
|
int result = (int) kittyGraphicsImageGet.invoke(image, KITTY_IMAGE_DATA_DATA_LEN, outLen);
|
||||||
|
checkResult("ghostty_kitty_graphics_image_get", result);
|
||||||
|
return outLen.get(C_SIZE_T, 0);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
return rethrow(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public MemorySegment kittyPlacementIteratorNew() {
|
public MemorySegment kittyPlacementIteratorNew() {
|
||||||
try (Arena arena = Arena.ofConfined()) {
|
try (Arena arena = Arena.ofConfined()) {
|
||||||
MemorySegment out = arena.allocate(C_POINTER);
|
MemorySegment out = arena.allocate(C_POINTER);
|
||||||
|
|||||||
Reference in New Issue
Block a user