Commit graph

22 commits

Author SHA1 Message Date
a9e25ec67f Re-select OEM font into DC before every render pass
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>
2026-03-02 19:23:22 -06:00
78753a65d8 Render ExtTextOut directly to screen DC, eliminating memory DC intermediate
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>
2026-03-02 19:17:18 -06:00
a7780c8030 Replace DIB pixel rendering with ExtTextOut + memory DC BitBlt
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>
2026-03-02 19:00:58 -06:00
40dabea161 Fix Ofs() syntax: GMEM_FIXED offset is always 0
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>
2026-03-02 18:26:35 -06:00
4bbc382070 Fix Ofs() call: use intermediate variable for typed pointer cast
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 18:25:16 -06:00
e7454f87d1 Fix LES instruction: Delphi 1.0 inline ASM can't resolve object fields
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>
2026-03-02 18:23:22 -06:00
c378abc9e5 Inline CSI parsing, ASM nibble table, cursor dedup, reccom block copy
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>
2026-03-02 18:20:51 -06:00
8e3bad86e3 Bypass 255-byte string limit and batch plain text runs in parser
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>
2026-03-02 17:15:35 -06:00
75d598c4c5 Eliminate heap allocs in ANSI parser, add ScrollDC for scroll rendering
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>
2026-03-02 16:03:45 -06:00
fbf4ed7c40 Fix input lag: add PM_NOYIELD, skip idle GetDC, eliminate redundant renders
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>
2026-03-01 19:28:56 -06:00
ec0ec8f074 Replace event-driven WM_COMMNOTIFY architecture with polling main loop
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>
2026-03-01 19:01:40 -06:00
acf1a6b691 Remove BeginUpdate/EndUpdate, fix rendering starvation, add variable docs
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>
2026-03-01 18:34:19 -06:00
b6f08a3150 Remove render throttle and add smart blink dirty tracking
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>
2026-02-28 18:55:28 -06:00
c5d31ca930 Replace per-pixel branching with nibble lookup table and inline ASM
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>
2026-02-27 18:33:59 -06:00
0ff633f605 Replace GDI rendering with font atlas and direct pixel writes
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>
2026-02-26 23:21:22 -06:00
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
c3ae983a73 Add TKPAnsi ANSI BBS terminal emulation component for Delphi 1.0
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>
2026-02-25 23:12:23 -06:00