28x terminal redraw improvement.

This commit is contained in:
Scott Duensing 2026-03-12 22:01:33 -05:00
parent 291eaf0c35
commit 0a06d8f354
5 changed files with 215 additions and 67 deletions

View file

@ -1338,9 +1338,20 @@ static void pollAnsiTermWidgetsWalk(AppContextT *ctx, WidgetT *w, WindowT *win)
if (w->type == WidgetAnsiTermE) { if (w->type == WidgetAnsiTermE) {
wgtAnsiTermPoll(w); wgtAnsiTermPoll(w);
if (wgtAnsiTermRepaint(w) > 0) { int32_t dirtyY = 0;
int32_t dirtyH = 0;
if (wgtAnsiTermRepaint(w, &dirtyY, &dirtyH) > 0) {
win->contentDirty = true; win->contentDirty = true;
dvxInvalidateWindow(ctx, win);
// Dirty only the affected rows (in screen coords) instead of
// the entire window. This dramatically reduces compositor and
// LFB flush work for cursor blink and single-row updates.
int32_t scrollY = win->vScroll ? win->vScroll->value : 0;
int32_t rectX = win->x + win->contentX;
int32_t rectY = win->y + win->contentY + dirtyY - scrollY;
int32_t rectW = win->contentW;
dirtyListAdd(&ctx->dirty, rectX, rectY, rectW, dirtyH);
} }
} }

View file

@ -432,6 +432,143 @@ void drawMaskedBitmap(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, in
} }
// ============================================================
// drawTermRow
// ============================================================
//
// Renders an entire row of terminal character cells in one pass.
// lineData points to (ch, attr) pairs. palette is a 16-entry
// packed-color table. This avoids per-character function call
// overhead, redundant clip calculation, and spanFill startup
// costs that make drawChar expensive when called 80× per row.
void drawTermRow(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, int32_t cols, const uint8_t *lineData, const uint32_t *palette, bool blinkVisible, int32_t cursorCol) {
int32_t cw = font->charWidth;
int32_t ch = font->charHeight;
int32_t bpp = ops->bytesPerPixel;
int32_t pitch = d->pitch;
// Row-level clip: reject if entirely outside vertically
int32_t clipX1 = d->clipX;
int32_t clipX2 = d->clipX + d->clipW;
int32_t clipY1 = d->clipY;
int32_t clipY2 = d->clipY + d->clipH;
if (y + ch <= clipY1 || y >= clipY2) {
return;
}
// Vertical clip for glyph scanlines
int32_t rowStart = 0;
int32_t rowEnd = ch;
if (y < clipY1) { rowStart = clipY1 - y; }
if (y + ch > clipY2) { rowEnd = clipY2 - y; }
// Horizontal clip: find first and last visible column
int32_t rowW = cols * cw;
int32_t firstCol = 0;
int32_t lastCol = cols;
if (x + rowW <= clipX1 || x >= clipX2) {
return;
}
if (x < clipX1) {
firstCol = (clipX1 - x) / cw;
}
if (x + rowW > clipX2) {
lastCol = (clipX2 - x + cw - 1) / cw;
if (lastCol > cols) { lastCol = cols; }
}
// Per-column clip for partially visible edge cells
int32_t edgeColStart = 0;
if (x + firstCol * cw < clipX1) {
edgeColStart = clipX1 - (x + firstCol * cw);
}
// Render each visible cell
for (int32_t col = firstCol; col < lastCol; col++) {
uint8_t gch = lineData[col * 2];
uint8_t attr = lineData[col * 2 + 1];
uint32_t fg = palette[attr & 0x0F];
uint32_t bg = palette[(attr >> 4) & 0x07];
// Blink: hide text during off phase
if ((attr & 0x80) && !blinkVisible) {
fg = bg;
}
// Cursor: invert colors
if (col == cursorCol) {
uint32_t tmp = fg;
fg = bg;
bg = tmp;
}
int32_t cx = x + col * cw;
// Determine per-cell horizontal clip
int32_t cStart = 0;
int32_t cEnd = cw;
if (col == firstCol) {
cStart = edgeColStart;
}
if (cx + cw > clipX2) {
cEnd = clipX2 - cx;
}
// Look up glyph data
int32_t idx = (uint8_t)gch - font->firstChar;
const uint8_t *glyph = NULL;
if (idx >= 0 && idx < font->numChars) {
glyph = font->glyphData + idx * ch;
}
// Render scanlines
if (bpp == 2) {
uint16_t fg16 = (uint16_t)fg;
uint16_t bg16 = (uint16_t)bg;
for (int32_t row = rowStart; row < rowEnd; row++) {
uint16_t *dst = (uint16_t *)(d->backBuf + (y + row) * pitch + cx * 2);
uint8_t bits = glyph ? glyph[row] : 0;
for (int32_t p = cStart; p < cEnd; p++) {
dst[p] = (bits & (0x80 >> p)) ? fg16 : bg16;
}
}
} else if (bpp == 4) {
for (int32_t row = rowStart; row < rowEnd; row++) {
uint32_t *dst = (uint32_t *)(d->backBuf + (y + row) * pitch + cx * 4);
uint8_t bits = glyph ? glyph[row] : 0;
for (int32_t p = cStart; p < cEnd; p++) {
dst[p] = (bits & (0x80 >> p)) ? fg : bg;
}
}
} else {
uint8_t fg8 = (uint8_t)fg;
uint8_t bg8 = (uint8_t)bg;
for (int32_t row = rowStart; row < rowEnd; row++) {
uint8_t *dst = d->backBuf + (y + row) * pitch + cx;
uint8_t bits = glyph ? glyph[row] : 0;
for (int32_t p = cStart; p < cEnd; p++) {
dst[p] = (bits & (0x80 >> p)) ? fg8 : bg8;
}
}
}
}
}
// ============================================================ // ============================================================
// drawText // drawText
// ============================================================ // ============================================================

