Compare commits

..

2 Commits

Author SHA1 Message Date
1cd908e5d0 fix memory leak 2026-06-02 03:26:24 +02:00
06a9d5d3ec improve kitty image data handling 2026-06-01 02:15:54 +02:00
5 changed files with 102 additions and 23 deletions

View File

@@ -30,6 +30,14 @@ public final class KittyGraphics {
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) {
try (KittyPlacementIterator iterator = KittyPlacementIterator.open(library, graphics, layer)) {
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_ROWS),
iterator.getI32(GhosttyLibrary.KITTY_GRAPHICS_PLACEMENT_DATA_Z),
image.map(KittyImage::snapshot),
image,
Optional.ofNullable(renderInfo)
));
}

View File

@@ -13,16 +13,46 @@ public final class KittyImage {
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() {
return new KittyImageSnapshot(
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)
);
return new KittyImageSnapshot(id(), number(), width(), height(), format(), compression(), data());
}
MemorySegment handle() {

View File

@@ -15,7 +15,7 @@ public record KittyPlacement(
long columns,
long rows,
int z,
Optional<KittyImageSnapshot> image,
Optional<KittyImage> image,
Optional<KittyRenderInfo> renderInfo
) {
}

View File

@@ -11,8 +11,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
public final class Terminal implements AutoCloseable {
private final GhosttyLibrary library;
private final MemorySegment handle;
private final Arena callbackArena = Arena.ofShared();
private final AtomicBoolean closed = new AtomicBoolean();
private Arena ptyWriterArena;
private Arena deviceAttributesArena;
private MemorySegment ptyWriterStub = MemorySegment.NULL;
private MemorySegment deviceAttributesStub = MemorySegment.NULL;
private PtyWriter ptyWriter;
@@ -40,31 +41,51 @@ public final class Terminal implements AutoCloseable {
public void setPtyWriter(PtyWriter writer) {
ensureOpen();
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) {
ptyWriterStub = MemorySegment.NULL;
ptyWriterArena = 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);
library.terminalSetPointer(handle, GhosttyLibrary.TERMINAL_OPT_WRITE_PTY, ptyWriterStub);
if (previous != null) {
previous.close();
}
}
public void setDeviceAttributesProvider(DeviceAttributesProvider provider) {
ensureOpen();
deviceAttributesProvider = provider;
// See setPtyWriter: a per-stub arena lets reassignment free the previous stub.
Arena previous = deviceAttributesArena;
if (provider == null) {
deviceAttributesStub = MemorySegment.NULL;
deviceAttributesArena = null;
library.terminalSetPointer(handle, GhosttyLibrary.TERMINAL_OPT_DEVICE_ATTRIBUTES, MemorySegment.NULL);
return;
} else {
Arena arena = Arena.ofShared();
deviceAttributesStub = library.upcallDeviceAttributesProvider(provider, arena);
deviceAttributesArena = arena;
library.terminalSetPointer(
handle,
GhosttyLibrary.TERMINAL_OPT_DEVICE_ATTRIBUTES,
deviceAttributesStub
);
}
deviceAttributesStub = library.upcallDeviceAttributesProvider(provider, callbackArena);
library.terminalSetPointer(
handle,
GhosttyLibrary.TERMINAL_OPT_DEVICE_ATTRIBUTES,
deviceAttributesStub
);
if (previous != null) {
previous.close();
}
}
public String text() {
@@ -156,7 +177,14 @@ public final class Terminal implements AutoCloseable {
public void close() {
if (closed.compareAndSet(false, true)) {
library.terminalFree(handle);
callbackArena.close();
if (ptyWriterArena != null) {
ptyWriterArena.close();
ptyWriterArena = null;
}
if (deviceAttributesArena != null) {
deviceAttributesArena.close();
deviceAttributesArena = null;
}
}
}

View File

@@ -1434,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() {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_POINTER);