Optimize TKPAnsi rendering with batched TextOut and dirty row tracking

Separate parsing from rendering to eliminate per-character GDI calls.
ProcessChar now only updates cell data in memory; rendering is deferred
to FlipToScreen which batches consecutive same-color cells into single
TextOut calls (~5-10 per row instead of 80). Partial BitBlt transfers
only the dirty row band to the screen. Non-blinking rows render to one
buffer and BitBlt to the second, halving GDI work for typical content.

Also removes redundant GetCommError from KPComm receive path and adds
BeginUpdate/EndUpdate batching in the test app's CommEvent handler.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Duensing 2026-02-26 21:15:51 -06:00
parent be566a5767
commit ca99d1d21b
4 changed files with 576 additions and 235 deletions

File diff suppressed because it is too large Load diff

View file

@ -351,7 +351,6 @@ end;
function TKPComm.GetInput: string; function TKPComm.GetInput: string;
var var
Stat: TComStat;
BytesToRead: Integer; BytesToRead: Integer;
BytesRead: Integer; BytesRead: Integer;
Buf: array[0..255] of Char; Buf: array[0..255] of Char;
@ -360,15 +359,12 @@ begin
if not FPortOpen or (FCommId < 0) then if not FPortOpen or (FCommId < 0) then
Exit; Exit;
GetCommError(FCommId, Stat); { Read directly without querying GetCommError first. ReadComm }
BytesToRead := Stat.cbInQue; { returns the number of bytes actually available (up to BytesToRead) }
{ so the extra GetCommError round-trip is unnecessary overhead. }
BytesToRead := 255;
if (FInputLen > 0) and (BytesToRead > FInputLen) then if (FInputLen > 0) and (BytesToRead > FInputLen) then
BytesToRead := FInputLen; BytesToRead := FInputLen;
if BytesToRead > 255 then
BytesToRead := 255;
if BytesToRead <= 0 then
Exit;
BytesRead := ReadComm(FCommId, @Buf, BytesToRead); BytesRead := ReadComm(FCommId, @Buf, BytesToRead);
if BytesRead <= 0 then if BytesRead <= 0 then
@ -558,14 +554,12 @@ end;
procedure TKPComm.ProcessReceiveNotify; procedure TKPComm.ProcessReceiveNotify;
var
Stat: TComStat;
begin begin
if FRThreshold <= 0 then if FRThreshold <= 0 then
Exit; Exit;
GetCommError(FCommId, Stat); { WM_COMMNOTIFY with CN_RECEIVE means data is available -- the driver }
if Integer(Stat.cbInQue) >= FRThreshold then { already checked the threshold. No need to call GetCommError here. }
DoCommEvent(comEvReceive); DoCommEvent(comEvReceive);
end; end;

View file