View file

@ -39,6 +39,11 @@ int32_t textWidthAccel(const BitmapFontT *font, const char *text);
// andMask/xorData are arrays of uint16_t, one per row // andMask/xorData are arrays of uint16_t, one per row
void drawMaskedBitmap(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, const uint16_t *andMask, const uint16_t *xorData, uint32_t fgColor, uint32_t bgColor); void drawMaskedBitmap(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, const uint16_t *andMask, const uint16_t *xorData, uint32_t fgColor, uint32_t bgColor);
// Draw a row of terminal character cells (ch/attr pairs with a 16-color palette).
// Renders 'cols' cells starting at (x,y). Much faster than calling drawChar per cell.
// cursorCol: column index to draw inverted (cursor), or -1 for no cursor.
void drawTermRow(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, int32_t cols, const uint8_t *lineData, const uint32_t *palette, bool blinkVisible, int32_t cursorCol);
// Dotted focus rectangle (every other pixel) // Dotted focus rectangle (every other pixel)
void drawFocusRect(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color); void drawFocusRect(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color);

View file

@ -327,6 +327,9 @@ typedef struct WidgetT {
uint32_t dirtyRows; // bitmask of rows needing repaint uint32_t dirtyRows; // bitmask of rows needing repaint
int32_t lastCursorRow; // cursor row at last repaint int32_t lastCursorRow; // cursor row at last repaint
int32_t lastCursorCol; // cursor col at last repaint int32_t lastCursorCol; // cursor col at last repaint
// Cached packed palette (avoids packColor per repaint)
uint32_t packedPalette[16];
bool paletteValid;
// Communications interface (all NULL = disconnected) // Communications interface (all NULL = disconnected)
void *commCtx; void *commCtx;
int32_t (*commRead)(void *ctx, uint8_t *buf, int32_t maxLen); int32_t (*commRead)(void *ctx, uint8_t *buf, int32_t maxLen);
@ -519,8 +522,9 @@ int32_t wgtAnsiTermPoll(WidgetT *w);
// Fast repaint: renders only dirty rows directly into the window's content // Fast repaint: renders only dirty rows directly into the window's content
// buffer, bypassing the full widget paint pipeline. Returns number of rows // buffer, bypassing the full widget paint pipeline. Returns number of rows
// repainted (0 if nothing was dirty). // repainted (0 if nothing was dirty). If outY/outH are non-NULL, they receive
int32_t wgtAnsiTermRepaint(WidgetT *w); // the content-buffer-relative Y and height of the repainted region.
int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH);
// ============================================================ // ============================================================
// Operations // Operations

View file

