diff --git a/dvx/dvxApp.c b/dvx/dvxApp.c index 0c381b5..ef21b2d 100644 --- a/dvx/dvxApp.c +++ b/dvx/dvxApp.c @@ -1338,9 +1338,20 @@ static void pollAnsiTermWidgetsWalk(AppContextT *ctx, WidgetT *w, WindowT *win) if (w->type == WidgetAnsiTermE) { wgtAnsiTermPoll(w); - if (wgtAnsiTermRepaint(w) > 0) { + int32_t dirtyY = 0; + int32_t dirtyH = 0; + + if (wgtAnsiTermRepaint(w, &dirtyY, &dirtyH) > 0) { 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); } } diff --git a/dvx/dvxDraw.c b/dvx/dvxDraw.c index b9d8c5a..893dd29 100644 --- a/dvx/dvxDraw.c +++ b/dvx/dvxDraw.c @@ -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 // ============================================================ diff --git a/dvx/dvxDraw.h b/dvx/dvxDraw.h index 73bc68e..9d5c4d1 100644 --- a/dvx/dvxDraw.h +++ b/dvx/dvxDraw.h @@ -39,6 +39,11 @@ int32_t textWidthAccel(const BitmapFontT *font, const char *text); // 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); +// 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) void drawFocusRect(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color); diff --git a/dvx/dvxWidget.h b/dvx/dvxWidget.h index 5a219f2..e9425a5 100644 --- a/dvx/dvxWidget.h +++ b/dvx/dvxWidget.h @@ -327,6 +327,9 @@ typedef struct WidgetT { uint32_t dirtyRows; // bitmask of rows needing repaint int32_t lastCursorRow; // cursor row 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) void *commCtx; 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 // buffer, bypassing the full widget paint pipeline. Returns number of rows -// repainted (0 if nothing was dirty). -int32_t wgtAnsiTermRepaint(WidgetT *w); +// repainted (0 if nothing was dirty). If outY/outH are non-NULL, they receive +// the content-buffer-relative Y and height of the repainted region. +int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH); // ============================================================ // Operations diff --git a/dvx/widgets/widgetAnsiTerm.c b/dvx/widgets/widgetAnsiTerm.c index ed1ad23..4f0f060 100644 --- a/dvx/widgets/widgetAnsiTerm.c +++ b/dvx/widgets/widgetAnsiTerm.c @@ -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 ansiTermBuildPalette(WidgetT *w, const DisplayT *d); static void ansiTermDeleteLines(WidgetT *w, int32_t count); static void ansiTermDirtyRange(WidgetT *w, int32_t startCell, int32_t count); 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 // ============================================================ @@ -1164,7 +1186,7 @@ int32_t wgtAnsiTermPoll(WidgetT *w) { // no relayout, no other widgets). This keeps ACK turnaround fast // 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) { return 0; } @@ -1216,19 +1238,18 @@ int32_t wgtAnsiTermRepaint(WidgetT *w) { int32_t cols = w->as.ansiTerm.cols; int32_t rows = w->as.ansiTerm.rows; - int32_t cellW = font->charWidth; int32_t cellH = font->charHeight; int32_t baseX = w->x + ANSI_BORDER; int32_t baseY = w->y + ANSI_BORDER; - // Build palette - uint32_t palette[16]; - for (int32_t i = 0; i < 16; i++) { - palette[i] = packColor(&cd, sCgaPalette[i][0], sCgaPalette[i][1], sCgaPalette[i][2]); - } + // Use cached palette + ansiTermBuildPalette(w, &cd); + const uint32_t *palette = w->as.ansiTerm.packedPalette; - bool viewingLive = (w->as.ansiTerm.scrollPos == w->as.ansiTerm.scrollbackCount); - int32_t repainted = 0; + bool viewingLive = (w->as.ansiTerm.scrollPos == w->as.ansiTerm.scrollbackCount); + int32_t repainted = 0; + int32_t minRow = rows; + int32_t maxRow = -1; for (int32_t row = 0; row < rows; row++) { if (!(dirty & (1U << row))) { @@ -1238,37 +1259,27 @@ int32_t wgtAnsiTermRepaint(WidgetT *w) { int32_t lineIndex = w->as.ansiTerm.scrollPos + row; const uint8_t *lineData = ansiTermGetLine(w, lineIndex); - for (int32_t col = 0; col < cols; col++) { - uint8_t ch = lineData[col * 2]; - 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 && - row == w->as.ansiTerm.cursorRow && col == w->as.ansiTerm.cursorCol) { - uint32_t tmp = fg; - fg = bg; - bg = tmp; - } - - int32_t cx = baseX + col * cellW; - int32_t cy = baseY + row * cellH; - - drawChar(&cd, ops, font, cx, cy, (char)ch, fg, bg, true); + // Cursor column for this row (-1 if cursor not on this row) + int32_t curCol2 = -1; + if (viewingLive && w->as.ansiTerm.cursorVisible && w->as.ansiTerm.cursorOn && + row == w->as.ansiTerm.cursorRow) { + curCol2 = w->as.ansiTerm.cursorCol; } + drawTermRow(&cd, ops, font, baseX, baseY + row * cellH, cols, lineData, palette, w->as.ansiTerm.blinkVisible, curCol2); + + if (row < minRow) { minRow = row; } + if (row > maxRow) { maxRow = row; } repainted++; } w->as.ansiTerm.dirtyRows = 0; w->as.ansiTerm.lastCursorRow = w->as.ansiTerm.cursorRow; w->as.ansiTerm.lastCursorCol = w->as.ansiTerm.cursorCol; + + if (outY) { *outY = baseY + minRow * cellH; } + if (outH) { *outH = (maxRow - minRow + 1) * cellH; } + return repainted; } @@ -1497,12 +1508,9 @@ void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit bevel.width = ANSI_BORDER; drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel); - // Build the 16-color packed palette for this display format - uint32_t palette[16]; - - for (int32_t i = 0; i < 16; i++) { - palette[i] = packColor(d, sCgaPalette[i][0], sCgaPalette[i][1], sCgaPalette[i][2]); - } + // Build/cache the 16-color packed palette + ansiTermBuildPalette(w, d); + const uint32_t *palette = w->as.ansiTerm.packedPalette; int32_t cols = w->as.ansiTerm.cols; 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 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++) { int32_t lineIndex = w->as.ansiTerm.scrollPos + row; const uint8_t *lineData = ansiTermGetLine(w, lineIndex); - for (int32_t col = 0; col < cols; col++) { - uint8_t ch = lineData[col * 2]; - 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 && - row == w->as.ansiTerm.cursorRow && col == w->as.ansiTerm.cursorCol) { - uint32_t tmp = fg; - fg = bg; - bg = tmp; - } - - int32_t cx = baseX + col * cellW; - int32_t cy = baseY + row * cellH; - - drawChar(d, ops, font, cx, cy, (char)ch, fg, bg, true); + // Cursor column for this row (-1 if cursor not on this row) + int32_t curCol = -1; + if (viewingLive && w->as.ansiTerm.cursorVisible && w->as.ansiTerm.cursorOn && + row == w->as.ansiTerm.cursorRow) { + curCol = w->as.ansiTerm.cursorCol; } + + drawTermRow(d, ops, font, baseX, baseY + row * cellH, cols, lineData, palette, w->as.ansiTerm.blinkVisible, curCol); } // Draw scrollbar