Sizing the form in the constructor via HandleNeeded was unreliable --
RecalcCellSize runs again during Show with different font metrics,
shrinking FAnsi while the form keeps its stale size. Move the
ClientWidth/ClientHeight assignment into Run after Show, when handles
are fully created and RecalcCellSize has settled. Restore constructor
Width/Height defaults in TKPAnsi for reasonable pre-handle dimensions.
Remove CP437 box-drawing diagnostic from the terminal.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Expose CellWidth/CellHeight properties on TKPAnsi and remove hardcoded
Width/Height from both the TKPAnsi constructor and TMainForm constructor.
TESTMAIN now computes form size from FAnsi.Width/Height after handle
allocation triggers RecalcCellSize. Fix bytesRead in reccom() from
int16_t to uint16_t to match avail/chunk types.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WriteDeferredBuf now dirties the old cursor row before the render
loop so the reverse-video cursor block is erased. FlipToScreen's
ghost cleanup was ineffective because FLastCursorRow was already
updated by WriteDeferredBuf.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ParseDataBuf is now pure CPU work with no GDI calls -- the parsing
loop stays cache-hot in the 486 L1. WriteDeferredBuf parses first,
then renders all dirty rows in one batch. Removes ~105 lines of
inline FLiveDC rendering from 8 methods. Also fixes missing DC
local variable in Paint.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WriteDeferredBuf now acquires a screen DC and renders each character
run via ExtTextOut as it arrives, eliminating the per-row deferred
dirty scan. FlushPendingScrolls coalesces scroll-ups into a single
ScrollDC call. FlipToScreen becomes a lightweight blink/fallback pass.
Removed 50ms render throttle from TESTMAIN -- no longer needed since
characters appear on screen as they are parsed.
Simplified: removed ClearLine (duplicate of AllocLine), DirtyAll,
DirtyRow, GetCursorCol/Row (dead code), FTextBlinkOn (always equal
to FBlinkOn), ParseData (inlined), ETO_CLIPPED (unused), redundant
zero-initializations in constructor. Write and WriteDeferred now
delegate to WriteDeferredBuf.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Relying on CS_OWNDC to retain the font between frames is fragile --
Delphi's Canvas infrastructure can deselect it during paint cycles.
Without the OEM font, ExtTextOut renders with SYSTEM_FONT (ANSI_CHARSET),
causing CP437 box-drawing glyphs to display as accented letters, cell
metrics to mismatch (lines clipped), and cursor position to drift.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The MemDC + BitBlt approach wrote every pixel twice (ExtTextOut to system
memory, then BitBlt to video memory). Now ExtTextOut renders directly to
the CS_OWNDC screen DC, letting the display driver write text straight
into the framebuffer via its optimized raster font path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The 8bpp DIB pipeline (font atlas, nibble lookup table, inline ASM glyph
expansion, SetDIBitsToDevice) is replaced with GDI text output: ExtTextOut
per color run into a memory DC, then BitBlt per row to the screen.
The memory bitmap is in native device format, so BitBlt is a raw copy with
no 8bpp-to-device color conversion. ExtTextOut goes through the display
driver's optimized text path instead of software pixel expansion.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Delphi 1.0 Ofs() rejects dereferenced pointer expressions.
GMEM_FIXED allocations always have offset 0, so hardcode it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace 'les di, FGlyphBuf' with explicit GlyphBase/GlyphSeg locals
that are extracted in Pascal before the ASM block.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Four performance optimizations targeting the hottest paths:
- Parse CSI params (P1/P2) as integers during scan-ahead loop,
eliminating ParseParamBuf call from ExecuteCSI (~200 cycles/seq)
- Replace 16-iteration Pascal nibble table rebuild (64 branch+store)
with 32 straight-line MOV word using precomputed BGBG/BGFG/FGBG/FGFG
- Integrate cursor FG/BG swap into main RenderRow column loop,
removing duplicate nibble rebuild + ASM glyph expansion overlay pass
- Replace byte-at-a-time reccom loop with _fmemcpy block copy split
at ring buffer wrap point, reducing far pointer overhead from O(n) to O(1)
Also includes previously uncommitted space fast-path in RenderRow and
inlined escape sequence handling in ParseDataBuf.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add ReadInputBuf to TKPComm for direct PChar reads up to 2048 bytes,
eliminating short string allocation and 8x fewer ReadComm API calls.
Add ParseDataBuf to TKPAnsi with run batching: scans ahead for printable
text runs, computes colors once per run, fills cells in tight loop
without per-character state/wrap checks.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace FParamStr string with fixed char buffer (FParamBuf/FParamLen)
to eliminate per-character heap allocations during CSI escape sequence
parsing. Replace ParseParams (Copy + StrToIntDef per token) with
ParseParamBuf that parses integers directly from the char buffer with
zero allocations.
Replace FAllDirty in DoScrollUp with FPendingScrolls counter. In
FlipToScreen, coalesce pending scrolls into a single ScrollDC call
that shifts on-screen pixels, then only render the newly exposed
bottom rows. Reduces per-scroll GDI cost from 25 SetDIBitsToDevice
calls to 1-3.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PeekMessage without PM_NOYIELD surrenders the timeslice on every empty
queue check (~55ms per yield in Win16). Adding pm_NoYield keeps the
polling loop hot so keystrokes and serial echoes are processed without
scheduler delays.
FlipToScreen was calling GetDC/ReleaseDC on every loop iteration even
with zero dirty rows. Added early-exit scan before acquiring a DC.
TickBlink was calling FlipToScreen redundantly (main loop also calls it).
Removed the FlipToScreen from TickBlink and reordered the main loop to
TickBlink (dirty only) then FlipToScreen (single render pass).
Also: removed FBlinkOn := True reset from ParseData (was dirtying the
cursor row on every incoming chunk), added WriteDeferred for parse-only
without render, moved FlipToScreen from private to public, added Show
call before entering the polling loop.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The ISR still fills the ring buffer (mandatory for 115200 baud), but the
app now polls ReadComm directly via a PeekMessage loop instead of waiting
for WM_COMMNOTIFY. Blink uses GetTickCount instead of WM_TIMER. This
eliminates all Windows message overhead from the data path while keeping
the message loop alive for keyboard, paint, and scrollbar.
Removed from KPCOMM.PAS: NotifyWndProc, hidden notification window,
RegisterClass/CreateWindow, EnableCommNotification, SetCommEventMask,
DoCommEvent, Process*Notify methods, OnComm/CommEvent/RThreshold/
SThreshold properties, modem shadow state (CTS/DSR/CD).
Removed from KPANSI.PAS: WM_TIMER handler, SetTimer/KillTimer, replaced
with public TickBlink method using GetTickCount at 500ms intervals.
Removed from drv/isr.c: checkNotify function and its call from
isrDispatch. Removed from drv/commdrv.c: pfnPostMessage, all
rxNotifySent/txNotifySent edge-trigger bookkeeping, gutted
enableNotification to a no-op API-compat stub. Removed from
drv/commdrv.h: rxNotifySent/txNotifySent fields (shifts struct layout),
PostMessageProcT typedef, pfnPostMessage extern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove BeginUpdate/EndUpdate batching from TKPAnsi entirely -- Write now
renders immediately via FlipToScreen after every ParseData call. Remove
FPendingScroll (caused rendering deadlock: EndUpdate refused to call
FlipToScreen while FPendingScroll > 0, but only FlipToScreen cleared it).
DoScrollUp simplified to set FAllDirty directly.
CommEvent drain loop retained (required by edge-triggered CN_RECEIVE) but
each chunk renders immediately -- no deferred batching. Edge-triggered
notifications verified starvation-free at all levels: ISR, driver, KPCOMM
dispatch, terminal rendering, and keyboard output path.
Add comprehensive variable comments to all project files: TKPAnsi (44
fields), TKPComm (23 fields), TMainForm (9 fields), PortStateT, and
driver globals.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Data now hits the screen immediately when it arrives — no artificial
delay. The render throttle (RenderTickMs, FLastRenderTick) is removed
entirely. ParseData and EndUpdate call FlipToScreen unconditionally;
callers control batching via BeginUpdate/EndUpdate.
Blink toggle no longer calls DirtyAll. New DirtyBlinkRows method only
marks the cursor row and rows containing blink cells, reducing blink
overhead from ~63ms (25 rows) to ~3ms (1-3 rows) on a 486. Cursor
ghost handling in FlipToScreen dirties the old cursor row when the
cursor moves between rows.
Constant mini-frame values (Stride, CellH, PixSeg, GlyphSeg) are
pushed once before the column loop instead of per-cell, saving 320
push instructions per row.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rewrite RenderRow inner loop: split each glyph byte into two nibbles,
look up 4 pre-resolved palette bytes per nibble from a 64-byte table,
and write as word stores — zero branching in the hot path. Replace 25
per-row GlobalAlloc buffers with a single reusable buffer and move glyph
data into a GlobalAlloc'd block shared with the nibble table. All
arithmetic is 16-bit Word (no Longint). Uses mini-frame technique to
safely access local variables from inline ASM after DS/SI/DI clobber.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Build a monochrome font atlas at startup (BuildAtlas), then render
terminal cells by writing palette indices directly into 8bpp DIB row
buffers (RenderRow). Each dirty row reaches the screen via a single
SetDIBitsToDevice call instead of ~12 GDI calls (TextOut, SetTextColor,
SetBkColor, BitBlt). This reduces per-frame GDI overhead by ~10x,
targeting smooth playback of BBS door games on Win16.
Key changes:
- TTermCell FG/BG from TColor to Byte (palette index 0-15)
- Font atlas: render 256 CP437 glyphs into monochrome bitmap, extract
per-glyph pixel masks via GetBitmapBits
- Per-row 8bpp DIB buffers via GlobalAlloc replace dual memory DCs
- RenderRow: zero-GDI atlas lookup + byte writes with cursor overlay
- FlipToScreen: ScrollDC on screen only, SetDIBitsToDevice per dirty row
- Text blink via FTextBlinkOn + re-render replaces dual-buffer phase swap
- Removed: CreateBuffers, DestroyBuffers, PaintLine, ClearBufRect,
RedrawBuffers, DrawRow, FBufDC/FBufBmp/FBlinkPhase fields
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
TKPAnsi is a TCustomControl descendant providing a visual ANSI terminal
with 16-color palette, scrollback buffer, blinking cursor, and ANSI music.
Supports CSI sequences (cursor movement, erase, SGR colors/attributes,
insert/delete lines/chars, scroll), DEC private modes (wrap, cursor
visibility), and keyboard translation (arrows, function keys, etc.).
Test app (TESTMAIN.PAS) updated to wire TKPAnsi to TKPComm as a
full terminal: received data feeds the terminal display, keystrokes
are sent out the serial port.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>