@ -60,6 +60,7 @@ static const int32_t sAnsiToCga[8] = { 0, 4, 2, 6, 1, 5, 3, 7 };
// ============================================================ // ============================================================
static void ansiTermAddToScrollback(WidgetT *w, int32_t screenRow); static void ansiTermAddToScrollback(WidgetT *w, int32_t screenRow);
static void ansiTermBuildPalette(WidgetT *w, const DisplayT *d);
static void ansiTermDeleteLines(WidgetT *w, int32_t count); static void ansiTermDeleteLines(WidgetT *w, int32_t count);
static void ansiTermDirtyRange(WidgetT *w, int32_t startCell, int32_t count); static void ansiTermDirtyRange(WidgetT *w, int32_t startCell, int32_t count);
static void ansiTermDirtyRow(WidgetT *w, int32_t row); static void ansiTermDirtyRow(WidgetT *w, int32_t row);
@ -104,6 +105,27 @@ static void ansiTermAddToScrollback(WidgetT *w, int32_t screenRow) {
} }
// ============================================================
// ansiTermBuildPalette
// ============================================================
//
// Build the packed 16-color palette and cache it in the widget.
// Only recomputed when paletteValid is false (first use or
// after a display format change).
static void ansiTermBuildPalette(WidgetT *w, const DisplayT *d) {
if (w->as.ansiTerm.paletteValid) {
return;
}
for (int32_t i = 0; i < 16; i++) {
w->as.ansiTerm.packedPalette[i] = packColor(d, sCgaPalette[i][0], sCgaPalette[i][1], sCgaPalette[i][2]);
}
w->as.ansiTerm.paletteValid = true;
}
// ============================================================ // ============================================================
// ansiTermDirtyRange // ansiTermDirtyRange
// ============================================================ // ============================================================
@ -1164,7 +1186,7 @@ int32_t wgtAnsiTermPoll(WidgetT *w) {
// no relayout, no other widgets). This keeps ACK turnaround fast // no relayout, no other widgets). This keeps ACK turnaround fast
// for the serial link. // for the serial link.
int32_t wgtAnsiTermRepaint(WidgetT *w) { int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH) {
if (!w || w->type != WidgetAnsiTermE || !w->window) { if (!w || w->type != WidgetAnsiTermE || !w->window) {
return 0; return 0;
} }
@ -1216,19 +1238,18 @@ int32_t wgtAnsiTermRepaint(WidgetT *w) {
int32_t cols = w->as.ansiTerm.cols; int32_t cols = w->as.ansiTerm.cols;
int32_t rows = w->as.ansiTerm.rows; int32_t rows = w->as.ansiTerm.rows;
int32_t cellW = font->charWidth;
int32_t cellH = font->charHeight; int32_t cellH = font->charHeight;
int32_t baseX = w->x + ANSI_BORDER; int32_t baseX = w->x + ANSI_BORDER;
int32_t baseY = w->y + ANSI_BORDER; int32_t baseY = w->y + ANSI_BORDER;
// Build palette // Use cached palette
uint32_t palette[16]; ansiTermBuildPalette(w, &cd);
for (int32_t i = 0; i < 16; i++) { const uint32_t *palette = w->as.ansiTerm.packedPalette;
palette[i] = packColor(&cd, sCgaPalette[i][0], sCgaPalette[i][1], sCgaPalette[i][2]);
}
bool viewingLive = (w->as.ansiTerm.scrollPos == w->as.ansiTerm.scrollbackCount); bool viewingLive = (w->as.ansiTerm.scrollPos == w->as.ansiTerm.scrollbackCount);
int32_t repainted = 0; int32_t repainted = 0;
int32_t minRow = rows;
int32_t maxRow = -1;
for (int32_t row = 0; row < rows; row++) { for (int32_t row = 0; row < rows; row++) {
if (!(dirty & (1U << row))) { if (!(dirty & (1U << row))) {
@ -1238,37 +1259,27 @@ int32_t wgtAnsiTermRepaint(WidgetT *w) {
int32_t lineIndex = w->as.ansiTerm.scrollPos + row; int32_t lineIndex = w->as.ansiTerm.scrollPos + row;
const uint8_t *lineData = ansiTermGetLine(w, lineIndex); const uint8_t *lineData = ansiTermGetLine(w, lineIndex);
for (int32_t col = 0; col < cols; col++) { // Cursor column for this row (-1 if cursor not on this row)
uint8_t ch = lineData[col * 2]; int32_t curCol2 = -1;
uint8_t attr = lineData[col * 2 + 1];
uint32_t fg = palette[attr & ATTR_FG_MASK];
uint32_t bg = palette[(attr >> 4) & 0x07];
// Blink: hide text during off phase
if ((attr & ATTR_BLINK_BIT) && !w->as.ansiTerm.blinkVisible) {
fg = bg;
}
if (viewingLive && w->as.ansiTerm.cursorVisible && w->as.ansiTerm.cursorOn && if (viewingLive && w->as.ansiTerm.cursorVisible && w->as.ansiTerm.cursorOn &&
row == w->as.ansiTerm.cursorRow && col == w->as.ansiTerm.cursorCol) { row == w->as.ansiTerm.cursorRow) {
uint32_t tmp = fg; curCol2 = w->as.ansiTerm.cursorCol;
fg = bg;
bg = tmp;
} }
int32_t cx = baseX + col * cellW; drawTermRow(&cd, ops, font, baseX, baseY + row * cellH, cols, lineData, palette, w->as.ansiTerm.blinkVisible, curCol2);
int32_t cy = baseY + row * cellH;
drawChar(&cd, ops, font, cx, cy, (char)ch, fg, bg, true);
}
if (row < minRow) { minRow = row; }
if (row > maxRow) { maxRow = row; }
repainted++; repainted++;
} }
w->as.ansiTerm.dirtyRows = 0; w->as.ansiTerm.dirtyRows = 0;
w->as.ansiTerm.lastCursorRow = w->as.ansiTerm.cursorRow; w->as.ansiTerm.lastCursorRow = w->as.ansiTerm.cursorRow;
w->as.ansiTerm.lastCursorCol = w->as.ansiTerm.cursorCol; w->as.ansiTerm.lastCursorCol = w->as.ansiTerm.cursorCol;
if (outY) { *outY = baseY + minRow * cellH; }
if (outH) { *outH = (maxRow - minRow + 1) * cellH; }
return repainted; return repainted;
} }
@ -1497,12 +1508,9 @@ void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
bevel.width = ANSI_BORDER; bevel.width = ANSI_BORDER;
drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel); drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
// Build the 16-color packed palette for this display format // Build/cache the 16-color packed palette
uint32_t palette[16]; ansiTermBuildPalette(w, d);
const uint32_t *palette = w->as.ansiTerm.packedPalette;
for (int32_t i = 0; i < 16; i++) {
palette[i] = packColor(d, sCgaPalette[i][0], sCgaPalette[i][1], sCgaPalette[i][2]);
}
int32_t cols = w->as.ansiTerm.cols; int32_t cols = w->as.ansiTerm.cols;
int32_t rows = w->as.ansiTerm.rows; int32_t rows = w->as.ansiTerm.rows;
@ -1515,36 +1523,19 @@ void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
// Determine if viewing live terminal or scrollback // Determine if viewing live terminal or scrollback
bool viewingLive = (w->as.ansiTerm.scrollPos == sbCount); bool viewingLive = (w->as.ansiTerm.scrollPos == sbCount);
// Render character cells // Render character cells row by row using bulk renderer
for (int32_t row = 0; row < rows; row++) { for (int32_t row = 0; row < rows; row++) {
int32_t lineIndex = w->as.ansiTerm.scrollPos + row; int32_t lineIndex = w->as.ansiTerm.scrollPos + row;
const uint8_t *lineData = ansiTermGetLine(w, lineIndex); const uint8_t *lineData = ansiTermGetLine(w, lineIndex);
for (int32_t col = 0; col < cols; col++) { // Cursor column for this row (-1 if cursor not on this row)
uint8_t ch = lineData[col * 2]; int32_t curCol = -1;
uint8_t attr = lineData[col * 2 + 1];
uint32_t fg = palette[attr & ATTR_FG_MASK];
uint32_t bg = palette[(attr >> 4) & 0x07];
// Blink: hide text during off phase
if ((attr & ATTR_BLINK_BIT) && !w->as.ansiTerm.blinkVisible) {
fg = bg;
}
// Draw cursor as inverse block (only when viewing live terminal)
if (viewingLive && w->as.ansiTerm.cursorVisible && w->as.ansiTerm.cursorOn && if (viewingLive && w->as.ansiTerm.cursorVisible && w->as.ansiTerm.cursorOn &&
row == w->as.ansiTerm.cursorRow && col == w->as.ansiTerm.cursorCol) { row == w->as.ansiTerm.cursorRow) {
uint32_t tmp = fg; curCol = w->as.ansiTerm.cursorCol;
fg = bg;
bg = tmp;
} }
int32_t cx = baseX + col * cellW; drawTermRow(d, ops, font, baseX, baseY + row * cellH, cols, lineData, palette, w->as.ansiTerm.blinkVisible, curCol);
int32_t cy = baseY + row * cellH;
drawChar(d, ops, font, cx, cy, (char)ch, fg, bg, true);
}
} }
// Draw scrollbar // Draw scrollbar