10 Commits

Author SHA1 Message Date
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
5 changed files with 116 additions and 45 deletions

View File

@@ -5,7 +5,7 @@ connection.gradle.distribution=GRADLE_DISTRIBUTION(LOCAL_INSTALLATION(/home/anon
connection.project.dir= connection.project.dir=
eclipse.preferences.version=1 eclipse.preferences.version=1
gradle.user.home= 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= jvm.arguments=
offline.mode=false offline.mode=false
override.workspace.settings=true override.workspace.settings=true

8
flake.lock generated
View File

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

View File

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

View File

@@ -12,6 +12,7 @@ import dev.jlibghostty.RenderColor;
import dev.jlibghostty.RenderCursorStyle; import dev.jlibghostty.RenderCursorStyle;
import dev.jlibghostty.RenderRow; import dev.jlibghostty.RenderRow;
import dev.jlibghostty.RenderStateSnapshot; import dev.jlibghostty.RenderStateSnapshot;
import javafx.geometry.Rectangle2D;
import javafx.scene.SnapshotParameters; import javafx.scene.SnapshotParameters;
import javafx.scene.canvas.Canvas; import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext; import javafx.scene.canvas.GraphicsContext;
@@ -101,11 +102,7 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
if (dirty == DIRTY_FULL) { if (dirty == DIRTY_FULL) {
software.paintFullOrShifted(gc, target.snapshotFull(), px, py, width, height, active); software.paintFullOrShifted(gc, target.snapshotFull(), px, py, width, height, active);
} else if (dirty == DIRTY_PARTIAL) { } else if (dirty == DIRTY_PARTIAL) {
if (snapshot != null && snapshot.renderRows().size() == snapshot.rows()) { software.paintDirty(gc, target, snapshot, px, py, width, height, active);
software.paintFullOrShifted(gc, snapshot, px, py, width, height, active);
} else {
software.paintDirty(gc, snapshot, px, py, width, height, active);
}
} }
// dirty == FALSE: nothing visible changed. // dirty == FALSE: nothing visible changed.
} }
@@ -679,12 +676,42 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
private long[] rowHashes = new long[0]; private long[] rowHashes = new long[0];
private CursorState lastCursor = CursorState.none(); private CursorState lastCursor = CursorState.none();
private GlyphCache glyphs; 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() { private void invalidate() {
rowHashes = new long[0]; rowHashes = new long[0];
lastCursor = CursorState.none(); 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, private void paintFull(GraphicsContext gc, RenderStateSnapshot snapshot,
double px, double py, double paneWidth, double paneHeight, boolean active) { double px, double py, double paneWidth, double paneHeight, boolean active) {
ensure(paneWidth, paneHeight); ensure(paneWidth, paneHeight);
@@ -735,14 +762,14 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
present(gc, px, py); 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) { double px, double py, double paneWidth, double paneHeight, boolean active) {
ensure(paneWidth, paneHeight); ensure(paneWidth, paneHeight);
if (snapshot == null) { if (snapshot == null) {
return; return;
} }
if (rowHashes.length != snapshot.rows()) { if (rowHashes.length != snapshot.rows()) {
paintFull(gc, snapshot, px, py, paneWidth, paneHeight, active); paintFull(gc, target.snapshotFull(), px, py, paneWidth, paneHeight, active);
return; return;
} }
@@ -751,6 +778,7 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
int newCursorRow = cursor.viewportRow(); int newCursorRow = cursor.viewportRow();
boolean cursorChanged = !cursor.equals(lastCursor); boolean cursorChanged = !cursor.equals(lastCursor);
boolean[] repainted = new boolean[snapshot.rows()]; boolean[] repainted = new boolean[snapshot.rows()];
boolean needsCursorDraw = cursorChanged;
for (RenderRow row : snapshot.renderRows()) { for (RenderRow row : snapshot.renderRows()) {
if (!row.dirty()) { if (!row.dirty()) {
@@ -759,28 +787,46 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
paintRow(row); paintRow(row);
rowHashes[row.row()] = rowHash(row); rowHashes[row.row()] = rowHash(row);
repainted[row.row()] = true; repainted[row.row()] = true;
if (row.row() == newCursorRow) {
needsCursorDraw = true;
}
} }
if (cursorChanged) { 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; lastCursor = cursor;
drawCursor(snapshot); if (needsCursorDraw) {
drawCursor(snapshot);
}
drawBorder(active); drawBorder(active);
present(gc, px, py); 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]) { if (rowIndex < 0 || rowIndex >= repainted.length || repainted[rowIndex]) {
return; return true;
} }
RenderRow row = rowByIndex(snapshot, rowIndex); RenderRow row = rowByIndex(snapshot, rowIndex);
if (row != null) { if (row == null || !row.dirty()) {
paintRow(row); return false;
rowHashes[rowIndex] = rowHash(row);
repainted[rowIndex] = true;
} }
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) { 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) { 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); gc.drawImage(image, px, py);
} }
@@ -900,6 +950,9 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
int dy = rows * lineHeight(); int dy = rows * lineHeight();
int top = contentTop(); int top = contentTop();
int contentHeight = rowHashes.length * lineHeight(); 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) { if (dy == 0 || Math.abs(dy) >= contentHeight) {
fillRect(0, top, width, contentHeight, argbPre(PANE_BACKGROUND)); fillRect(0, top, width, contentHeight, argbPre(PANE_BACKGROUND));
return; return;
@@ -1020,26 +1073,28 @@ final class GhosttyTerminalRenderer extends TerminalRenderer {
int red = (rgb >> 16) & 0xff; int red = (rgb >> 16) & 0xff;
int green = (rgb >> 8) & 0xff; int green = (rgb >> 8) & 0xff;
int blue = rgb & 0xff; int blue = rgb & 0xff;
for (int gy = 0; gy < glyph.height; gy++) { // Clamp the glyph rectangle to the buffer once, so the inner loops carry no
int py = y + gy; // per-pixel bounds check (this is the hottest pixel loop on a text repaint).
if (py < 0 || py >= height) { int gyStart = Math.max(0, -y);
continue; int gyEnd = Math.min(glyph.height, height - y);
} int gxStart = Math.max(0, -x);
int rowOffset = py * width; 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; int glyphOffset = gy * glyph.width;
for (int gx = 0; gx < glyph.width; gx++) { for (int gx = gxStart; gx < gxEnd; gx++) {
int px = x + gx;
if (px < 0 || px >= width) {
continue;
}
int alpha = glyph.alpha[glyphOffset + gx] & 0xff; int alpha = glyph.alpha[glyphOffset + gx] & 0xff;
if (alpha == 0) { if (alpha == 0) {
continue; continue;
} }
int index = rowOffset + px; int index = rowOffset + x + gx;
pixels[index] = blendOpaque(pixels[index], red, green, blue, alpha); 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) { 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) { 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++) { for (int i = 0; i < lineWidth; i++) {
fillRect(x + i, y + i, w - (2 * i), 1, color); fillRectRaw(x + i, y + i, w - (2 * i), 1, color);
fillRect(x + i, y + h - 1 - i, w - (2 * i), 1, color); fillRectRaw(x + i, y + h - 1 - i, w - (2 * i), 1, color);
fillRect(x + i, y + i, 1, h - (2 * i), color); fillRectRaw(x + i, y + i, 1, h - (2 * i), color);
fillRect(x + w - 1 - 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); 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) { 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 x0 = Math.max(0, x);
int y0 = Math.max(0, y); int y0 = Math.max(0, y);
int x1 = Math.min(width, x + w); int x1 = Math.min(width, x + w);
int y1 = Math.min(height, y + h); int y1 = Math.min(height, y + h);
if (x0 >= x1 || y0 >= y1) { if (x0 >= x1 || y0 >= y1) {
return; return false;
} }
for (int py = y0; py < y1; py++) { for (int py = y0; py < y1; py++) {
Arrays.fill(pixels, (py * width) + x0, (py * width) + x1, color); Arrays.fill(pixels, (py * width) + x0, (py * width) + x1, color);
} }
return true;
} }
private int contentLeft() { private int contentLeft() {

View File

@@ -283,13 +283,15 @@ public final class TerminalPane implements AutoCloseable, RenderTarget {
} }
/** Paint the whole pane; see {@link TerminalRenderer#paintFull}. */ /** 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); renderer.paintFull(gc, this, active);
return snapshotVersion;
} }
/** Repaint what changed; see {@link TerminalRenderer#paintIncremental}. */ /** 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); renderer.paintIncremental(gc, this, active);
return snapshotVersion;
} }
@Override @Override