11 Commits

Author SHA1 Message Date
8ac07218fe send backtab (ESC [ Z) for Shift+Tab
KeyEncoder mapped TAB to a plain tab regardless of Shift, so Shift+Tab sent the
same byte as Tab. Apps that use backtab for reverse navigation (fish completion
menu, helix theme picker) never saw it. Emit CSI Z when Shift is held.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 93d53fcef6)
2026-05-31 22:34:45 +02:00
6bf69e8572 update jlibghostty 2026-05-31 22:23:14 +02:00
07585a314c Upload only changed rows to GPU and hoist glyph bounds checks
paintIncremental's per-row dirty work was negated by present() calling
PixelBuffer.updateBuffer(null), which re-uploads the whole pane texture
every frame. Track the vertical band of buffer rows written since the
last present and hand that to updateBuffer so only changed rows upload.
The border is now drawn without extending the dirty band (its pixels are
unchanged between incremental frames). Also clamp blitGlyph's rectangle
once instead of bounds-checking every glyph pixel in the inner loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 22:12:45 +02:00
bdb33450f1 update jlibghostty 2026-05-31 21:51:57 +02:00
Gregor Lohaus
2c020bb6cb fix race condition 2026-05-31 18:12:44 +02:00
Gregor Lohaus
71a533ec34 clear context new fix 2026-05-31 18:05:57 +02:00
Gregor Lohaus
54b08c7eca revert failed fix 2026-05-31 18:00:49 +02:00
Gregor Lohaus
2fcdb286af Fixed the partial-dirty blanking regression 2026-05-31 17:59:26 +02:00
Gregor Lohaus
e6848ec684 revert failed fixed 2026-05-31 17:56:36 +02:00
Gregor Lohaus
38822d66b8 Fixed the partial-dirty blanking regression 2026-05-31 17:51:53 +02:00
Gregor Lohaus
586150de59 Fixed the partial-dirty blanking regression 2026-05-31 17:48:04 +02:00
6 changed files with 117 additions and 46 deletions

View File

@@ -5,7 +5,7 @@ connection.gradle.distribution=GRADLE_DISTRIBUTION(LOCAL_INSTALLATION(/home/anon
connection.project.dir=
eclipse.preferences.version=1
gradle.user.home=
java.home=/nix/store/c3pl7bqrx3d2rc3dh98z6yaj0mv1p52g-openjdk-21.0.10+7/lib/openjdk
java.home=/home/anon/.local/lib/graalvm-jdk-21.0.9+7.1
jvm.arguments=
offline.mode=false
override.workspace.settings=true

8
flake.lock generated
View File

@@ -70,11 +70,11 @@
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1780079529,
"narHash": "sha256-AxlGTL8c5xSLcQHvWlm994IdOqxsN8iKrA02Cpv7vso=",
"lastModified": 1780258814,
"narHash": "sha256-8rxL7xaZ/loYg3zdt0w5+hfNyHFVknDZN360NzrtCsQ=",
"ref": "refs/heads/main",
"rev": "68121d50b52fb56038871c97c97e7a12ffe987c2",
"revCount": 20,
"rev": "6a3d5aa0b0b1f738c958e2a2f0249574c07d9c4d",
"revCount": 23,
"type": "git",
"url": "https://gitea.gregorlohaus.com/gregor/jlibghostty.git"
},

View File

@@ -244,8 +244,7 @@ public final class Compositor {
drawTabBar(gc, canvas.getWidth(), topInset);
}
for (TerminalPane pane : panes) {
pane.paintFull(gc, isActive(pane));
paneContentVersion.put(pane, pane.contentVersion());
paneContentVersion.put(pane, pane.paintFull(gc, isActive(pane)));
}
}
@@ -262,8 +261,7 @@ public final class Compositor {
if (drawn != null && drawn == pane.contentVersion()) {
continue;
}
pane.paintIncremental(gc, isActive(pane));
paneContentVersion.put(pane, pane.contentVersion());
paneContentVersion.put(pane, pane.paintIncremental(gc, isActive(pane)));
}
}

View File

