Compare commits

...

6 commits

Author SHA1 Message Date
cd55adae4f Fix cursor artifacts and WM_TIMER starvation during high-speed data
Three fixes:

1. Cursor font mismatch: cursor overlay TextOut used the default DC
   font instead of FPaintFont (OEM_CHARSET). The wrong font size left
   residual pixels that buffer BitBlt didn't fully cover. Fix: select
   FPaintFont + OPAQUE BkMode into the CS_OWNDC screen DC once in
   RecalcCellSize, and re-assert before cursor TextOut in case VCL
   Paint altered the DC state.

2. WM_TIMER starvation: on Win16, WM_TIMER is lowest-priority and
   never dispatched while WM_COMMNOTIFY floods the queue. Fix: render
   from EndUpdate using GetTickCount throttle (no timer dependency).
   The timer remains as fallback for idle blink and trailing data.

3. Scroll performance: restore deferred ScrollDC at render time (not
   during parsing) so only newly-revealed rows need PaintLine instead
   of all 25. Dirty flags shift with each DoScrollUp to track the
   correct logical line positions after scroll.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 21:40:58 -06:00
c565be8489 Decouple rendering from data processing with timer-based display
Data parsing (ProcessChar) is now purely memory operations. EndUpdate
and ParseData set FRenderPending instead of calling FlipToScreen.
A 55ms timer (~18 Hz, matching Win16 tick resolution) drives rendering:
dirty rows are painted and BitBlt'd to screen only on timer ticks.
Blink toggles every 9 ticks (~500ms). This prevents high-throughput
data (door games, file transfers) from saturating the CPU with GDI
calls — between timer ticks, data just accumulates in cell buffers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 21:28:14 -06:00
a96dbb3faa Fix scroll rendering artifacts by removing ScrollDC
ScrollDC shifted buffer pixels during parsing before dirty rows were
rendered, causing unrendered garbage to propagate into non-dirty rows
on successive scrolls. Replace with FAllDirty to repaint all rows from
cell data after any scroll. With batched TextOut this costs ~250 GDI
calls per scroll instead of the old 12,000, so the overhead is minimal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 21:18:15 -06:00
ca99d1d21b 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>
2026-02-26 21:15:51 -06:00
be566a5767 Fix OEM font rendering and terminal responsiveness in TKPAnsi
Replace TBitmap back buffer with raw GDI memory DC to prevent Delphi's
TCanvas from overriding the OEM_CHARSET font selection. Add OUT_RASTER_PRECIS
to CreateFontIndirect to ensure Windows maps to a true CP437 raster font
instead of a TrueType substitution. Optimize paint loop with fixed char
array instead of string concatenation. Restore Update call in ParseData
so WM_PAINT is not starved by WM_COMMNOTIFY. Add text blink support via
timer-driven FG/BG toggling and store blink as cell attribute instead of
mapping to bright background.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 17:48:20 -06:00
a3c52e0817 Add terminal query responses for BBS ANSI detection
Handle ENQ (0x05), ESC[c (Device Attributes), ESC[5n (Device Status
Report), and ESC[6n (Cursor Position Report).  Responses are sent
back through OnKeyData so BBSes can detect ANSI terminal capability.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 00:06:39 -06:00
5 changed files with 726 additions and 144 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,13 +554,11 @@ 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

@ -116,6 +116,9 @@ CSI (ESC\[) sequences:
| ESC\[*params*m | SGR | Set graphic rendition (see below) | | ESC\[*params*m | SGR | Set graphic rendition (see below) |
| ESC\[s | SCP | Save cursor position | | ESC\[s | SCP | Save cursor position |
| ESC\[u | RCP | Restore cursor position | | ESC\[u | RCP | Restore cursor position |
| ESC\[c | DA | Device Attributes — responds ESC\[?1;0c (VT100) |
| ESC\[5n | DSR | Device Status Report — responds ESC\[0n (OK) |
| ESC\[6n | CPR | Cursor Position Report — responds ESC\[*row*;*col*R |
SGR codes: SGR codes:
@ -148,6 +151,7 @@ Control characters:
| LF (#10) | Line feed (scrolls at bottom) | | LF (#10) | Line feed (scrolls at bottom) |
| BS (#8) | Backspace (no erase) | | BS (#8) | Backspace (no erase) |
| TAB (#9) | Tab to next 8-column stop | | TAB (#9) | Tab to next 8-column stop |
| ENQ (#5) | Answerback — responds ESC\[?1;0c (VT100) |
| BEL (#7) | System beep | | BEL (#7) | System beep |
**ANSI Music:** **ANSI Music:**

View file

@ -85,9 +85,19 @@ begin
case FComm.CommEvent of case FComm.CommEvent of
comEvReceive: comEvReceive:
begin begin
{ Drain all available data in a single update batch. This }
{ suppresses per-Write rendering so we get one paint at the }
{ end instead of one per 255-byte chunk. }
FAnsi.BeginUpdate;
try
repeat
S := FComm.Input; S := FComm.Input;
if Length(S) > 0 then if Length(S) > 0 then
FAnsi.Write(S); 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;
@ -952,6 +964,8 @@ int16_t FAR PASCAL _export inicom(DCB FAR *dcb)
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) {