@ -85,9 +85,19 @@ begin
case FComm.CommEvent of case FComm.CommEvent of
comEvReceive: comEvReceive:
begin begin
S := FComm.Input; { Drain all available data in a single update batch. This }
if Length(S) > 0 then { suppresses per-Write rendering so we get one paint at the }
FAnsi.Write(S); { end instead of one per 255-byte chunk. }
FAnsi.BeginUpdate;
try
repeat
S := FComm.Input;
if Length(S) > 0 then
FAnsi.Write(S);
until Length(S) = 0;
finally
FAnsi.EndUpdate;
end;
end; end;
end; end;
end; end;
@ -131,7 +141,7 @@ begin
FEditSettings.Left := 148; FEditSettings.Left := 148;
FEditSettings.Top := 8; FEditSettings.Top := 8;
FEditSettings.Width := 140; FEditSettings.Width := 140;
FEditSettings.Text := '9600,N,8,1'; FEditSettings.Text := '115200,N,8,1';
FBtnOpen := TButton.Create(Self); FBtnOpen := TButton.Create(Self);
FBtnOpen.Parent := Self; FBtnOpen.Parent := Self;
@ -164,6 +174,15 @@ begin
FAnsi.Left := 0; FAnsi.Left := 0;
FAnsi.Top := 38; FAnsi.Top := 38;
FAnsi.OnKeyData := AnsiKeyData; FAnsi.OnKeyData := AnsiKeyData;
{ Font diagnostic: write known CP437 box-drawing characters. }
{ If the OEM font is working, you should see: }
{ Line 1: single-line box top ┌───┐ }
{ Line 2: shade + full block ░▒▓█ }
{ Line 3: single-line box bottom └───┘ }
{ If you see accented letters (Ú Ä ¿ ° ± ² Û À Ù), the font is }
{ ANSI_CHARSET instead of OEM_CHARSET. }
FAnsi.Write(#$DA#$C4#$C4#$C4#$BF' '#$B0#$B1#$B2#$DB' '#$C0#$C4#$C4#$C4#$D9#13#10);
end; end;

View file

@ -259,6 +259,13 @@ void applyBaudRate(PortStateT *port, uint16_t baud)
base = port->baseAddr; base = port->baseAddr;
divisor = (uint16_t)(BAUD_DIVISOR_BASE / actualBaud); divisor = (uint16_t)(BAUD_DIVISOR_BASE / actualBaud);
// Guard: divisor 0 means the UART treats it as 65536, giving ~1.76 baud.
// This can happen when BuildCommDCB stores a raw truncated value for
// 115200 (e.g. 0xE101 = 57601) and a future rate exceeds 115200.
if (divisor == 0) {
divisor = 1;
}
// Set DLAB to access divisor latch // Set DLAB to access divisor latch
lcr = (uint8_t)_inp(base + UART_LCR); lcr = (uint8_t)_inp(base + UART_LCR);
_outp(base + UART_LCR, lcr | LCR_DLAB); _outp(base + UART_LCR, lcr | LCR_DLAB);
@ -315,6 +322,11 @@ void applyLineParams(PortStateT *port, uint8_t byteSize, uint8_t parity, uint8_t
break; break;
} }
dbgHex16("KPCOMM: applyLine byteSize", (uint16_t)byteSize);
dbgHex16("KPCOMM: applyLine parity", (uint16_t)parity);
dbgHex16("KPCOMM: applyLine stopBits", (uint16_t)stopBits);
dbgHex16("KPCOMM: applyLine LCR", (uint16_t)lcr);
_outp(base + UART_LCR, lcr); _outp(base + UART_LCR, lcr);
port->byteSize = byteSize; port->byteSize = byteSize;
@ -948,10 +960,12 @@ int16_t FAR PASCAL _export inicom(DCB FAR *dcb)
_inp(port->baseAddr + UART_RBR); _inp(port->baseAddr + UART_RBR);
// Populate ComDEB for third-party compatibility // Populate ComDEB for third-party compatibility
port->comDeb.port = port->baseAddr; port->comDeb.port = port->baseAddr;
port->comDeb.baudRate = port->baudRate; port->comDeb.baudRate = port->baudRate;
port->comDeb.qInSize = port->rxSize; port->comDeb.qInSize = port->rxSize;
port->comDeb.qOutSize = port->txSize; port->comDeb.qOutSize = port->txSize;
port->comDeb.lcrShadow = (uint8_t)_inp(port->baseAddr + UART_LCR);
port->comDeb.mcrShadow = (uint8_t)_inp(port->baseAddr + UART_MCR);
// Enable receive and line status interrupts // Enable receive and line status interrupts
_outp(port->baseAddr + UART_IER, IER_RDA | IER_LSI | IER_MSI); _outp(port->baseAddr + UART_IER, IER_RDA | IER_LSI | IER_MSI);
@ -1101,7 +1115,9 @@ void primeTx(PortStateT *port)
// reactivateOpenCommPorts - Reactivate all ports after task switch (ordinal 18) // reactivateOpenCommPorts - Reactivate all ports after task switch (ordinal 18)
// //
// Called by Windows when switching back to this VM. // Called by Windows when switching back to this VM.
// Re-enables interrupts and restores MCR state. // Restores full UART state: baud rate, line params (LCR), MCR, FIFOs,
// and re-enables interrupts. A DOS fullscreen app or VM switch may
// have reprogrammed the UART, so we must restore everything.
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
void FAR PASCAL _export reactivateOpenCommPorts(void) void FAR PASCAL _export reactivateOpenCommPorts(void)
{ {
@ -1117,6 +1133,12 @@ void FAR PASCAL _export reactivateOpenCommPorts(void)
continue; continue;
} }
// Restore baud rate (sets DLAB, writes divisor, clears DLAB)
applyBaudRate(port, port->baudRate);
// Restore line parameters (word length, parity, stop bits)
applyLineParams(port, port->byteSize, port->parity, port->stopBits);
// Restore MCR (DTR, RTS, OUT2) // Restore MCR (DTR, RTS, OUT2)
mcr = MCR_OUT2; mcr = MCR_OUT2;
if (port->dtrState) { if (port->dtrState) {