@@ -12,6 +12,7 @@ import dev.jlibghostty.RenderColor;
import dev.jlibghostty.RenderCursorStyle;
import dev.jlibghostty.RenderRow;
import dev.jlibghostty.RenderStateSnapshot;
import javafx.geometry.Rectangle2D;
import javafx.scene.SnapshotParameters;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
@@ -101,11 +102,7 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
if (dirty == DIRTY_FULL) {
software.paintFullOrShifted(gc, target.snapshotFull(), px, py, width, height, active);
} else if (dirty == DIRTY_PARTIAL) {
if (snapshot != null && snapshot.renderRows().size() == snapshot.rows()) {
software.paintFullOrShifted(gc, snapshot, px, py, width, height, active);
} else {
software.paintDirty(gc, snapshot, px, py, width, height, active);
}
software.paintDirty(gc, target, snapshot, px, py, width, height, active);
}
// dirty == FALSE: nothing visible changed.
}
@@ -679,12 +676,42 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
private long[] rowHashes = new long[0];
private CursorState lastCursor = CursorState.none();
private GlyphCache glyphs;
// Half-open [min, max) vertical span of buffer rows written since the last present, so
// present() can upload only that band to the GPU instead of the whole pane texture.
private int dirtyMinY = Integer.MAX_VALUE;
private int dirtyMaxY = Integer.MIN_VALUE;
private void invalidate() {
rowHashes = new long[0];
lastCursor = CursorState.none();
}
// Record that buffer rows [y0, y1) changed; clamped to the buffer in dirtyRegion().
private void markDirtyRows(int y0, int y1) {
if (y0 < dirtyMinY) {
dirtyMinY = y0;
}
if (y1 > dirtyMaxY) {
dirtyMaxY = y1;
}
}
private void resetDirty() {
dirtyMinY = Integer.MAX_VALUE;
dirtyMaxY = Integer.MIN_VALUE;
}
// The region to hand PixelBuffer.updateBuffer: a full-width band covering the rows
// written this frame (clamped to the buffer), or EMPTY when nothing changed.
private Rectangle2D dirtyRegion() {
int y0 = Math.max(0, dirtyMinY);
int y1 = Math.min(height, dirtyMaxY);
if (y0 >= y1) {
return Rectangle2D.EMPTY;
}
return new Rectangle2D(0, y0, width, y1 - y0);
}
private void paintFull(GraphicsContext gc, RenderStateSnapshot snapshot,
double px, double py, double paneWidth, double paneHeight, boolean active) {
ensure(paneWidth, paneHeight);
@@ -735,14 +762,14 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
present(gc, px, py);
}
private void paintDirty(GraphicsContext gc, RenderStateSnapshot snapshot,
private void paintDirty(GraphicsContext gc, RenderTarget target, RenderStateSnapshot snapshot,
double px, double py, double paneWidth, double paneHeight, boolean active) {
ensure(paneWidth, paneHeight);
if (snapshot == null) {
return;
}
if (rowHashes.length != snapshot.rows()) {
paintFull(gc, snapshot, px, py, paneWidth, paneHeight, active);
paintFull(gc, target.snapshotFull(), px, py, paneWidth, paneHeight, active);
return;
}
@@ -751,6 +778,7 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
int newCursorRow = cursor.viewportRow();
boolean cursorChanged = !cursor.equals(lastCursor);
boolean[] repainted = new boolean[snapshot.rows()];
boolean needsCursorDraw = cursorChanged;
for (RenderRow row : snapshot.renderRows()) {
if (!row.dirty()) {
@@ -759,28 +787,46 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
paintRow(row);
rowHashes[row.row()] = rowHash(row);
repainted[row.row()] = true;
if (row.row() == newCursorRow) {
needsCursorDraw = true;
}
}
if (cursorChanged) {
repaintCursorRow(snapshot, oldCursorRow, repainted);
if (!repaintCursorRow(snapshot, oldCursorRow, repainted)) {
paintFullOrShifted(gc, target.snapshotFull(), px, py, paneWidth, paneHeight, active);
return;
}
}
if (repaintedRowHasCursor(newCursorRow, repainted)
&& !repaintCursorRow(snapshot, newCursorRow, repainted)) {
paintFullOrShifted(gc, target.snapshotFull(), px, py, paneWidth, paneHeight, active);
return;
}
repaintCursorRow(snapshot, newCursorRow, repainted);
lastCursor = cursor;
drawCursor(snapshot);
if (needsCursorDraw) {
drawCursor(snapshot);
}
drawBorder(active);
present(gc, px, py);
}
private void repaintCursorRow(RenderStateSnapshot snapshot, int rowIndex, boolean[] repainted) {
private boolean repaintCursorRow(RenderStateSnapshot snapshot, int rowIndex, boolean[] repainted) {
if (rowIndex < 0 || rowIndex >= repainted.length || repainted[rowIndex]) {
return;
return true;
}
RenderRow row = rowByIndex(snapshot, rowIndex);
if (row != null) {
paintRow(row);
rowHashes[rowIndex] = rowHash(row);
repainted[rowIndex] = true;
if (row == null || !row.dirty()) {
return false;
}
paintRow(row);
rowHashes[rowIndex] = rowHash(row);
repainted[rowIndex] = true;
return true;
}
private boolean repaintedRowHasCursor(int rowIndex, boolean[] repainted) {
return rowIndex >= 0 && rowIndex < repainted.length && repainted[rowIndex];
}
private RenderRow rowByIndex(RenderStateSnapshot snapshot, int rowIndex) {
@@ -820,7 +866,11 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
}
private void present(GraphicsContext gc, double px, double py) {
pixelBuffer.updateBuffer(ignored -> null);
// Only re-upload the rows that actually changed; the unchanged remainder of the pane
// texture is already correct on the GPU from the previous frame.
Rectangle2D dirty = dirtyRegion();
resetDirty();
pixelBuffer.updateBuffer(ignored -> dirty);
gc.drawImage(image, px, py);
}
@@ -900,6 +950,9 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
int dy = rows * lineHeight();
int top = contentTop();
int contentHeight = rowHashes.length * lineHeight();
// The whole content region shifts; the arraycopy below moves pixels that the
// per-strip fillRect calls don't touch, so mark the full content band for upload.
markDirtyRows(top, top + contentHeight);
if (dy == 0 || Math.abs(dy) >= contentHeight) {
fillRect(0, top, width, contentHeight, argbPre(PANE_BACKGROUND));
return;
@@ -1020,26 +1073,28 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
int red = (rgb >> 16) & 0xff;
int green = (rgb >> 8) & 0xff;
int blue = rgb & 0xff;
for (int gy = 0; gy < glyph.height; gy++) {
int py = y + gy;
if (py < 0 || py >= height) {
continue;
}
int rowOffset = py * width;
// Clamp the glyph rectangle to the buffer once, so the inner loops carry no
// per-pixel bounds check (this is the hottest pixel loop on a text repaint).
int gyStart = Math.max(0, -y);
int gyEnd = Math.min(glyph.height, height - y);
int gxStart = Math.max(0, -x);
int gxEnd = Math.min(glyph.width, width - x);
if (gyStart >= gyEnd || gxStart >= gxEnd) {
return;
}
for (int gy = gyStart; gy < gyEnd; gy++) {
int rowOffset = (y + gy) * width;
int glyphOffset = gy * glyph.width;
for (int gx = 0; gx < glyph.width; gx++) {
int px = x + gx;
if (px < 0 || px >= width) {
continue;
}
for (int gx = gxStart; gx < gxEnd; gx++) {
int alpha = glyph.alpha[glyphOffset + gx] & 0xff;
if (alpha == 0) {
continue;
}
int index = rowOffset + px;
int index = rowOffset + x + gx;
pixels[index] = blendOpaque(pixels[index], red, green, blue, alpha);
}
}
markDirtyRows(y + gyStart, y + gyEnd);
}
private int blendOpaque(int dst, int red, int green, int blue, int alpha) {
@@ -1078,11 +1133,16 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
}
private void strokeRect(int x, int y, int w, int h, int color, int lineWidth) {
// The border is redrawn every frame to restore the side edges over the rows we
// repaint, but its pixels never change between incremental frames. Write it without
// marking the dirty band: the segments inside a repainted row's band are already
// covered by that band (and so re-uploaded), and the segments outside it are
// identical to what is already on the GPU, so they need no upload.
for (int i = 0; i < lineWidth; i++) {
fillRect(x + i, y + i, w - (2 * i), 1, color);
fillRect(x + i, y + h - 1 - i, w - (2 * i), 1, color);
fillRect(x + i, y + i, 1, h - (2 * i), color);
fillRect(x + w - 1 - i, y + i, 1, h - (2 * i), color);
fillRectRaw(x + i, y + i, w - (2 * i), 1, color);
fillRectRaw(x + i, y + h - 1 - i, w - (2 * i), 1, color);
fillRectRaw(x + i, y + i, 1, h - (2 * i), color);
fillRectRaw(x + w - 1 - i, y + i, 1, h - (2 * i), color);
}
}
@@ -1103,19 +1163,30 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
pixels[index] = blendOpaque(pixels[index], red, green, blue, alpha);
}
}
markDirtyRows(y0, y1);
}
private void fillRect(int x, int y, int w, int h, int color) {
int y0 = Math.max(0, y);
int y1 = Math.min(height, y + h);
if (fillRectRaw(x, y, w, h, color)) {
markDirtyRows(y0, y1);
}
}
// Raw fill with no dirty-band tracking; returns whether any pixels were written.
private boolean fillRectRaw(int x, int y, int w, int h, int color) {
int x0 = Math.max(0, x);
int y0 = Math.max(0, y);
int x1 = Math.min(width, x + w);
int y1 = Math.min(height, y + h);
if (x0 >= x1 || y0 >= y1) {
return;
return false;
}
for (int py = y0; py < y1; py++) {
Arrays.fill(pixels, (py * width) + x0, (py * width) + x1, color);
}
return true;
}
private int contentLeft() {

View File

@@ -26,7 +26,7 @@ final class KeyEncoder {
return switch (code) {
case ENTER -> "\r";
case BACK_SPACE -> "\u007f";
case TAB -> "\t";
case TAB -> event.isShiftDown() ? "\u001b[Z" : "\t";
case ESCAPE -> "\u001b";
case UP -> "\u001b[A";
case DOWN -> "\u001b[B";

View File

@@ -283,13 +283,15 @@ public final class TerminalPane implements AutoCloseable, RenderTarget {
}
/** Paint the whole pane; see {@link TerminalRenderer#paintFull}. */
public void paintFull(GraphicsContext gc, boolean active) {
public long paintFull(GraphicsContext gc, boolean active) {
renderer.paintFull(gc, this, active);
return snapshotVersion;
}
/** Repaint what changed; see {@link TerminalRenderer#paintIncremental}. */
public void paintIncremental(GraphicsContext gc, boolean active) {
public long paintIncremental(GraphicsContext gc, boolean active) {
renderer.paintIncremental(gc, this, active);
return snapshotVersion;
}
@Override