From 26c3d7440d9b87c3ed602a9d3163232d2ce8a862 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Fri, 20 Mar 2026 20:00:05 -0500 Subject: [PATCH] Updated docs. --- README.md | 311 +++++-- apps/README.md | 342 ++++++- apps/cpanel/cpanel.c | 15 +- dvx/README.md | 2034 +++++++++++++++++------------------------- dvxshell/README.md | 442 ++++++--- packet/README.md | 379 ++++++-- proxy/README.md | 161 ++-- rs232/README.md | 412 ++++++--- seclink/README.md | 369 +++++--- security/README.md | 367 ++++++-- tasks/README.md | 353 ++++---- termdemo/README.md | 96 +- 12 files changed, 3216 insertions(+), 2065 deletions(-) diff --git a/README.md b/README.md index 5e56f00..5d66c2a 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,74 @@ -# DVX -- DOS Visual eXecutive +# DVX -- DOS Visual eXecutive -A Windows 3.x-style desktop shell for DOS, built with DJGPP/DPMI. Combines a -windowed GUI compositor, cooperative task switcher, and DXE3 dynamic loading to -create a multitasking desktop environment where applications are `.app` shared -libraries loaded at runtime. +A windowed GUI compositor and desktop shell for DOS, built with +DJGPP/DPMI. DVX combines a Motif-style window manager, dirty-rectangle +compositor, cooperative task switcher, and DXE3 dynamic application +loading into a multitasking desktop environment where applications are +`.app` shared libraries loaded at runtime. -## Components +Targets real and emulated 486+ hardware with VESA VBE 2.0+ linear +framebuffer. No bank switching -- LFB or fail. -``` -dvxgui/ - dvx/ GUI compositor library -> lib/libdvx.a - tasks/ Cooperative task switcher -> lib/libtasks.a - dvxshell/ Desktop shell -> bin/dvx.exe - apps/ DXE app modules (.app files) -> bin/apps/*/*.app - rs232/ ISR-driven UART serial driver -> lib/librs232.a - packet/ HDLC framing, CRC, Go-Back-N -> lib/libpacket.a - security/ DH key exchange, XTEA-CTR cipher -> lib/libsecurity.a - seclink/ Secure serial link wrapper -> lib/libseclink.a - proxy/ Linux SecLink-to-telnet proxy -> bin/secproxy - termdemo/ Encrypted ANSI terminal demo -> bin/termdemo.exe -``` -## Building +## Features -Requires the DJGPP cross-compiler (`i586-pc-msdosdjgpp-gcc`). +- Motif/GEOS-style beveled window chrome with drag, resize, minimize, + maximize, and restore +- Dirty-rectangle compositor -- only changed regions are flushed to video + memory, critical for acceptable frame rates on 486/Pentium hardware +- 32 widget types: buttons, text inputs, list boxes, tree views, list + views, tab controls, sliders, spinners, progress bars, dropdowns, + combo boxes, splitters, scroll panes, ANSI terminal emulator, and more +- Flexbox-style automatic layout engine (VBox/HBox containers with + weighted space distribution) +- Dropdown menus with cascading submenus, checkbox and radio items, + keyboard accelerators, and context menus +- Modal dialogs: message box (OK/Cancel/Yes/No/Retry) and file + open/save with directory navigation and filter dropdown +- 20-color theme system with live preview and INI-based theme files +- Wallpaper support: stretch, tile, or center with bilinear scaling and + 16bpp ordered dithering +- Live video mode switching without restart +- Mouse wheel support (CuteMouse Wheel API) +- Screenshots (full screen or per-window) saved as PNG +- Clipboard (copy/cut/paste within DVX) +- Timer widget for periodic callbacks +- Cooperative task switcher for apps that need their own main loop +- DXE3 dynamic application loading with crash recovery (SIGSEGV, + SIGFPE, SIGILL caught and isolated per-app) +- INI-based preferences system with typed read/write accessors +- Encrypted serial networking stack (DH key exchange, XTEA-CTR cipher) +- Platform abstraction layer -- DOS/DJGPP production target, Linux/SDL2 + development target -``` -make -C dvx # builds lib/libdvx.a -make -C tasks # builds lib/libtasks.a -make -C dvxshell # builds bin/dvx.exe -make -C apps # builds bin/apps/*/*.app -``` -Set `DJGPP_PREFIX` in the Makefiles if your toolchain is installed somewhere -other than `~/djgpp/djgpp`. +## Target Hardware + +- **CPU**: 486 baseline, Pentium-optimized paths where significant +- **Video**: VESA VBE 2.0+ with linear framebuffer +- **OS**: DOS with DPMI (CWSDPMI or equivalent) +- **Supported depths**: 8, 15, 16, 24, 32 bpp +- **Test platform**: 86Box with PCI video cards + ## Architecture The shell runs as a single DOS executable (`dvx.exe`) that loads -applications dynamically via DJGPP's DXE3 shared library system. Each app -is a `.app` file exporting an `appDescriptor` and `appMain` entry point. +applications dynamically via DJGPP's DXE3 shared library system. ``` +-------------------------------------------------------------------+ -| dvx.exe (Task 0) | -| +-------------+ +-----------+ +----------+ +----------------+ | -| +-------------+ +-----------+ +------------+ | -| | shellMain | | shellApp | | shellExport| | -| | (event loop)| | (lifecycle| | (DXE symbol| | -| | | | + reaper)| | export) | | -| +-------------+ +-----------+ +------------+ | -| | | | | -| +------+-------+ +----+-----+ +----+----+ | -| | libdvx.a | |libtasks.a| | libdxe | | -| | (GUI/widgets)| |(scheduler)| | (DJGPP) | | -| +--------------+ +----------+ +---------+ | +| dvx.exe (Task 0) | +| +-------------+ +-----------+ +------------+ | +| | shellMain | | shellApp | | shellExport| | +| | (event loop)| | (lifecycle| | (DXE symbol| | +| | | | + reaper)| | export) | | +| +-------------+ +-----------+ +------------+ | +| | | | | +| +------+-------+ +----+-----+ +----+----+ | +| | libdvx.a | |libtasks.a| | libdxe | | +| | (GUI/widgets)| |(scheduler)| | (DJGPP) | | +| +--------------+ +----------+ +---------+ | +-------------------------------------------------------------------+ | | +---------+ +---------+ @@ -65,58 +79,199 @@ is a `.app` file exporting an `appDescriptor` and `appMain` entry point. ### App Types -**Callback-only** (`hasMainLoop = false`): `appMain` creates windows, registers -callbacks, and returns. The app lives through event callbacks in the shell's -main loop. No dedicated task or stack needed. +**Callback-only** (`hasMainLoop = false`): `appMain` creates windows, +registers callbacks, and returns. The app lives through event callbacks +in the shell's main loop. No dedicated task or stack needed. -**Main-loop** (`hasMainLoop = true`): A cooperative task is created for the app. -`appMain` runs its own loop calling `tsYield()` to share CPU. Used for apps that -need continuous processing (clocks, terminal emulators, games). +**Main-loop** (`hasMainLoop = true`): A cooperative task is created for +the app. `appMain` runs its own loop calling `tsYield()` to share CPU. +Used for apps that need continuous processing (clocks, terminal +emulators, games). ### Crash Recovery -The shell installs signal handlers for SIGSEGV, SIGFPE, and SIGILL. If an app -crashes, the handler `longjmp`s back to the shell's main loop, the crashed app -is force-killed, and the shell continues running. Diagnostic information -(registers, faulting EIP) is logged to `dvx.log`. +The shell installs signal handlers for SIGSEGV, SIGFPE, and SIGILL. If +an app crashes, the handler `longjmp`s back to the shell's main loop, +the crashed app is force-killed, and the shell continues running. +Diagnostic information (registers, faulting EIP) is logged to `dvx.log`. -## Sample Apps -| App | Type | Description | -|-----|------|-------------| -| `progman.app` | Callback | Program Manager: app launcher grid, Task Manager | -| `notepad.app` | Callback | Text editor with file I/O | -| `clock.app` | Main-loop | Digital clock (multi-instance capable) | -| `dvxdemo.app` | Callback | Widget system showcase | +## Directory Structure -## Target Platform +``` +dvxgui/ + dvx/ GUI compositor library (libdvx.a) + platform/ Platform abstraction (DOS/DJGPP, Linux/SDL2) + widgets/ Widget system (32 types, one file per type) + thirdparty/ stb_image.h, stb_image_write.h + tasks/ Cooperative task switcher (libtasks.a) + thirdparty/ stb_ds.h + dvxshell/ Desktop shell (dvx.exe) + apps/ DXE app modules (.app files) + progman/ Program Manager -- app launcher grid + notepad/ Text editor with file I/O + clock/ Digital clock (multi-instance, main-loop) + dvxdemo/ Widget system showcase / demo app + cpanel/ Control Panel -- themes, wallpaper, video, mouse + imgview/ Image Viewer -- BMP/PNG/JPEG/GIF display + rs232/ ISR-driven UART serial driver (librs232.a) + packet/ HDLC framing, CRC-16, Go-Back-N (libpacket.a) + security/ DH key exchange, XTEA-CTR cipher (libsecurity.a) + seclink/ Secure serial link wrapper (libseclink.a) + proxy/ Linux SecLink-to-telnet proxy (secproxy) + termdemo/ Encrypted ANSI terminal demo (termdemo.exe) + themes/ Color theme files (.thm) + wpaper/ Bundled wallpaper images + bin/ Build output (dvx.exe, apps/, config/) + lib/ Build output (static libraries) + releases/ Release archives +``` + + +## Building + +Requires the DJGPP cross-compiler (`i586-pc-msdosdjgpp-gcc`). + +```bash +# Build everything (dvx lib, tasks lib, shell, all apps) +make + +# Build individual components +make -C dvx # builds lib/libdvx.a +make -C tasks # builds lib/libtasks.a +make -C dvxshell # builds bin/dvx.exe +make -C apps # builds bin/apps/*/*.app + +# Clean all build artifacts +make clean +``` + +Set `DJGPP_PREFIX` in the component Makefiles if your toolchain is +installed somewhere other than `~/djgpp/djgpp`. -- **CPU**: 486 baseline, Pentium-optimized paths where significant -- **Video**: VESA VBE 2.0+ with linear framebuffer -- **OS**: DOS with DPMI (CWSDPMI or equivalent) -- **Test platform**: 86Box ## Deployment +### CD-ROM ISO (86Box) + +The primary deployment method is an ISO image mounted as a CD-ROM in +86Box: + +```bash +./mkcd.sh ``` + +This builds everything, then creates an ISO 9660 image with 8.3 +filenames at `~/.var/app/net._86box._86Box/data/86Box/dvx.iso`. Mount +it in 86Box as a CD-ROM drive and run `D:\DVX.EXE` (or whatever drive +letter is assigned). + +### Floppy Image + +For boot floppy setups with CD-ROM drivers: + +```bash mcopy -o -i bin/dvx.exe ::DVX.EXE mcopy -s -o -i bin/apps ::APPS +mcopy -s -o -i bin/config ::CONFIG ``` -The `apps/` directory structure must be preserved on the target -- Program Manager -recursively scans `apps/` for `.app` files at startup. +The `apps/` and `config/` directory structures must be preserved on the +target -- Program Manager recursively scans `apps/` for `.app` files at +startup. -## Documentation + +## Configuration + +DVX reads its configuration from `CONFIG\DVX.INI` on the target +filesystem. The INI file uses a standard `[section]` / `key = value` +format. Settings are applied at startup and can be changed live from the +Control Panel app. + +```ini +[video] +width = 640 +height = 480 +bpp = 16 + +[mouse] +wheel = normal +doubleclick = 500 +acceleration = medium + +[desktop] +wallpaper = C:\DVX\WPAPER\SWOOP.JPG +wallpaperMode = stretch +theme = C:\DVX\THEMES\WIN31.THM +``` + +### Video Section + +- `width`, `height` -- requested resolution (closest VESA mode is used) +- `bpp` -- preferred color depth (8, 15, 16, 24, or 32) + +### Mouse Section + +- `wheel` -- `normal` or `reversed` +- `doubleclick` -- double-click speed in milliseconds (200--900) +- `acceleration` -- `off`, `low`, `medium`, or `high` + +### Desktop Section + +- `wallpaper` -- path to wallpaper image (BMP, PNG, JPEG, GIF) +- `wallpaperMode` -- `stretch`, `tile`, or `center` +- `theme` -- path to color theme file (.thm) + + +## Bundled Applications + +| App | File | Type | Description | +|-----|------|------|-------------| +| Program Manager | `progman.app` | Callback | App launcher grid with icons; also provides the Task Manager (Ctrl+Esc) for switching between running apps | +| Notepad | `notepad.app` | Callback | Text editor with File/Edit menus, open/save dialogs, clipboard, and undo | +| Clock | `clock.app` | Main-loop | Digital clock display; multi-instance capable | +| DVX Demo | `dvxdemo.app` | Callback | Widget system showcase demonstrating all 32 widget types | +| Control Panel | `cpanel.app` | Callback | System settings: color themes with live preview, wallpaper selection, video mode switching, mouse configuration | +| Image Viewer | `imgview.app` | Callback | Displays BMP, PNG, JPEG, and GIF images with file dialog | + + +## Serial / Networking Stack + +A layered encrypted serial communications stack for connecting DVX to +remote systems (BBS, etc.) through 86Box's emulated UART: + +| Layer | Library | Description | +|-------|---------|-------------| +| rs232 | `librs232.a` | ISR-driven UART driver with FIFO support, automatic UART type detection (8250 through 16750), configurable baud rate | +| packet | `libpacket.a` | HDLC framing with byte stuffing, CRC-16 integrity checks, Go-Back-N sliding window for reliable delivery, 255-byte max payload | +| security | `libsecurity.a` | 1024-bit Diffie-Hellman key exchange (RFC 2409 Group 2), XTEA-CTR stream cipher, XTEA-CTR DRBG random number generator | +| seclink | `libseclink.a` | Convenience wrapper: multiplexed channels (0--127), per-packet encryption flag, bulk send helper | +| proxy | `secproxy` | Linux-side bridge: 86Box emulated serial port <-> secLink <-> telnet BBS; socket shim replaces rs232 API | + + +## Third-Party Dependencies + +All third-party code is vendored as single-header libraries with no +external dependencies: + +| Library | Location | Purpose | +|---------|----------|---------| +| stb_image.h | `dvx/thirdparty/` | Image loading (BMP, PNG, JPEG, GIF) | +| stb_image_write.h | `dvx/thirdparty/` | Image writing (PNG export for screenshots) | +| stb_ds.h | `tasks/thirdparty/` | Dynamic array and hash map (used by task manager) | + + +## Component Documentation Each component directory has its own README with detailed API reference: -- [`dvx/README.md`](dvx/README.md) -- GUI library architecture and API -- [`tasks/README.md`](tasks/README.md) -- Task switcher API -- [`dvxshell/README.md`](dvxshell/README.md) -- Shell internals and DXE app contract -- [`apps/README.md`](apps/README.md) -- Writing DXE applications -- [`rs232/README.md`](rs232/README.md) -- Serial port driver -- [`packet/README.md`](packet/README.md) -- Packet transport protocol -- [`security/README.md`](security/README.md) -- Cryptographic primitives -- [`seclink/README.md`](seclink/README.md) -- Secure serial link -- [`proxy/README.md`](proxy/README.md) -- Linux SecLink proxy -- [`termdemo/README.md`](termdemo/README.md) -- Encrypted terminal demo +- [`dvx/README.md`](dvx/README.md) -- GUI library architecture and API +- [`tasks/README.md`](tasks/README.md) -- Task switcher API +- [`dvxshell/README.md`](dvxshell/README.md) -- Shell internals and DXE app contract +- [`apps/README.md`](apps/README.md) -- Writing DXE applications +- [`rs232/README.md`](rs232/README.md) -- Serial port driver +- [`packet/README.md`](packet/README.md) -- Packet transport protocol +- [`security/README.md`](security/README.md) -- Cryptographic primitives +- [`seclink/README.md`](seclink/README.md) -- Secure serial link +- [`proxy/README.md`](proxy/README.md) -- Linux SecLink proxy +- [`termdemo/README.md`](termdemo/README.md) -- Encrypted terminal demo diff --git a/apps/README.md b/apps/README.md index 79055c0..9047f10 100644 --- a/apps/README.md +++ b/apps/README.md @@ -1,9 +1,51 @@ # DVX Shell Applications -DXE3 shared library applications loaded at runtime by the DVX Shell. Each app -is a `.app` file (DXE3 format) placed under the `apps/` directory tree. The -Program Manager scans this directory recursively and displays all discovered -apps. +DXE3 application modules for the DVX Shell. Each app is a `.app` file (DXE3 +shared object format) placed in a subdirectory under `apps/`. The Program +Manager scans this directory recursively at startup and displays all discovered +apps in a launcher grid. + +## App Contract + +Every DXE app must export two symbols and may optionally export a third: + +```c +// Required: app metadata +AppDescriptorT appDescriptor = { + .name = "My App", + .hasMainLoop = false, + .multiInstance = false, + .stackSize = SHELL_STACK_DEFAULT, + .priority = TS_PRIORITY_NORMAL +}; + +// Required: entry point -- called once by the shell after dlopen +int32_t appMain(DxeAppContextT *ctx); + +// Optional: graceful shutdown hook -- called before force-kill +void appShutdown(void); +``` + +`appMain` receives a `DxeAppContextT` with: + +| Field | Type | Description | +|-------|------|-------------| +| `shellCtx` | `AppContextT *` | The shell's GUI context -- pass to all `dvx*`/`wgt*` calls | +| `appId` | `int32_t` | This app's unique ID (1-based slot index; 0 = shell) | +| `appDir` | `char[260]` | Directory containing the `.app` file (for relative resource paths) | +| `configDir` | `char[260]` | Writable config directory (`CONFIG//`) | + +Return 0 from `appMain` on success, non-zero on failure (shell will unload). + +### AppDescriptorT Fields + +| Field | Type | Description | +|-------|------|-------------| +| `name` | `char[64]` | Display name shown in Task Manager and title bars | +| `hasMainLoop` | `bool` | `false` = callback-only (runs in task 0); `true` = gets own cooperative task | +| `multiInstance` | `bool` | `true` = allow multiple instances via temp file copy | +| `stackSize` | `int32_t` | Task stack in bytes; `SHELL_STACK_DEFAULT` (0) for the default | +| `priority` | `int32_t` | `TS_PRIORITY_LOW`, `TS_PRIORITY_NORMAL`, or `TS_PRIORITY_HIGH` | ## Building @@ -12,17 +54,237 @@ make # builds all .app files into ../bin/apps// make clean # removes objects and binaries ``` -Requires `lib/libdvx.a`, `lib/libtasks.a`, and the DXE3 tools (`dxe3gen`) -from the DJGPP toolchain. +Each app is compiled to an object file with the DJGPP cross-compiler, then +packaged into a `.app` via `dxe3gen`: -## Applications +```makefile +$(BINDIR)/myapp/myapp.app: $(OBJDIR)/myapp.o | $(BINDIR)/myapp + $(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $< +``` -| App | File | Type | Description | -|-----|------|------|-------------| -| Program Manager | `progman/progman.c` | Callback | Desktop app: app launcher grid, Task Manager (Ctrl+Esc), window management | -| Notepad | `notepad/notepad.c` | Callback | Text editor with file I/O, dirty tracking via hash | -| Clock | `clock/clock.c` | Main-loop | Digital clock, multi-instance capable | -| DVX Demo | `dvxdemo/dvxdemo.c` | Callback | Widget system showcase with all widget types | +- `-E _appDescriptor` and `-E _appMain` export the required symbols (COFF + underscore prefix). +- `-E _appShutdown` is added for apps that export a shutdown hook (e.g., + Clock). +- `-U` marks all other symbols as unresolved imports to be resolved from the + shell's export table at dlopen time. + +Requires `lib/libdvx.a`, `lib/libtasks.a`, and the DXE3 tools from the DJGPP +toolchain. + +## Directory Structure + +``` +apps/ + Makefile -- build rules for all apps + README.md -- this file + progman/ + progman.c -- Program Manager (desktop shell) + notepad/ + notepad.c -- text editor + clock/ + clock.c -- digital clock + cpanel/ + cpanel.c -- Control Panel (system settings) + imgview/ + imgview.c -- image viewer + dvxdemo/ + dvxdemo.c -- widget showcase + *.bmp -- toolbar icons and sample images +``` + +Each app lives in its own subdirectory. The subdirectory name becomes the +output path under `bin/apps/` (e.g., `bin/apps/progman/progman.app`). + +## Bundled Applications + +### Program Manager (`progman/`) + +The desktop shell and default app launcher. Loaded automatically by the shell +at startup as the "desktop app" -- closing it prompts to exit the entire DVX +Shell. + +- **App launcher grid**: scans `apps/` recursively for `.app` files (skipping + itself), displays them as a grid of buttons. Click or double-click to + launch. +- **Menu bar**: File (Run..., Exit Shell), Options (Minimize on Run), Window + (Cascade, Tile, Tile H, Tile V), Help (About, System Information, Task + Manager). +- **Minimize on Run**: optional preference -- when enabled, Program Manager + minimizes itself after launching an app, getting out of the way. +- **Status bar**: shows the count of running applications, updated in + real-time via `shellRegisterDesktopUpdate()`. +- **System Information**: opens a read-only text area showing CPU, memory, + drive, and video details gathered by the platform layer. +- **Preferences**: saved to `CONFIG/PROGMAN/progman.ini` via the standard + prefs system. Currently stores the "Minimize on Run" setting. + +Type: callback-only. Single instance. + +### Notepad (`notepad/`) + +A basic text editor with file I/O and dirty-change tracking. + +- **TextArea widget**: handles all editing -- keyboard input, cursor movement, + selection, scrolling, word wrap, copy/paste, undo (Ctrl+Z). Notepad only + wires up menus and file I/O around it. +- **File menu**: New, Open, Save, Save As, Exit. +- **Edit menu**: Cut, Copy, Paste, Select All (Ctrl+X/C/V/A). +- **CR/LF handling**: files are opened in binary mode to avoid DJGPP's + translation. `platformStripLineEndings()` normalizes on load; + `platformLineEnding()` writes platform-native line endings on save. +- **Dirty tracking**: uses djb2-xor hash of the text content. Cheap detection + without storing a full shadow buffer. Prompts "Save changes?" on close/new/ + open if dirty. +- **32 KB text buffer**: keeps memory bounded on DOS. Larger files are + silently truncated on load. +- **Multi-instance**: each instance gets its own DXE code+data via temp file + copy. Window positions are offset +20px so instances cascade naturally. + +Type: callback-only. Multi-instance. + +### Clock (`clock/`) + +A digital clock displaying 12-hour time and date, centered in a small +non-resizable window. + +- **Main-loop app**: polls `time()` each iteration, repaints when the second + changes, then calls `tsYield()`. CPU usage is near zero because the check + is cheap and yields immediately when nothing changes. +- **Raw paint callback**: renders directly into the window's content buffer + using `rectFill` and `drawText` -- no widget tree. Demonstrates the + lower-level alternative to the widget system for custom rendering. +- **Shutdown hook**: exports `appShutdown()` so the shell can signal a clean + exit when force-killing via Task Manager or during shell shutdown. +- **Low priority**: uses `TS_PRIORITY_LOW` since clock updates are cosmetic + and should never preempt interactive apps. + +Type: main-loop. Multi-instance. + +### Control Panel (`cpanel/`) + +System configuration with four tabs, all changes previewing live. OK saves to +`CONFIG/DVX.INI`; Cancel reverts to the state captured when the panel opened. + +**Mouse tab:** +- Scroll wheel direction (Normal / Reversed) via dropdown. +- Double-click speed (200-900 ms) via slider with numeric label and test + button. +- Mouse acceleration (Off / Low / Medium / High) via dropdown. +- All changes apply immediately via `dvxSetMouseConfig()`. + +**Colors tab:** +- List of all 20 system colors (`ColorCountE` entries from `dvxColorName`). +- RGB sliders (0-255) for the selected color, with numeric labels and a + canvas swatch preview. +- Changes apply live via `dvxSetColor()` -- the entire desktop updates in + real time. +- **Themes**: dropdown of `.thm` files from `CONFIG/THEMES/`, with Apply, + Load..., Save As..., and Reset buttons. Themes are loaded/saved via + `dvxLoadTheme()`/`dvxSaveTheme()`. +- Reset restores the compiled-in default color scheme. + +**Desktop tab:** +- Wallpaper list: scans `CONFIG/WPAPER/` for BMP/JPG/PNG files. +- Apply, Browse..., and Clear buttons. +- Mode dropdown: Stretch, Tile, Center. Changes apply live via + `dvxSetWallpaperMode()`. + +**Video tab:** +- List of all enumerated VESA modes with human-readable depth names (e.g., + "800x600 65 thousand colors"). +- Apply Mode button or double-click to switch. +- **10-second confirmation dialog**: after switching, a modal "Keep this + mode?" dialog counts down. If the user clicks Yes, the mode is kept. + Clicking No, closing the dialog, or letting the timer expire reverts to + the previous mode. Prevents being stuck in an unsupported mode. + +Type: callback-only. Single instance. + +### Image Viewer (`imgview/`) + +Displays BMP, PNG, JPG, and GIF images loaded via stb_image. + +- **Bilinear scaling**: images are scaled to fit the window while preserving + aspect ratio. The scaler converts from RGB to the native pixel format + (8/16/32 bpp) during the scale pass. +- **Deferred resize**: during a window drag-resize, the old scaled image is + shown. The expensive bilinear rescale only runs after the drag ends + (`sAc->stack.resizeWindow < 0`), avoiding per-frame scaling lag. +- **Responsive scaling**: for large images, `dvxUpdate()` is called every 32 + scanlines during the scale loop to keep the UI responsive. +- **File menu**: Open (Ctrl+O), Close. Keyboard accelerator table registered. +- **Multi-instance**: multiple viewers can be open simultaneously, each with + its own image. +- **Raw paint callback**: renders directly into the content buffer with a dark + gray background and centered blit of the scaled image. + +Type: callback-only. Multi-instance. + +### DVX Demo (`dvxdemo/`) + +A comprehensive widget showcase and integration test. Opens several windows +demonstrating the full DVX widget system: + +- **Main window**: three raw-paint windows -- text rendering with full menu + bar/accelerators/context menu, vertical gradient, and checkerboard pattern + with scrollbars. +- **Widget Demo window**: form pattern with labeled inputs (text, password, + masked phone number), checkboxes, radio groups, single and multi-select + list boxes with context menus and drag reorder. +- **Advanced Widgets window**: nine tab pages covering every widget type -- + dropdown, combo box, progress bar (horizontal and vertical), slider, + spinner, tree view (with drag reorder), multi-column list view (with + multi-select and drag reorder), scroll pane, toolbar (with image buttons + and text fallback), image from file, text area, canvas (with mouse + drawing), splitter (nested horizontal+vertical for explorer-style layout), + and a disabled-state comparison of all widget types. +- **ANSI Terminal window**: terminal emulator widget with sample output + demonstrating bold, reverse, blink, all 16 colors, background colors, + CP437 box-drawing characters, and 500-line scrollback. + +Type: callback-only. Single instance. + +## App Preferences + +Apps that need persistent settings use the shell's config directory system: + +```c +// In appMain: +shellEnsureConfigDir(ctx); // create CONFIG// if needed + +char path[260]; +shellConfigPath(ctx, "settings.ini", path, sizeof(path)); +prefsLoad(path); + +// Read/write: +int32_t val = prefsGetInt("section", "key", defaultVal); +prefsSetInt("section", "key", newVal); +prefsSave(); +``` + +The preferences system handles INI file format with `[section]` headers and +`key=value` pairs. Missing files or keys silently return defaults. + +## Event Model + +DVX apps receive events through two mechanisms: + +**Widget callbacks** (high-level): +- `onClick`, `onDblClick`, `onChange` on individual widgets. +- The widget system handles focus, tab order, mouse hit testing, keyboard + dispatch, and repainting automatically. +- Used by most apps for standard UI (buttons, inputs, lists, sliders, etc.). + +**Window callbacks** (low-level): +- `onPaint(win, dirtyRect)` -- render directly into the window's content + buffer. Used by Clock, Image Viewer, and the DVX Demo paint windows. +- `onClose(win)` -- window close requested (close gadget, Alt+F4). +- `onResize(win, contentW, contentH)` -- window was resized. +- `onMenu(win, menuId)` -- menu item selected or keyboard accelerator fired. + +Both mechanisms can be mixed in the same app. For example, DVX Demo uses +widgets in some windows and raw paint callbacks in others. ## Writing a New App @@ -86,6 +348,11 @@ AppDescriptorT appDescriptor = { static bool sQuit = false; static void onClose(WindowT *win) { + (void)win; + sQuit = true; +} + +void appShutdown(void) { sQuit = true; } @@ -111,16 +378,25 @@ int32_t appMain(DxeAppContextT *ctx) { ### Adding to the build -Add your app directory and source to `apps/Makefile`. Each app is compiled to -an object file, then linked into a `.app` via `dxe3gen`: +Add your app directory and source to `apps/Makefile`: ```makefile -$(BIN_DIR)/myapp.app: $(OBJ_DIR)/myapp/myapp.o - $(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $< +APPS = ... myapp + +myapp: $(BINDIR)/myapp/myapp.app + +$(BINDIR)/myapp/myapp.app: $(OBJDIR)/myapp.o | $(BINDIR)/myapp + $(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $< + +$(OBJDIR)/myapp.o: myapp/myapp.c | $(OBJDIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(BINDIR)/myapp: + mkdir -p $(BINDIR)/myapp ``` -The `-E` flags export the required symbols. `-U` marks unresolved symbols as -imports to be resolved from the shell's export table at load time. +Add `-E _appShutdown` to the `dxe3gen` line if the app exports a shutdown +hook. ## App Guidelines @@ -128,14 +404,22 @@ imports to be resolved from the shell's export table at load time. `SHELL_STACK_DEFAULT`. - Use `ctx->shellCtx` (the `AppContextT *`) for all DVX API calls. - Callback-only apps must destroy their own windows in `onClose` via - `dvxDestroyWindow()`. The shell detects the last window closing and - reaps the app. + `dvxDestroyWindow()`. The shell detects the last window closing and reaps + the app automatically. - Main-loop apps must call `tsYield()` regularly. A task that never yields - blocks the entire system. -- Use file-scoped `static` variables for app state. Each DXE has its own data - segment, so there is no collision between apps. -- Set `multiInstance = true` in the descriptor if the app can safely run - multiple copies simultaneously. -- Avoid `static inline` functions in shared headers. Code inlined into the DXE - binary cannot be updated without recompiling the app. Use macros for trivial - expressions or regular functions exported through the shell's DXE table. + blocks the entire system (cooperative multitasking -- no preemption). +- Export `appShutdown()` for main-loop apps so the shell can signal a clean + exit on force-kill or shell shutdown. +- Use file-scoped `static` variables for app state. Each DXE has its own + data segment, so there is no collision between apps even with identical + variable names. +- Set `multiInstance = true` only if the app can safely run multiple copies + simultaneously. Each instance gets independent globals and statics. +- Avoid `static inline` functions in shared headers. Code inlined into the + DXE binary cannot be updated without recompiling the app. Use macros for + trivial expressions or regular functions exported through the shell's DXE + table. +- Use `ctx->appDir` for loading app-relative resources (icons, data files). + The working directory is shared by all apps and belongs to the shell. +- Use `shellEnsureConfigDir()` + `shellConfigPath()` for persistent settings. + Never write to the app's own directory -- use `CONFIG//` instead. diff --git a/apps/cpanel/cpanel.c b/apps/cpanel/cpanel.c index c84976a..4ceefba 100644 --- a/apps/cpanel/cpanel.c +++ b/apps/cpanel/cpanel.c @@ -878,13 +878,16 @@ static void restoreSnapshot(void) { dvxChangeVideoMode(sAc, sSavedVideoW, sSavedVideoH, sSavedVideoBpp); } - // Restore wallpaper mode and image - sAc->wallpaperMode = sSavedWpMode; + // Restore wallpaper only if path or mode changed + if (strcmp(sAc->wallpaperPath, sSavedWallpaperPath) != 0 || + sAc->wallpaperMode != sSavedWpMode) { + sAc->wallpaperMode = sSavedWpMode; - if (sSavedWallpaperPath[0]) { - dvxSetWallpaper(sAc, sSavedWallpaperPath); - } else { - dvxSetWallpaper(sAc, NULL); + if (sSavedWallpaperPath[0]) { + dvxSetWallpaper(sAc, sSavedWallpaperPath); + } else { + dvxSetWallpaper(sAc, NULL); + } } } diff --git a/dvx/README.md b/dvx/README.md index 36d5c71..9a06715 100644 --- a/dvx/README.md +++ b/dvx/README.md @@ -1,92 +1,109 @@ -# DVX GUI +# DVX GUI Library (libdvx.a) -A DOS Visual eXecutive windowed GUI compositor for DOS, targeting DJGPP/DPMI with -VESA VBE 2.0+ linear framebuffer. +The core GUI compositor library for DVX. Provides VESA video setup, +2D drawing primitives, dirty-rectangle compositing, a full window +manager with Motif-style chrome, and a 32-type widget toolkit with +automatic layout. Applications include `dvxApp.h` (which pulls in all +lower layers) and optionally `dvxWidget.h` for the widget system. + + +## Architecture + +The library is organized in five layers, each a `.h`/`.c` pair. Higher +layers depend on lower ones but never the reverse. + +``` +Layer 5 dvxApp Event loop, mouse/keyboard input, public API +Layer 4 dvxWm Window stack, chrome, drag, resize, focus, menus +Layer 3 dvxComp Dirty rectangle list, merge, LFB flush +Layer 2 dvxDraw Spans, rects, bevels, text, bitmaps (asm inner loops) +Layer 1 dvxVideo VESA init, LFB mapping, backbuffer, pixel format + dvxWidget Widget/layout system (optional, standalone) +``` + + +## File Structure + +### Core Layers + +| File | Purpose | +|------|---------| +| `dvxVideo.h/.c` | Layer 1: VESA VBE mode negotiation, LFB mapping, system RAM backbuffer, pixel format discovery, color packing | +| `dvxDraw.h/.c` | Layer 2: Rectangle fills, bitmap blits, text rendering, bevels, lines, cursor/icon rendering | +| `dvxComp.h/.c` | Layer 3: Dirty rectangle tracking, merge, backbuffer-to-LFB flush | +| `dvxWm.h/.c` | Layer 4: Window lifecycle, Z-order stack, chrome drawing, hit testing, drag/resize/scroll | +| `dvxApp.h/.c` | Layer 5: AppContextT, event loop, window creation, color scheme, wallpaper, screenshots | + +### Supporting Files + +| File | Purpose | +|------|---------| +| `dvxTypes.h` | Shared type definitions used by all layers (PixelFormatT, DisplayT, WindowT, ColorSchemeT, etc.) | +| `dvxWidget.h` | Widget system public API (32 widget types, layout, events) | +| `dvxDialog.h/.c` | Modal dialogs (message box, file open/save) | +| `dvxPrefs.h/.c` | INI-based preferences system (read/write with typed accessors) | +| `dvxFont.h` | Embedded 8x16 VGA bitmap font glyph data (CP437, 256 glyphs) | +| `dvxCursor.h` | Mouse cursor bitmask data (5 shapes: arrow, resize H/V/NWSE/NESW) | +| `dvxPalette.h` | Default VGA palette for 8-bit mode | +| `dvxIcon.c` | stb_image implementation unit (BMP/PNG/JPEG/GIF loading) | +| `dvxImageWrite.c` | stb_image_write implementation unit (PNG export) | + +### Platform Abstraction + +| File | Purpose | +|------|---------| +| `platform/dvxPlatform.h` | OS/CPU-neutral interface: video, input, span ops, filesystem | +| `platform/dvxPlatformDos.c` | DOS/DJGPP: VESA, DPMI, INT 16h/33h, rep stosl/movsd asm spans | + +To port DVX to a new platform, implement a new `dvxPlatformXxx.c` +against `platform/dvxPlatform.h` and swap it in the Makefile. No other +files need modification. + +### Widget System + +| File | Purpose | +|------|---------| +| `widgets/widgetInternal.h` | Shared internal header: vtable type, constants, cross-widget prototypes | +| `widgets/widgetClass.c` | Widget class table: one WidgetClassT vtable entry per type | +| `widgets/widgetCore.c` | Allocation, tree ops, hit testing, flag-based type queries | +| `widgets/widgetLayout.c` | Two-pass layout engine (measure + arrange) | +| `widgets/widgetEvent.c` | Mouse, keyboard, scroll, resize, and paint event dispatch | +| `widgets/widgetOps.c` | Paint dispatcher, public operations (wgtFind, wgtDestroy, etc.) | +| `widgets/widget*.c` | One file per widget type (button, checkbox, slider, etc.) | + +Each widget type is self-contained in its own `.c` file. Dispatch for +paint, layout, mouse, keyboard, getText/setText, and destroy is driven +by a per-type vtable (WidgetClassT) rather than switch statements. +Adding a new widget type requires only a new `.c` file and an entry in +the class table. + +### Third Party + +| File | Purpose | +|------|---------| +| `thirdparty/stb_image.h` | Single-header image loader (BMP, PNG, JPEG, GIF) | +| `thirdparty/stb_image_write.h` | Single-header image writer (PNG) | -Motif-style beveled chrome, dirty-rectangle compositing, draggable and -resizable windows, dropdown menus, scrollbars, and a declarative widget/layout -system with buttons, checkboxes, radios, text inputs, dropdowns, combo boxes, -sliders, spinners, progress bars, tab controls, tree views, list views, -scroll panes, splitters, toolbars, status bars, images, image buttons, -drawable canvases, password inputs, masked/formatted inputs, and an ANSI BBS -terminal emulator. ## Building Requires the DJGPP cross-compiler (`i586-pc-msdosdjgpp-gcc`). -The GUI is built as a static library (`lib/libdvx.a`). The DVX Shell -(`dvxshell/`) and DXE applications (`apps/`) link against it. - -``` +```bash make # builds ../lib/libdvx.a make clean # removes obj/ and lib/ ``` -Set `DJGPP_PREFIX` in the Makefile if your toolchain is installed somewhere -other than `~/djgpp/djgpp`. +Set `DJGPP_PREFIX` in the Makefile if your toolchain is installed +somewhere other than `~/djgpp/djgpp`. -## Architecture - -The library is organized in five layers. Each layer is a `.h`/`.c` pair. -Application code only needs to include `dvxApp.h` (which pulls in the rest) -and optionally `dvxWidget.h`. - -``` -Layer 1 dvxVideo VESA init, LFB mapping, backbuffer, pixel format -Layer 2 dvxDraw Spans, rects, bevels, text, bitmaps (asm inner loops) -Layer 3 dvxComp Dirty rectangle list, merge, LFB flush -Layer 4 dvxWm Window stack, chrome, drag, resize, focus, menus, scrollbars -Layer 5 dvxApp Event loop, mouse/keyboard input, public API - dvxWidget Widget/layout system (optional, standalone) -``` - -Supporting files: - -| File | Purpose | -|------|---------| -| `dvxTypes.h` | Shared type definitions used by all layers | -| `dvxFont.h` | Built-in 8x14 bitmap font glyph data | -| `dvxCursor.h` | Mouse cursor bitmask data (5 shapes) | -| `dvxPalette.h` | Default VGA palette for 8-bit mode | -| `dvxDialog.h` | Modal dialogs (message box, file dialog) | -| `dvxIcon.c` | stb_image implementation unit (BMP/PNG/JPEG/GIF) | -| `dvxImageWrite.c` | stb_image_write implementation unit (PNG export) | -| `thirdparty/stb_image.h` | Third-party single-header image loader | -| `thirdparty/stb_image_write.h` | Third-party single-header image writer | - -The platform abstraction lives in `platform/`: - -| File | Purpose | -|------|---------| -| `platform/dvxPlatform.h` | OS/CPU-neutral interface: video, input, span ops, filename validation | -| `platform/dvxPlatformDos.c` | DOS/DJGPP implementation: VESA, DPMI, INT 16h/33h, rep stosl/movsl | - -To port DVX to a new platform, implement a new `dvxPlatformXxx.c` against -`platform/dvxPlatform.h` and swap it in the Makefile. No other files need -modification. - -The widget system lives in `widgets/`: - -| File | Purpose | -|------|---------| -| `widgets/widgetInternal.h` | Shared internal header: vtable type, constants, cross-widget prototypes | -| `widgets/widgetClass.c` | Widget class table: one `WidgetClassT` vtable entry per widget type | -| `widgets/widgetCore.c` | Allocation, tree ops, hit testing, flag-based type queries | -| `widgets/widgetLayout.c` | Two-pass layout engine (measure + arrange) | -| `widgets/widgetEvent.c` | Mouse, keyboard, scroll, resize, and paint event dispatch | -| `widgets/widgetOps.c` | Paint dispatcher, public operations (`wgtFind`, `wgtDestroy`, etc.) | -| `widgets/widget*.c` | One file per widget type (button, checkbox, slider, etc.) | - -Each widget type is self-contained in its own `.c` file. Dispatch for -paint, layout, mouse, keyboard, getText/setText, and destroy is driven -by a per-type vtable (`WidgetClassT`) rather than switch statements in -the core files. Adding a new widget type requires only a new `.c` file -and an entry in the class table. --- -## Quick start + +## Quick Start + +### Minimal window (raw drawing) ```c #include "dvxApp.h" @@ -105,7 +122,6 @@ static void onPaint(WindowT *win, RectT *dirty) { int main(void) { AppContextT ctx; - if (dvxInit(&ctx, 1024, 768, 16) != 0) { return 1; } @@ -119,12 +135,11 @@ int main(void) { dvxRun(&ctx); dvxShutdown(&ctx); - return 0; } ``` -## Quick start (widgets) +### Widget-based window ```c #include "dvxApp.h" @@ -162,79 +177,94 @@ int main(void) { WidgetT *btn = wgtButton(root, "Go"); btn->onClick = onButtonClick; - wgtInvalidate(root); // initial layout + paint + wgtInvalidate(root); dvxRun(&ctx); dvxShutdown(&ctx); - return 0; } ``` + --- -## Application API (`dvxApp.h`) + +## Application API (dvxApp.h) ### Lifecycle ```c -int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, - int32_t preferredBpp); +int32_t dvxInit(AppContextT *ctx, int32_t requestedW, + int32_t requestedH, int32_t preferredBpp); ``` -Initialize VESA video, input, fonts, color scheme, and cursors. Finds a mode -matching the requested resolution and bit depth (8, 15, 16, or 32). Returns -0 on success, -1 on failure. +Initialize VESA video, input, fonts, color scheme, and cursors. Finds +a mode matching the requested resolution and bit depth. Returns 0 on +success, -1 on failure. ```c void dvxRun(AppContextT *ctx); ``` -Enter the main event loop. Handles mouse movement, button clicks, keyboard -input, window management, dirty-rectangle compositing, and LFB flush. -Returns when `dvxQuit()` is called or ESC is pressed. +Enter the main event loop. Handles mouse, keyboard, window management, +compositing, and LFB flush. Returns when `dvxQuit()` is called. ```c bool dvxUpdate(AppContextT *ctx); ``` -Process one iteration of the event loop: poll input, dispatch events, -composite dirty regions, and flush. Returns `true` if the GUI is still -running, `false` when exit has been requested. Use this instead of -`dvxRun()` when embedding the GUI inside an existing main loop. +Process one iteration of the event loop. Returns `true` if still +running, `false` when exit requested. Use instead of `dvxRun()` when +embedding the GUI in an existing main loop. ```c void dvxShutdown(AppContextT *ctx); ``` -Restore text mode, release LFB mapping, and free the backbuffer. +Restore text mode, release LFB mapping, free the backbuffer. ```c void dvxQuit(AppContextT *ctx); ``` -Request exit from the main loop. `dvxRun()` returns on the next iteration. +Request exit from the main loop. + +### Video Mode + +```c +int32_t dvxChangeVideoMode(AppContextT *ctx, int32_t requestedW, + int32_t requestedH, int32_t preferredBpp); +``` +Switch to a new video mode live. Reallocates the backbuffer, all window +content buffers, repacks colors, rescales wallpaper, and repositions +windows. Returns 0 on success, -1 on failure (old mode restored). + +```c +const VideoModeInfoT *dvxGetVideoModes(const AppContextT *ctx, + int32_t *count); +``` +Return the list of available VESA modes (enumerated at init). ### Windows ```c WindowT *dvxCreateWindow(AppContextT *ctx, const char *title, - int32_t x, int32_t y, int32_t w, int32_t h, - bool resizable); + int32_t x, int32_t y, int32_t w, int32_t h, + bool resizable); +WindowT *dvxCreateWindowCentered(AppContextT *ctx, const char *title, + int32_t w, int32_t h, bool resizable); +void dvxDestroyWindow(AppContextT *ctx, WindowT *win); ``` -Create a window at screen position (`x`, `y`) with outer dimensions `w` x `h`. -If `resizable` is true, the window gets resize handles and a maximize button. -The window is raised to the top and given focus. Returns NULL on failure. After creation, set `win->userData` and install callbacks: -| Callback | Signature | When called | -|----------|-----------|-------------| +| Callback | Signature | When Called | +|----------|-----------|------------| | `onPaint` | `void (WindowT *win, RectT *dirtyArea)` | Content needs redrawing | | `onKey` | `void (WindowT *win, int32_t key, int32_t mod)` | Key press (focused window) | | `onMouse` | `void (WindowT *win, int32_t x, int32_t y, int32_t buttons)` | Mouse event in content area | -| `onResize` | `void (WindowT *win, int32_t newW, int32_t newH)` | Window resized | +| `onResize` | `void (WindowT *win, int32_t newW, int32_t newH)` | Window resized by user | | `onClose` | `void (WindowT *win)` | Close button double-clicked | -| `onMenu` | `void (WindowT *win, int32_t menuId)` | Menu item selected | +| `onMenu` | `void (WindowT *win, int32_t menuId)` | Menu item selected or accelerator triggered | | `onScroll` | `void (WindowT *win, ScrollbarOrientE orient, int32_t value)` | Scrollbar moved | -Mouse/key coordinates are relative to the content area. `buttons` is a -bitmask (bit 0 = left, bit 1 = right, bit 2 = middle). +Mouse/key coordinates are relative to the content area. `buttons` is +a bitmask: `MOUSE_LEFT` (1), `MOUSE_RIGHT` (2), `MOUSE_MIDDLE` (4). Example: @@ -248,52 +278,67 @@ WindowT *win = dvxCreateWindow(&ctx, "My Window", 50, 50, 400, 300, true); win->userData = &ctx; win->onClose = onClose; win->onPaint = myPaintHandler; -win->onMenu = myMenuHandler; ``` -```c -void dvxDestroyWindow(AppContextT *ctx, WindowT *win); -``` -Remove a window from the stack and free its resources. +### Window Properties + +Set directly on WindowT after creation: + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `maxW` | `int32_t` | -1 | Maximum width when maximized (-1 = screen width) | +| `maxH` | `int32_t` | -1 | Maximum height when maximized (-1 = screen height) | +| `modal` | `bool` | false | When true, only this window receives input | +| `userData` | `void *` | NULL | Application data pointer | + +### Window Operations ```c void dvxSetTitle(AppContextT *ctx, WindowT *win, const char *title); -``` -Change the title bar text. - -```c int32_t dvxSetWindowIcon(AppContextT *ctx, WindowT *win, const char *path); -``` -Load a BMP, PNG, JPEG, or GIF image and assign it as the window's minimized icon. -The image is converted to the display pixel format and scaled to 64x64. -Returns 0 on success. - -```c void dvxMinimizeWindow(AppContextT *ctx, WindowT *win); void dvxMaximizeWindow(AppContextT *ctx, WindowT *win); void dvxFitWindow(AppContextT *ctx, WindowT *win); ``` -`dvxFitWindow` resizes the window to fit its widget tree's natural size. -### Window arrangement +`dvxFitWindow` resizes the window to exactly fit its widget tree's +computed minimum size (plus chrome). Useful for dialog boxes. + +### Window Arrangement ```c -dvxCascadeWindows(&ctx); // staggered cascade -dvxTileWindows(&ctx); // grid tile -dvxTileWindowsH(&ctx); // side-by-side -dvxTileWindowsV(&ctx); // top-to-bottom +dvxCascadeWindows(&ctx); // staggered diagonal cascade +dvxTileWindows(&ctx); // NxM grid fill +dvxTileWindowsH(&ctx); // side-by-side, equal width +dvxTileWindowsV(&ctx); // stacked, equal height ``` ### Invalidation ```c void dvxInvalidateRect(AppContextT *ctx, WindowT *win, - int32_t x, int32_t y, int32_t w, int32_t h); + int32_t x, int32_t y, int32_t w, int32_t h); void dvxInvalidateWindow(AppContextT *ctx, WindowT *win); ``` + Mark a region (or the entire content area) as needing repaint. The compositor flushes dirty rectangles to the LFB on the next frame. +### Content Buffer + +Each window has a persistent content backbuffer in display pixel format: + +| Field | Description | +|-------|-------------| +| `contentBuf` | Pixel data in native display format | +| `contentPitch` | Bytes per scanline | +| `contentW` | Width in pixels | +| `contentH` | Height in pixels | + +Paint callbacks write directly into `contentBuf`. The compositor copies +visible portions to the screen backbuffer, then flushes dirty rects to +the LFB. + ### Clipboard ```c @@ -303,13 +348,31 @@ int32_t len; const char *text = dvxClipboardGet(&len); ``` +Process-wide static buffer. Adequate for copy/paste within DVX on +single-tasking DOS. + ### Screenshots ```c -dvxScreenshot(&ctx, "screen.png"); // entire screen -dvxWindowScreenshot(&ctx, win, "window.png"); // single window content +dvxScreenshot(&ctx, "screen.png"); // entire screen +dvxWindowScreenshot(&ctx, win, "window.png"); // single window content ``` +Converts from native pixel format to RGB for the PNG encoder. + +### Image Loading + +```c +uint8_t *dvxLoadImage(const AppContextT *ctx, const char *path, + int32_t *outW, int32_t *outH, int32_t *outPitch); +void dvxFreeImage(uint8_t *data); +int32_t dvxSaveImage(const AppContextT *ctx, const uint8_t *data, + int32_t w, int32_t h, int32_t pitch, const char *path); +``` + +Load BMP/PNG/JPEG/GIF files and convert to the display's native pixel +format. Save native-format pixel data to PNG. + ### Accessors ```c @@ -319,57 +382,176 @@ const BitmapFontT *dvxGetFont(const AppContextT *ctx); const ColorSchemeT *dvxGetColors(const AppContextT *ctx); ``` -### Window properties +### Mouse Configuration -Set these directly on the `WindowT` struct after creation: +```c +void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, + int32_t dblClickMs, int32_t accelThreshold); +``` -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `maxW` | `int32_t` | -1 | Maximum width when maximized (-1 = screen width) | -| `maxH` | `int32_t` | -1 | Maximum height when maximized (-1 = screen height) | -| `userData` | `void *` | NULL | Application data pointer, passed through to callbacks | +`wheelDir`: 1 = normal, -1 = reversed. +`dblClickMs`: double-click speed in milliseconds. +`accelThreshold`: double-speed threshold in mickeys/sec (0 = unchanged). -### Content buffer - -Each window has a persistent content backbuffer: - -| Field | Description | -|-------|-------------| -| `contentBuf` | Pixel data in display format | -| `contentPitch` | Bytes per scanline | -| `contentW` | Width in pixels | -| `contentH` | Height in pixels | - -Paint callbacks write directly into `contentBuf`. The compositor copies -visible portions to the screen backbuffer, then flushes dirty rects to -the LFB. --- -## Menu bars + +## Color System + +DVX uses a 20-color scheme that controls the entire UI appearance. All +colors are pre-packed into native pixel format at init time for +zero-cost per-pixel rendering. + +### ColorSchemeT Fields + +| Field | Usage | +|-------|-------| +| `desktop` | Desktop background fill | +| `windowFace` | Window frame fill | +| `windowHighlight` | Bevel light edge (top/left) | +| `windowShadow` | Bevel dark edge (bottom/right) | +| `activeTitleBg` / `activeTitleFg` | Focused window title bar | +| `inactiveTitleBg` / `inactiveTitleFg` | Unfocused window title bar | +| `contentBg` / `contentFg` | Window content area defaults | +| `menuBg` / `menuFg` | Menu bar and popup menus | +| `menuHighlightBg` / `menuHighlightFg` | Highlighted menu item | +| `buttonFace` | Push button interior | +| `scrollbarBg` / `scrollbarFg` / `scrollbarTrough` | Scrollbar elements | +| `cursorFg` / `cursorBg` | Mouse cursor colors | + +### ColorIdE Enum + +Each color has an integer ID for programmatic access: + +``` +ColorDesktopE, ColorWindowFaceE, ColorWindowHighlightE, +ColorWindowShadowE, ColorActiveTitleBgE, ColorActiveTitleFgE, +ColorInactiveTitleBgE, ColorInactiveTitleFgE, ColorContentBgE, +ColorContentFgE, ColorMenuBgE, ColorMenuFgE, ColorMenuHighlightBgE, +ColorMenuHighlightFgE, ColorButtonFaceE, ColorScrollbarBgE, +ColorScrollbarFgE, ColorScrollbarTroughE, ColorCursorFgE, +ColorCursorBgE, ColorCountE +``` + +### Color API ```c -WindowT *win = dvxCreateWindow(&ctx, "Menus", 50, 50, 400, 300, true); -win->onMenu = onMenuCb; +void dvxSetColor(AppContextT *ctx, ColorIdE id, uint8_t r, uint8_t g, uint8_t b); +void dvxGetColor(const AppContextT *ctx, ColorIdE id, uint8_t *r, uint8_t *g, uint8_t *b); +void dvxApplyColorScheme(AppContextT *ctx); +void dvxResetColorScheme(AppContextT *ctx); +``` +### Theme Files + +Theme files are INI-format with a `[colors]` section containing RGB +values for each of the 20 color IDs: + +```c +bool dvxLoadTheme(AppContextT *ctx, const char *filename); +bool dvxSaveTheme(const AppContextT *ctx, const char *filename); +const char *dvxColorName(ColorIdE id); // INI key name +const char *dvxColorLabel(ColorIdE id); // human-readable label +``` + +Bundled themes: `cde.thm`, `geos.thm`, `win31.thm`. + + +--- + + +## Wallpaper System + +```c +bool dvxSetWallpaper(AppContextT *ctx, const char *path); +void dvxSetWallpaperMode(AppContextT *ctx, WallpaperModeE mode); +``` + +Three display modes: + +| Mode | Enum | Description | +|------|------|-------------| +| Stretch | `WallpaperStretchE` | Bilinear scaling to fill screen; ordered dithering for 16bpp | +| Tile | `WallpaperTileE` | Repeating pattern from top-left | +| Center | `WallpaperCenterE` | Centered with desktop color fill around edges | + +The wallpaper is pre-rendered to screen dimensions in native pixel +format. Pass NULL to `dvxSetWallpaper` to clear. The wallpaper path is +stored so the image can be reloaded after a video mode change. + +Supported formats: BMP, PNG, JPEG, GIF (via stb_image). + + +--- + + +## Preferences System (dvxPrefs.h) + +INI-based configuration with typed read/write accessors and +caller-supplied defaults. + +```c +bool prefsLoad(const char *filename); +bool prefsSave(void); +bool prefsSaveAs(const char *filename); +void prefsFree(void); + +const char *prefsGetString(const char *section, const char *key, const char *defaultVal); +int32_t prefsGetInt(const char *section, const char *key, int32_t defaultVal); +bool prefsGetBool(const char *section, const char *key, bool defaultVal); + +void prefsSetString(const char *section, const char *key, const char *value); +void prefsSetInt(const char *section, const char *key, int32_t value); +void prefsSetBool(const char *section, const char *key, bool value); +void prefsRemove(const char *section, const char *key); +``` + +Only one file may be loaded at a time. If the file is missing or a key +is absent, getters return the caller-supplied default silently. Boolean +values recognize `true`/`yes`/`1` and `false`/`no`/`0`. + + +--- + + +## Menu System + +### Menu Bars + +```c +MenuBarT *wmAddMenuBar(WindowT *win); +MenuT *wmAddMenu(MenuBarT *bar, const char *label); +void wmAddMenuItem(MenuT *menu, const char *label, int32_t id); +void wmAddMenuCheckItem(MenuT *menu, const char *label, int32_t id, bool checked); +void wmAddMenuRadioItem(MenuT *menu, const char *label, int32_t id, bool checked); +void wmAddMenuSeparator(MenuT *menu); +MenuT *wmAddSubMenu(MenuT *parentMenu, const char *label); +``` + +The `&` prefix marks accelerator keys -- `"&File"` means Alt+F opens +the menu. Up to 8 menus per bar, 16 items per menu, submenus nested up +to 4 levels deep. + +After adding menus, call `wmUpdateContentRect(win)` and +`wmReallocContentBuf(win, &ctx.display)` to adjust the content area. + +Example: + +```c MenuBarT *bar = wmAddMenuBar(win); MenuT *fileMenu = wmAddMenu(bar, "&File"); -wmAddMenuItem(fileMenu, "&New", CMD_FILE_NEW); -wmAddMenuItem(fileMenu, "&Open...", CMD_FILE_OPEN); -wmAddMenuItem(fileMenu, "&Save", CMD_FILE_SAVE); +wmAddMenuItem(fileMenu, "&New", CMD_NEW); +wmAddMenuItem(fileMenu, "&Open...", CMD_OPEN); wmAddMenuSeparator(fileMenu); -wmAddMenuItem(fileMenu, "E&xit", CMD_FILE_EXIT); +wmAddMenuItem(fileMenu, "E&xit", CMD_EXIT); MenuT *viewMenu = wmAddMenu(bar, "&View"); -wmAddMenuCheckItem(viewMenu, "Tool&bar", CMD_VIEW_TOOLBAR, true); -wmAddMenuCheckItem(viewMenu, "&Status Bar", CMD_VIEW_STATUSBAR, true); -wmAddMenuSeparator(viewMenu); -wmAddMenuRadioItem(viewMenu, "S&mall", CMD_VIEW_SIZE_SMALL, false); -wmAddMenuRadioItem(viewMenu, "Me&dium", CMD_VIEW_SIZE_MED, true); -wmAddMenuRadioItem(viewMenu, "&Large", CMD_VIEW_SIZE_LARGE, false); +wmAddMenuCheckItem(viewMenu, "Tool&bar", CMD_TOOLBAR, true); +wmAddMenuRadioItem(viewMenu, "&Small", CMD_SMALL, false); +wmAddMenuRadioItem(viewMenu, "&Large", CMD_LARGE, true); -// Cascading submenu MenuT *zoomMenu = wmAddSubMenu(viewMenu, "&Zoom"); wmAddMenuItem(zoomMenu, "Zoom &In", CMD_ZOOM_IN); wmAddMenuItem(zoomMenu, "Zoom &Out", CMD_ZOOM_OUT); @@ -378,111 +560,91 @@ wmUpdateContentRect(win); wmReallocContentBuf(win, &ctx.display); ``` -The `&` prefix marks accelerator keys -- Alt+F opens "&File". -Up to 8 menus per bar, 16 items per menu, submenus nested up to 4 deep. +### Context Menus -Menu callback: +Right-click menus attached to windows or widgets: ```c -static void onMenuCb(WindowT *win, int32_t menuId) { - AppContextT *ctx = (AppContextT *)win->userData; - - switch (menuId) { - case CMD_FILE_EXIT: - dvxQuit(ctx); - break; - } -} -``` - -### Context menus - -Right-click menus can be attached to any window or widget: - -```c -// Window-level context menu MenuT *ctxMenu = wmCreateMenu(); wmAddMenuItem(ctxMenu, "Cu&t", CMD_CUT); wmAddMenuItem(ctxMenu, "&Copy", CMD_COPY); wmAddMenuItem(ctxMenu, "&Paste", CMD_PASTE); -wmAddMenuSeparator(ctxMenu); -wmAddMenuItem(ctxMenu, "&Properties...", CMD_PROPS); win->contextMenu = ctxMenu; // Widget-level context menu WidgetT *lb = wgtListBox(root); -MenuT *lbCtx = wmCreateMenu(); -wmAddMenuItem(lbCtx, "&Delete", CMD_DELETE); -wmAddMenuItem(lbCtx, "Select &All", CMD_SELALL); -lb->contextMenu = lbCtx; +lb->contextMenu = wmCreateMenu(); +wmAddMenuItem(lb->contextMenu, "&Delete", CMD_DELETE); ``` Context menus route through the window's `onMenu` callback. The caller -owns the `MenuT` (not freed by the widget system). +owns the MenuT allocation (free with `wmFreeMenu()`). ---- +### Accelerator Tables -## Accelerator tables - -Global hotkeys routed through the window's `onMenu` callback: +Keyboard shortcuts routed through `onMenu`: ```c AccelTableT *accel = dvxCreateAccelTable(); -dvxAddAccel(accel, 'N', ACCEL_CTRL, CMD_FILE_NEW); -dvxAddAccel(accel, 'O', ACCEL_CTRL, CMD_FILE_OPEN); -dvxAddAccel(accel, 'S', ACCEL_CTRL, CMD_FILE_SAVE); -dvxAddAccel(accel, 'Q', ACCEL_CTRL, CMD_FILE_EXIT); -dvxAddAccel(accel, KEY_F1, 0, CMD_HELP_ABOUT); +dvxAddAccel(accel, 'N', ACCEL_CTRL, CMD_NEW); +dvxAddAccel(accel, 'O', ACCEL_CTRL, CMD_OPEN); +dvxAddAccel(accel, 'S', ACCEL_CTRL, CMD_SAVE); +dvxAddAccel(accel, KEY_F1, 0, CMD_HELP); win->accelTable = accel; ``` -Modifier constants: `ACCEL_SHIFT` (0x03), `ACCEL_CTRL` (0x04), `ACCEL_ALT` (0x08). +Modifier constants: `ACCEL_SHIFT` (0x03), `ACCEL_CTRL` (0x04), +`ACCEL_ALT` (0x08). -Key constants for extended keys: `KEY_F1`--`KEY_F12`, `KEY_INSERT`, `KEY_DELETE`, -`KEY_HOME`, `KEY_END`, `KEY_PGUP`, `KEY_PGDN`. +Key constants for extended keys: `KEY_F1`--`KEY_F12`, `KEY_INSERT`, +`KEY_DELETE`, `KEY_HOME`, `KEY_END`, `KEY_PGUP`, `KEY_PGDN`. + +Up to 32 entries per table. Free with `dvxFreeAccelTable()`. -Up to 32 entries per table. The table is freed with `dvxFreeAccelTable()`. --- + ## Scrollbars ```c -wmAddVScrollbar(win, 0, 100, 25); // vertical, range 0-100, page size 25 -wmAddHScrollbar(win, 0, 100, 25); // horizontal -wmUpdateContentRect(win); -wmReallocContentBuf(win, &ctx.display); +ScrollbarT *wmAddVScrollbar(WindowT *win, int32_t min, int32_t max, int32_t pageSize); +ScrollbarT *wmAddHScrollbar(WindowT *win, int32_t min, int32_t max, int32_t pageSize); ``` -The `onScroll` callback fires when the user drags the thumb or clicks the -arrow buttons / trough. +After adding scrollbars, call `wmUpdateContentRect(win)` and +`wmReallocContentBuf(win, &ctx.display)`. The `onScroll` callback fires +when the user drags the thumb or clicks arrow buttons / trough. + +Widget-managed windows handle their own scrollbars automatically -- do +not add them manually to widget windows. -Widget windows manage their own scrollbars automatically -- do not add them -manually. --- -## Dialogs (`dvxDialog.h`) -### Message box +## Dialogs (dvxDialog.h) + +### Message Box ```c int32_t result = dvxMessageBox(&ctx, "Confirm", - "Are you sure you want to exit?", - MB_YESNO | MB_ICONQUESTION); + "Are you sure?", MB_YESNO | MB_ICONQUESTION); if (result == ID_YES) { dvxQuit(&ctx); } ``` -Button flags: `MB_OK`, `MB_OKCANCEL`, `MB_YESNO`, `MB_YESNOCANCEL`, `MB_RETRYCANCEL`. +Button flags: `MB_OK`, `MB_OKCANCEL`, `MB_YESNO`, `MB_YESNOCANCEL`, +`MB_RETRYCANCEL`. -Icon flags: `MB_ICONINFO`, `MB_ICONWARNING`, `MB_ICONERROR`, `MB_ICONQUESTION`. +Icon flags: `MB_ICONINFO`, `MB_ICONWARNING`, `MB_ICONERROR`, +`MB_ICONQUESTION`. Return values: `ID_OK`, `ID_CANCEL`, `ID_YES`, `ID_NO`, `ID_RETRY`. -### File dialog +### File Dialog ```c static const FileFilterT filters[] = { @@ -492,1046 +654,187 @@ static const FileFilterT filters[] = { }; char path[260]; - -if (dvxFileDialog(&ctx, "Open File", FD_OPEN, NULL, filters, 3, path, sizeof(path))) { +if (dvxFileDialog(&ctx, "Open File", FD_OPEN, NULL, + filters, 3, path, sizeof(path))) { // path contains the selected filename } - -if (dvxFileDialog(&ctx, "Save As", FD_SAVE, NULL, filters, 3, path, sizeof(path))) { - // path contains the chosen filename -} ``` -Flags: `FD_OPEN`, `FD_SAVE`. Pass `NULL` for `initialDir` to use the -current directory. Filename validation is platform-specific (DOS 8.3 on -the DOS platform). +Flags: `FD_OPEN`, `FD_SAVE`. Pass NULL for `initialDir` to use the +current directory. Filename validation is platform-specific (8.3 on DOS). + --- -## Video and drawing (`dvxVideo.h`, `dvxDraw.h`) -These are lower-level APIs. Application code typically only needs `packColor`. +## Video and Drawing (dvxVideo.h, dvxDraw.h) + +Lower-level APIs. Application code typically only needs `packColor`. + +### Color Packing ```c uint32_t packColor(const DisplayT *d, uint8_t r, uint8_t g, uint8_t b); ``` -Pack an RGB triple into the display's pixel format. -```c -void rectFill(DisplayT *d, const BlitOpsT *ops, - int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color); -``` -Fill a rectangle with a solid color (clipped to the display clip rect). +Pack RGB into the display's native pixel format. For 8-bit mode, +returns the nearest palette index. -```c -void drawBevel(DisplayT *d, const BlitOpsT *ops, - int32_t x, int32_t y, int32_t w, int32_t h, - const BevelStyleT *style); -``` -Draw a beveled frame. `BevelStyleT` specifies highlight, shadow, face colors -and border width. - -```c -void drawText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, - int32_t x, int32_t y, const char *text, - uint32_t fg, uint32_t bg, bool opaque); -int32_t drawChar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, - int32_t x, int32_t y, char ch, - uint32_t fg, uint32_t bg, bool opaque); -int32_t textWidth(const BitmapFontT *font, const char *text); -``` -Draw text using the built-in 8-pixel-wide bitmap font. `opaque` controls -whether background pixels are drawn. `drawChar` returns the advance width. - -```c -void drawTextN(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, - int32_t x, int32_t y, const char *text, int32_t count, - uint32_t fg, uint32_t bg, bool opaque); -``` -Bulk-render exactly `count` characters. Much faster than calling `drawChar` -per character because clip bounds are computed once and background is filled -in bulk. - -```c -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); -``` -Bulk-render a row of terminal character cells. `lineData` points to -`(char, attr)` byte pairs (2 bytes per cell). `palette` is a 16-entry -packed-color table. `blinkVisible` controls blink attribute visibility. -`cursorCol` is the column to draw inverted (-1 for no cursor). This is -much faster than calling `drawChar` per cell because clip bounds are -computed once for the whole row and there is no per-character function -call overhead. - -```c -char accelParse(const char *text); -``` -Parse an accelerator key from text with `&` markers (e.g. `"E&xit"` returns -`'x'`). Returns the lowercase accelerator character, or 0 if none found. - -```c -void drawTextAccel(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, - int32_t x, int32_t y, const char *text, - uint32_t fg, uint32_t bg, bool opaque); -int32_t textWidthAccel(const BitmapFontT *font, const char *text); -``` -Draw/measure text with `&` accelerator processing. The character after `&` -is underlined. `&&` produces a literal `&`. `textWidthAccel` returns the -width in pixels, excluding `&` markers. - -```c -void drawFocusRect(DisplayT *d, const BlitOpsT *ops, - int32_t x, int32_t y, int32_t w, int32_t h, - uint32_t color); -``` -Draw a dotted rectangle outline (every other pixel). Used to indicate -keyboard focus on widgets. - -```c -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 1-bit bitmap with AND/XOR masks (used for mouse cursors). Each row -is a `uint16_t`. Pixels where the AND mask is 0 are drawn using the XOR -data to select `fgColor` or `bgColor`. - -```c -void drawHLine(DisplayT *d, const BlitOpsT *ops, - int32_t x, int32_t y, int32_t w, uint32_t color); -void drawVLine(DisplayT *d, const BlitOpsT *ops, - int32_t x, int32_t y, int32_t h, uint32_t color); -``` - -```c -void rectCopy(DisplayT *d, const BlitOpsT *ops, - int32_t dstX, int32_t dstY, - const uint8_t *srcBuf, int32_t srcPitch, - int32_t srcX, int32_t srcY, int32_t w, int32_t h); -``` -Blit from a source buffer to the backbuffer with clipping. +### Clip Rectangle ```c void setClipRect(DisplayT *d, int32_t x, int32_t y, int32_t w, int32_t h); void resetClipRect(DisplayT *d); ``` ---- - -## Widget system (`dvxWidget.h`) - -An optional declarative layout system inspired by Amiga MUI and PC GEOS. -Widgets are arranged in a tree of VBox/HBox containers. A two-pass layout -engine (measure minimum sizes, then distribute space) handles geometry -automatically. Scrollbars appear when the window is too small for the -content. - -### Initialization - -```c -WidgetT *wgtInitWindow(AppContextT *ctx, WindowT *win); -``` -Attach the widget system to a window. Returns the root container (a VBox -filling the content area). Installs `onPaint`, `onMouse`, `onKey`, and -`onResize` handlers on the window. Build the widget tree by passing the -returned root (or its children) as `parent` to the widget creation -functions below. - -Call `wgtInvalidate(root)` after the tree is fully built to trigger the -initial layout and paint. - -### Containers - -```c -WidgetT *wgtVBox(WidgetT *parent); // vertical stack -WidgetT *wgtHBox(WidgetT *parent); // horizontal stack -WidgetT *wgtFrame(WidgetT *parent, const char *title); // titled border -``` -Containers hold child widgets and control layout direction. `wgtFrame` -draws a styled border with a title label and lays out children vertically. - -Example: - -```c -WidgetT *root = wgtInitWindow(&ctx, win); - -WidgetT *frame = wgtFrame(root, "User Input"); -WidgetT *row1 = wgtHBox(frame); -wgtLabel(row1, "Name:"); -wgtTextInput(row1, 64); - -WidgetT *row2 = wgtHBox(frame); -wgtLabel(row2, "Email:"); -wgtTextInput(row2, 128); - -WidgetT *btnRow = wgtHBox(root); -btnRow->align = AlignEndE; -wgtButton(btnRow, "OK"); -wgtButton(btnRow, "Cancel"); - -wgtInvalidate(root); -``` - -Frame styles (set `w->as.frame.style` after creation): - -| Style | Description | -|-------|-------------| -| `FrameInE` | Beveled inward / sunken (default) | -| `FrameOutE` | Beveled outward / raised | -| `FrameFlatE` | Solid color line; set `w->as.frame.color` (0 = windowShadow) | - -Container properties (set directly on the returned `WidgetT *`): - -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `align` | `WidgetAlignE` | `AlignStartE` | Main-axis alignment when no children have weight | -| `spacing` | `int32_t` | 0 (4px) | Tagged size: gap between children | -| `padding` | `int32_t` | 0 (4px) | Tagged size: internal padding | - -Alignment values: - -| Value | HBox meaning | VBox meaning | -|-------|-------------|-------------| -| `AlignStartE` | left | top | -| `AlignCenterE` | center | center | -| `AlignEndE` | right | bottom | - -### Label - -```c -WidgetT *wgtLabel(WidgetT *parent, const char *text); -``` -Static text label. Sized to fit its text. Supports `&` accelerator markers. - -```c -WidgetT *lbl = wgtLabel(root, "&Status:"); -wgtSetText(lbl, "Connected"); // change text later -``` - -### Button - -```c -WidgetT *wgtButton(WidgetT *parent, const char *text); -``` -Beveled push button. Visually depresses on mouse-down, tracks the cursor -while held, fires `onClick` on release if still over the button. - -```c -static void onOkClick(WidgetT *w) { - // handle button press -} - -WidgetT *btn = wgtButton(root, "&OK"); -btn->onClick = onOkClick; -``` - -### Checkbox - -```c -WidgetT *wgtCheckbox(WidgetT *parent, const char *text); -``` -Toggle checkbox. Read/write `w->as.checkbox.checked`. - -```c -WidgetT *chk = wgtCheckbox(root, "Enable feature &A"); -chk->as.checkbox.checked = true; // pre-check -chk->onChange = onCheckChanged; // notified on toggle -``` - -### Radio buttons - -```c -WidgetT *wgtRadioGroup(WidgetT *parent); -WidgetT *wgtRadio(WidgetT *parent, const char *text); -``` -Radio buttons with diamond-shaped indicators. Create a group, then add -options as children. The group tracks the selected index. - -```c -WidgetT *rg = wgtRadioGroup(root); -wgtRadio(rg, "Option &1"); -wgtRadio(rg, "Option &2"); -wgtRadio(rg, "Option &3"); -rg->onChange = onRadioChanged; - -// Read selection: -int32_t idx = rg->as.radioGroup.selectedIdx; -``` - -### TextInput - -```c -WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen); -``` -Single-line text input field. `maxLen` is the buffer capacity. Default -weight is 100 (stretches to fill). - -```c -WidgetT *row = wgtHBox(root); -wgtLabel(row, "&Name:"); -WidgetT *input = wgtTextInput(row, 64); -wgtSetText(input, "Default text"); -input->onChange = onTextChanged; - -// Read text: -const char *text = wgtGetText(input); -``` - -Editing features: cursor movement (arrows, Home, End), insert/overwrite, -selection (Shift+arrows, Shift+Home/End, Ctrl+A, double-click word, -triple-click all), clipboard (Ctrl+C copy, Ctrl+V paste, Ctrl+X cut), -single-level undo (Ctrl+Z), and horizontal scrolling when text exceeds -the visible width. - -### PasswordInput - -```c -WidgetT *wgtPasswordInput(WidgetT *parent, int32_t maxLen); -``` -Identical to `wgtTextInput` except characters are displayed as bullets. -Copy and cut to clipboard are disabled; paste is allowed. - -```c -WidgetT *pw = wgtPasswordInput(root, 32); -``` - -### MaskedInput - -```c -WidgetT *wgtMaskedInput(WidgetT *parent, const char *mask); -``` -Formatted input field. Mask characters: `#` = digit, `A` = letter, -`*` = any printable. All other characters are fixed literals. - -```c -wgtMaskedInput(root, "(###) ###-####"); // US phone -wgtMaskedInput(root, "##/##/####"); // date -wgtMaskedInput(root, "###-##-####"); // SSN -``` - -The mask string must remain valid for the widget's lifetime. - -### TextArea - -```c -WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen); -``` -Multi-line text editor with vertical and horizontal scrollbars. +### Rectangle Operations ```c -WidgetT *ta = wgtTextArea(root, 4096); -ta->weight = 100; -wgtSetText(ta, "Line 1\nLine 2\nLine 3"); +void rectFill(DisplayT *d, const BlitOpsT *ops, + int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color); +void rectCopy(DisplayT *d, const BlitOpsT *ops, + int32_t dstX, int32_t dstY, const uint8_t *srcBuf, + int32_t srcPitch, int32_t srcX, int32_t srcY, + int32_t w, int32_t h); ``` -Supports all TextInput editing features plus multi-line selection, -Enter for newlines, and vertical/horizontal auto-scroll. - -### ListBox - -```c -WidgetT *wgtListBox(WidgetT *parent); -void wgtListBoxSetItems(WidgetT *w, const char **items, int32_t count); -int32_t wgtListBoxGetSelected(const WidgetT *w); -void wgtListBoxSetSelected(WidgetT *w, int32_t idx); -``` +### Bevel Drawing ```c -static const char *items[] = {"Alpha", "Beta", "Gamma", "Delta"}; - -WidgetT *lb = wgtListBox(root); -wgtListBoxSetItems(lb, items, 4); -wgtListBoxSetSelected(lb, 0); -lb->weight = 100; -lb->onClick = onItemSelected; +void drawBevel(DisplayT *d, const BlitOpsT *ops, + int32_t x, int32_t y, int32_t w, int32_t h, + const BevelStyleT *style); ``` -Multi-select mode: +Convenience macros for common bevel styles: +- `BEVEL_RAISED(cs, bw)` -- standard raised (buttons, window frames) +- `BEVEL_SUNKEN(cs, face, bw)` -- sunken (text fields, list boxes) +- `BEVEL_TROUGH(cs)` -- scrollbar trough (1px sunken) +- `BEVEL_SB_BUTTON(cs)` -- scrollbar arrow button (1px raised) -```c -wgtListBoxSetMultiSelect(lb, true); -wgtListBoxSetItemSelected(lb, 0, true); -wgtListBoxSetItemSelected(lb, 2, true); -bool sel = wgtListBoxIsItemSelected(lb, 1); -wgtListBoxSelectAll(lb); -wgtListBoxClearSelection(lb); -``` - -Reorderable (drag-and-drop items): +### Text Rendering ```c -wgtListBoxSetReorderable(lb, true); +void drawText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, + int32_t x, int32_t y, const char *text, + uint32_t fg, uint32_t bg, bool opaque); +void drawTextN(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, + int32_t x, int32_t y, const char *text, int32_t count, + uint32_t fg, uint32_t bg, bool opaque); +int32_t textWidth(const BitmapFontT *font, const char *text); ``` -### Dropdown - -```c -WidgetT *wgtDropdown(WidgetT *parent); -void wgtDropdownSetItems(WidgetT *w, const char **items, int32_t count); -int32_t wgtDropdownGetSelected(const WidgetT *w); -void wgtDropdownSetSelected(WidgetT *w, int32_t idx); -``` -Non-editable selection widget with a popup list. +`drawTextN` is the fast path for bulk rendering -- computes clip bounds +once and fills the background in a single pass. ```c -static const char *colors[] = {"Red", "Green", "Blue", "Yellow"}; - -WidgetT *dd = wgtDropdown(root); -wgtDropdownSetItems(dd, colors, 4); -wgtDropdownSetSelected(dd, 0); -dd->onChange = onColorChanged; +void drawTextAccel(DisplayT *d, const BlitOpsT *ops, + const BitmapFontT *font, int32_t x, int32_t y, + const char *text, uint32_t fg, uint32_t bg, bool opaque); +int32_t textWidthAccel(const BitmapFontT *font, const char *text); +char accelParse(const char *text); ``` -The items array must remain valid for the widget's lifetime. +Text with `&` accelerator markers. The character after `&` is +underlined. `&&` produces a literal `&`. -### ComboBox +### Terminal Row Rendering ```c -WidgetT *wgtComboBox(WidgetT *parent, int32_t maxLen); -void wgtComboBoxSetItems(WidgetT *w, const char **items, int32_t count); -int32_t wgtComboBoxGetSelected(const WidgetT *w); -void wgtComboBoxSetSelected(WidgetT *w, int32_t idx); +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); ``` -Editable text field with a dropdown list. - -```c -static const char *sizes[] = {"Small", "Medium", "Large", "Extra Large"}; -WidgetT *cb = wgtComboBox(root, 32); -wgtComboBoxSetItems(cb, sizes, 4); -wgtComboBoxSetSelected(cb, 1); -cb->onChange = onSizeChanged; - -// Read typed/selected text: -const char *text = wgtGetText(cb); -``` - -### ProgressBar - -```c -WidgetT *wgtProgressBar(WidgetT *parent); // horizontal -WidgetT *wgtProgressBarV(WidgetT *parent); // vertical -void wgtProgressBarSetValue(WidgetT *w, int32_t value); -int32_t wgtProgressBarGetValue(const WidgetT *w); -``` - -```c -WidgetT *pb = wgtProgressBar(root); -pb->weight = 100; -wgtProgressBarSetValue(pb, 65); - -WidgetT *pbV = wgtProgressBarV(root); -wgtProgressBarSetValue(pbV, 75); -``` - -Value is 0--100 (percentage). - -### Slider - -```c -WidgetT *wgtSlider(WidgetT *parent, int32_t minVal, int32_t maxVal); -void wgtSliderSetValue(WidgetT *w, int32_t value); -int32_t wgtSliderGetValue(const WidgetT *w); -``` - -```c -WidgetT *slider = wgtSlider(root, 0, 100); -wgtSliderSetValue(slider, 50); -slider->onChange = onVolumeChanged; - -// Vertical slider: -WidgetT *vSlider = wgtSlider(root, 0, 255); -vSlider->as.slider.vertical = true; -``` +Renders an entire 80-column terminal row in one call. `lineData` points +to `(char, attr)` byte pairs. Much faster than per-character rendering. -### Spinner +### Lines and Focus ```c -WidgetT *wgtSpinner(WidgetT *parent, int32_t minVal, int32_t maxVal, int32_t step); -void wgtSpinnerSetValue(WidgetT *w, int32_t value); -int32_t wgtSpinnerGetValue(const WidgetT *w); -void wgtSpinnerSetRange(WidgetT *w, int32_t minVal, int32_t maxVal); -void wgtSpinnerSetStep(WidgetT *w, int32_t step); +void drawHLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, uint32_t color); +void drawVLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t h, uint32_t color); +void drawFocusRect(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color); ``` -Numeric up/down input with increment/decrement buttons. -```c -WidgetT *spin = wgtSpinner(root, 0, 999, 1); -wgtSpinnerSetValue(spin, 42); -spin->weight = 50; -spin->onChange = onQuantityChanged; -``` - -Up/Down arrows increment/decrement by `step`; Page Up/Down by `step * 10`. -Enter commits the typed value; Escape reverts. - -### TabControl - -```c -WidgetT *wgtTabControl(WidgetT *parent); -WidgetT *wgtTabPage(WidgetT *parent, const char *title); -void wgtTabControlSetActive(WidgetT *w, int32_t idx); -int32_t wgtTabControlGetActive(const WidgetT *w); -``` - -```c -WidgetT *tabs = wgtTabControl(root); - -WidgetT *page1 = wgtTabPage(tabs, "&General"); -wgtLabel(page1, "General settings"); -wgtCheckbox(page1, "&Enable logging"); - -WidgetT *page2 = wgtTabPage(tabs, "&Advanced"); -wgtLabel(page2, "Advanced settings"); -WidgetT *slider = wgtSlider(page2, 0, 100); -wgtSliderSetValue(slider, 75); - -WidgetT *page3 = wgtTabPage(tabs, "A&bout"); -wgtLabel(page3, "Version 1.0"); -``` - -Each page is a VBox. Only the active page is visible and receives layout. - -### StatusBar - -```c -WidgetT *wgtStatusBar(WidgetT *parent); -``` -Horizontal container with sunken panel background. Place at the bottom of -the root VBox. - -```c -WidgetT *sb = wgtStatusBar(root); -WidgetT *msg = wgtLabel(sb, "Ready"); -msg->weight = 100; // stretch to fill -wgtLabel(sb, "Line 1, Col 1"); -``` - -### Toolbar - -```c -WidgetT *wgtToolbar(WidgetT *parent); -``` -Horizontal container with raised background. Place at the top of the root -VBox. - -```c -WidgetT *tb = wgtToolbar(root); -WidgetT *btnNew = wgtImageButton(tb, newPixels, 16, 16, 16 * bpp); -WidgetT *btnOpen = wgtImageButton(tb, openPixels, 16, 16, 16 * bpp); -WidgetT *btnSave = wgtImageButton(tb, savePixels, 16, 16, 16 * bpp); -wgtVSeparator(tb); -wgtButton(tb, "&Help"); -``` - -### TreeView - -```c -WidgetT *wgtTreeView(WidgetT *parent); -WidgetT *wgtTreeItem(WidgetT *parent, const char *text); -void wgtTreeItemSetExpanded(WidgetT *w, bool expanded); -bool wgtTreeItemIsExpanded(const WidgetT *w); -WidgetT *wgtTreeViewGetSelected(const WidgetT *w); -void wgtTreeViewSetSelected(WidgetT *w, WidgetT *item); -``` - -```c -WidgetT *tree = wgtTreeView(root); - -WidgetT *docs = wgtTreeItem(tree, "Documents"); -wgtTreeItemSetExpanded(docs, true); -wgtTreeItem(docs, "README.md"); -wgtTreeItem(docs, "DESIGN.md"); - -WidgetT *src = wgtTreeItem(docs, "src"); -wgtTreeItemSetExpanded(src, true); -wgtTreeItem(src, "main.c"); -wgtTreeItem(src, "utils.c"); - -WidgetT *images = wgtTreeItem(tree, "Images"); -wgtTreeItem(images, "logo.png"); -wgtTreeItem(images, "icon.bmp"); -``` +`drawFocusRect` draws a dotted rectangle (alternating pixels) for +keyboard focus indicators. -Multi-select and reorderable: - -```c -wgtTreeViewSetMultiSelect(tree, true); -wgtTreeViewSetReorderable(tree, true); -``` - -The tree view manages its own scrollbars automatically. Default weight is 100. - -### ListView - -```c -WidgetT *wgtListView(WidgetT *parent); -void wgtListViewSetColumns(WidgetT *w, const ListViewColT *cols, int32_t count); -void wgtListViewSetData(WidgetT *w, const char **cellData, int32_t rowCount); -int32_t wgtListViewGetSelected(const WidgetT *w); -void wgtListViewSetSelected(WidgetT *w, int32_t idx); -``` -Multi-column list with sortable headers and resizable columns. - -```c -static const ListViewColT cols[] = { - {"Name", wgtChars(16), ListViewAlignLeftE}, - {"Size", wgtChars(8), ListViewAlignRightE}, - {"Type", wgtChars(12), ListViewAlignLeftE}, - {"Modified", wgtChars(12), ListViewAlignLeftE} -}; - -static const char *data[] = { - "AUTOEXEC.BAT", "412", "Batch File", "03/15/1994", - "CONFIG.SYS", "256", "System File", "03/15/1994", - "COMMAND.COM", "54,645", "Application", "09/30/1993", -}; - -WidgetT *lv = wgtListView(root); -wgtListViewSetColumns(lv, cols, 4); -wgtListViewSetData(lv, data, 3); -wgtListViewSetSelected(lv, 0); -lv->weight = 100; -``` - -Multi-select and sorting: - -```c -wgtListViewSetMultiSelect(lv, true); -wgtListViewSetSort(lv, 0, ListViewSortAscE); -wgtListViewSetReorderable(lv, true); - -// Header click callback for custom sort logic: -wgtListViewSetHeaderClickCallback(lv, onHeaderClick); -``` - -Column widths support tagged sizes (`wgtPixels`, `wgtChars`, `wgtPercent`). -Cell data is a flat array of strings in row-major order. Both the column -and data arrays must remain valid for the widget's lifetime. - -### ScrollPane - -```c -WidgetT *wgtScrollPane(WidgetT *parent); -``` -Scrollable container. Children are laid out vertically at their natural -size. Scrollbars appear automatically when content exceeds the visible area. - -```c -WidgetT *sp = wgtScrollPane(root); -sp->weight = 100; -sp->padding = wgtPixels(4); -sp->spacing = wgtPixels(4); - -wgtLabel(sp, "Scrollable content:"); -wgtHSeparator(sp); -for (int32_t i = 0; i < 8; i++) { - wgtCheckbox(sp, items[i]); -} -wgtHSeparator(sp); -wgtButton(sp, "Scrolled Button"); -``` - -### Splitter - -```c -WidgetT *wgtSplitter(WidgetT *parent, bool vertical); -void wgtSplitterSetPos(WidgetT *w, int32_t pos); -int32_t wgtSplitterGetPos(const WidgetT *w); -``` -Resizable divider between exactly two child widgets. `vertical = true` -creates a left|right split; `false` creates a top/bottom split. - -```c -// Horizontal split: tree on left, list on right -WidgetT *vSplit = wgtSplitter(root, true); -vSplit->weight = 100; -wgtSplitterSetPos(vSplit, 150); - -WidgetT *leftTree = wgtTreeView(vSplit); -// ... populate tree ... - -WidgetT *rightList = wgtListBox(vSplit); -// ... populate list ... -``` - -Nested splitters for complex layouts: - -```c -// Outer: top/bottom split -WidgetT *hSplit = wgtSplitter(root, false); -hSplit->weight = 100; -wgtSplitterSetPos(hSplit, 120); - -// Top pane: left/right split -WidgetT *vSplit = wgtSplitter(hSplit, true); -wgtSplitterSetPos(vSplit, 120); -wgtTreeView(vSplit); // left -wgtListBox(vSplit); // right - -// Bottom pane: detail view -WidgetT *detail = wgtFrame(hSplit, "Preview"); -wgtLabel(detail, "Select a file above."); -``` - -### Image - -```c -WidgetT *wgtImage(WidgetT *parent, uint8_t *data, - int32_t w, int32_t h, int32_t pitch); -WidgetT *wgtImageFromFile(WidgetT *parent, const char *path); -void wgtImageSetData(WidgetT *w, uint8_t *data, - int32_t imgW, int32_t imgH, int32_t pitch); -``` - -```c -// From file (BMP, PNG, JPEG, GIF): -wgtImageFromFile(root, "sample.bmp"); - -// From pixel buffer (display format, takes ownership): -uint8_t *pixels = loadMyImage(&w, &h, &pitch); -wgtImage(root, pixels, w, h, pitch); -``` - -### ImageButton - -```c -WidgetT *wgtImageButton(WidgetT *parent, uint8_t *data, - int32_t w, int32_t h, int32_t pitch); -void wgtImageButtonSetData(WidgetT *w, uint8_t *data, - int32_t imgW, int32_t imgH, int32_t pitch); -``` -Push button that displays an image instead of text. Behaves identically -to `wgtButton`. - -```c -uint8_t *iconData = loadBmpPixels(&ctx, "new.bmp", &imgW, &imgH, &imgPitch); -WidgetT *btn = wgtImageButton(tb, iconData, imgW, imgH, imgPitch); -btn->onClick = onNewClick; -``` - -Takes ownership of the pixel buffer (freed on destroy). - -### Canvas - -```c -WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h); -``` -Drawable bitmap canvas with a sunken bevel border. Supports interactive -freehand drawing (mouse strokes) and programmatic drawing. - -```c -const DisplayT *d = dvxGetDisplay(&ctx); -WidgetT *cv = wgtCanvas(root, 280, 100); - -wgtCanvasClear(cv, packColor(d, 255, 255, 255)); - -wgtCanvasSetPenColor(cv, packColor(d, 200, 0, 0)); -wgtCanvasDrawRect(cv, 5, 5, 50, 35); - -wgtCanvasSetPenColor(cv, packColor(d, 0, 0, 200)); -wgtCanvasFillCircle(cv, 150, 50, 25); - -wgtCanvasSetPenColor(cv, packColor(d, 0, 150, 0)); -wgtCanvasSetPenSize(cv, 3); -wgtCanvasDrawLine(cv, 70, 5, 130, 90); - -// Individual pixels: -wgtCanvasSetPixel(cv, 10, 10, packColor(d, 255, 0, 0)); -uint32_t px = wgtCanvasGetPixel(cv, 10, 10); - -// Save/load: -wgtCanvasSave(cv, "drawing.png"); -wgtCanvasLoad(cv, "image.png"); -``` - -Drawing primitives: `wgtCanvasDrawLine`, `wgtCanvasDrawRect` (outline), -`wgtCanvasFillRect` (solid), `wgtCanvasFillCircle`. All use the current -pen color and clip to the canvas bounds. - -### ANSI Terminal - -```c -WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows); -``` -ANSI BBS terminal emulator. Displays a character grid (default 80x25 if -cols/rows are 0) with full ANSI escape sequence support and a 16-color -CGA palette. - -```c -WidgetT *term = wgtAnsiTerm(root, 80, 25); -term->weight = 100; -wgtAnsiTermSetScrollback(term, 500); - -// Feed ANSI content directly: -static const uint8_t ansiData[] = - "\x1B[2J" // clear screen - "\x1B[1;34m=== Welcome ===\x1B[0m\r\n" - "\x1B[1mBold\x1B[0m, " - "\x1B[7mReverse\x1B[0m, " - "\x1B[5mBlinking\x1B[0m\r\n" - "\x1B[31m Red \x1B[32m Green \x1B[34m Blue \x1B[0m\r\n"; - -wgtAnsiTermWrite(term, ansiData, sizeof(ansiData) - 1); -``` - -Connect to a communications interface: - -```c -static int32_t myRead(void *ctx, uint8_t *buf, int32_t maxLen) { - return serialRead((SerialT *)ctx, buf, maxLen); -} - -static int32_t myWrite(void *ctx, const uint8_t *buf, int32_t len) { - return serialWrite((SerialT *)ctx, buf, len); -} - -wgtAnsiTermSetComm(term, &serial, myRead, myWrite); -``` - -ANSI escape sequences supported: - -| Sequence | Description | -|----------|-------------| -| `ESC[H` / `ESC[f` | Cursor position (CUP/HVP) | -| `ESC[A/B/C/D` | Cursor up/down/forward/back | -| `ESC[J` | Erase display (0=to end, 1=to start, 2=all) | -| `ESC[K` | Erase line (0=to end, 1=to start, 2=all) | -| `ESC[m` | SGR: colors 30-37/40-47, bright 90-97/100-107, bold(1), blink(5), reverse(7), reset(0) | -| `ESC[s` / `ESC[u` | Save / restore cursor position | -| `ESC[S` / `ESC[T` | Scroll up / down | -| `ESC[L` / `ESC[M` | Insert / delete lines | -| `ESC[?7h/l` | Enable / disable auto-wrap | -| `ESC[?25h/l` | Show / hide cursor | - -Additional functions: - -```c -void wgtAnsiTermClear(WidgetT *w); // clear + reset cursor -int32_t wgtAnsiTermPoll(WidgetT *w); // poll comm for data -int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *y, int32_t *h); // fast dirty-row repaint -void wgtAnsiTermSetScrollback(WidgetT *w, int32_t maxLines); -``` - -### Spacing and dividers - -```c -WidgetT *wgtSpacer(WidgetT *parent); // flexible space (weight=100) -WidgetT *wgtHSeparator(WidgetT *parent); // horizontal line -WidgetT *wgtVSeparator(WidgetT *parent); // vertical line -``` - -```c -WidgetT *row = wgtHBox(root); -wgtButton(row, "Left"); -wgtSpacer(row); // pushes next button to the right -wgtButton(row, "Right"); -``` - -### Tooltips - -```c -wgtSetTooltip(widget, "Tooltip text appears on hover"); -``` - -### Size specifications - -Size fields (`minW`, `minH`, `maxW`, `maxH`, `prefW`, `prefH`, `spacing`, -`padding`) accept tagged values created with: - -```c -wgtPixels(120) // 120 pixels -wgtChars(15) // 15 character widths -wgtPercent(50) // 50% of parent -``` - -A raw `0` means auto (use the widget's natural/computed size). - -### Weight - -The `weight` field controls how extra space is distributed among siblings. -When a container is larger than its children's combined minimum size, the -surplus is divided proportionally by weight. - -- `weight = 0` -- fixed size, does not stretch (default for most widgets) -- `weight = 100` -- normal stretch (default for spacers and text inputs) -- Relative values work: weights of 100, 200, 100 give a 1:2:1 split - -When all children have weight 0, the container's `align` property -determines where children are placed within the extra space. - -### Operations - -```c -void wgtSetText(WidgetT *w, const char *text); -const char *wgtGetText(const WidgetT *w); -``` -Get/set text for labels, buttons, checkboxes, radios, text inputs, -password inputs, masked inputs, combo boxes, and dropdowns. - -```c -void wgtSetEnabled(WidgetT *w, bool enabled); -void wgtSetVisible(WidgetT *w, bool visible); -``` -Disabled widgets are drawn grayed out and ignore input. Hidden widgets -are excluded from layout. - -```c -// Disable an entire group of widgets: -wgtSetEnabled(btn, false); -wgtSetEnabled(slider, false); -wgtSetEnabled(textInput, false); -``` - -```c -void wgtInvalidate(WidgetT *w); -``` -Trigger a full relayout and repaint of the widget tree. Call after -modifying widget properties, adding/removing widgets, or changing text. - -```c -void wgtInvalidatePaint(WidgetT *w); -``` -Lightweight repaint without relayout. Use when only visual state changed -(slider value, cursor blink, checkbox toggle) but widget sizes are stable. - -```c -WidgetT *wgtFind(WidgetT *root, const char *name); -void wgtSetName(WidgetT *w, const char *name); -``` -Search the subtree for a widget by name (up to 31 chars). Uses djb2 hash -for fast rejection. - -```c -wgtSetName(statusLabel, "status"); -// ... later, from any callback: -WidgetT *lbl = wgtFind(root, "status"); -wgtSetText(lbl, "Updated!"); -wgtInvalidate(lbl); -``` - -```c -void wgtDestroy(WidgetT *w); -``` -Remove a widget and all its children from the tree and free memory. - -```c -void wgtSetDebugLayout(AppContextT *ctx, bool enabled); -``` -When enabled, draws 1px neon-colored borders around all layout containers -so their bounds are visible. Each container gets a distinct color derived -from its pointer. - -### Layout and paint internals - -These are called automatically by `wgtInvalidate()`, but are available -for manual use when embedding the widget system in a custom event loop. - -```c -int32_t wgtResolveSize(int32_t taggedSize, int32_t parentSize, int32_t charWidth); -``` -Convert a tagged size (from `wgtPixels`, `wgtChars`, or `wgtPercent`) to -a pixel value given the parent's size and the font's character width. - -```c -void wgtLayout(WidgetT *root, int32_t availW, int32_t availH, - const BitmapFontT *font); -``` -Run the two-pass layout engine on the widget tree: measure minimum sizes -bottom-up, then distribute available space top-down. Sets `x`, `y`, `w`, -`h` on every widget. - -```c -void wgtPaint(WidgetT *root, DisplayT *d, const BlitOpsT *ops, - const BitmapFontT *font, const ColorSchemeT *colors); -``` -Paint the entire widget tree into the window's content buffer. --- -## Types reference (`dvxTypes.h`) -### WindowT - -Core window structure. Key fields: - -| Field | Type | Description | -|-------|------|-------------| -| `id` | `int32_t` | Unique window identifier | -| `x`, `y`, `w`, `h` | `int32_t` | Outer frame position and size | -| `contentX`, `contentY` | `int32_t` | Content area offset within frame | -| `contentW`, `contentH` | `int32_t` | Content area dimensions | -| `contentBuf` | `uint8_t *` | Pixel buffer (display format) | -| `contentPitch` | `int32_t` | Bytes per scanline in content buffer | -| `title` | `char[128]` | Title bar text | -| `visible` | `bool` | Window is shown | -| `focused` | `bool` | Window has input focus | -| `minimized` | `bool` | Window is minimized to icon | -| `maximized` | `bool` | Window is maximized | -| `resizable` | `bool` | Window has resize handles | -| `maxW`, `maxH` | `int32_t` | Max dimensions for maximize (-1 = screen) | -| `userData` | `void *` | Application data pointer | - -### ColorSchemeT - -All colors are pre-packed via `packColor()`: - -| Field | Usage | -|-------|-------| -| `desktop` | Desktop background | -| `windowFace` | Window frame fill | -| `windowHighlight` | Bevel light edge | -| `windowShadow` | Bevel dark edge | -| `activeTitleBg/Fg` | Focused title bar | -| `inactiveTitleBg/Fg` | Unfocused title bar | -| `contentBg/Fg` | Window content area | -| `menuBg/Fg` | Menu bar and popups | -| `menuHighlightBg/Fg` | Highlighted menu item | -| `buttonFace` | Button interior | -| `scrollbarBg/Fg/Trough` | Scrollbar elements | - -### BitmapFontT - -Fixed-width bitmap font (8px wide, 14 or 16px tall). Glyphs are packed -1bpp, `charHeight` bytes per glyph, MSB-first. - -### BevelStyleT +## Compositor (dvxComp.h) ```c -typedef struct { - uint32_t highlight; // top/left edge color - uint32_t shadow; // bottom/right edge color - uint32_t face; // interior fill (0 = no fill) - int32_t width; // border thickness in pixels -} BevelStyleT; +void dirtyListInit(DirtyListT *dl); +void dirtyListAdd(DirtyListT *dl, int32_t x, int32_t y, int32_t w, int32_t h); +void dirtyListMerge(DirtyListT *dl); +void dirtyListClear(DirtyListT *dl); +void flushRect(DisplayT *d, const RectT *r); +bool rectIntersect(const RectT *a, const RectT *b, RectT *result); +bool rectIsEmpty(const RectT *r); ``` +The compositing pipeline each frame: +1. Layers above call `dirtyListAdd()` for changed regions +2. `dirtyListMerge()` consolidates overlapping/adjacent rects +3. For each merged dirty rect, redraw desktop then each window + (back-to-front, painter's algorithm) +4. `flushRect()` copies each dirty rect from backBuf to the LFB + +Up to 128 dirty rects per frame (`MAX_DIRTY_RECTS`). + + --- -## Window chrome -Windows use a Motif/GEOS-style frame: +## Window Manager (dvxWm.h) -- 4px beveled outer border with perpendicular groove breaks -- 20px title bar (dark charcoal background when focused) -- Close button on the left edge (requires double-click) -- Minimize button on the right edge (always present) -- Maximize button to the left of minimize (resizable windows only) -- Optional menu bar below the title bar (20px) -- Optional scrollbars along the right and bottom edges (16px) +### Window Stack -Minimized windows appear as 64x64 beveled icons along the bottom of the -screen. If a window has an icon image set via `dvxSetWindowIcon()`, that -image is shown; otherwise a nearest-neighbor-scaled thumbnail of the -window's content buffer is used. Thumbnails are refreshed automatically -when the window's content changes, with updates staggered across frames -so only one icon redraws per interval. Double-click an icon to restore. +Windows are stored in an array of pointers ordered front-to-back: index +`count-1` is the topmost window. The compositor iterates back-to-front +for painting and front-to-back for hit testing. -### Keyboard window management +```c +void wmInit(WindowStackT *stack); +WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, + int32_t x, int32_t y, int32_t w, int32_t h, bool resizable); +void wmDestroyWindow(WindowStackT *stack, WindowT *win); +void wmRaiseWindow(WindowStackT *stack, DirtyListT *dl, int32_t idx); +void wmSetFocus(WindowStackT *stack, DirtyListT *dl, int32_t idx); +``` + +### Hit Testing + +```c +int32_t wmHitTest(const WindowStackT *stack, int32_t mx, int32_t my, int32_t *hitPart); +``` + +Returns the stack index and hit region: +`HIT_CONTENT`, `HIT_TITLE`, `HIT_CLOSE`, `HIT_RESIZE`, `HIT_MENU`, +`HIT_VSCROLL`, `HIT_HSCROLL`, `HIT_MINIMIZE`, `HIT_MAXIMIZE`, +`HIT_NONE`. + +### Window Operations + +```c +void wmMaximize(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT *win); +void wmMinimize(WindowStackT *stack, DirtyListT *dl, WindowT *win); +void wmRestore(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT *win); +void wmRestoreMinimized(WindowStackT *stack, DirtyListT *dl, WindowT *win); +``` + +### System Menu + +The system menu (control menu) appears on single-click of the close +gadget. Commands: Restore, Move, Size, Minimize, Maximize, Close, +Screenshot, Window Screenshot. Matches Windows 3.x behavior. + +### Keyboard Window Management | Key | Action | |-----|--------| @@ -1541,38 +844,349 @@ so only one icon redraws per interval. Double-click an icon to restore. | Alt+Space | Open system menu | | F10 | Activate menu bar | + --- -## Platform abstraction (`platform/dvxPlatform.h`) -All OS-specific code is behind a single interface. To port DVX, implement -these functions in a new `dvxPlatformXxx.c`: +## Widget System (dvxWidget.h) + +A retained-mode widget toolkit layered on top of the window manager. +Widgets form a tree rooted at a per-window VBox container. Layout is +automatic via a flexbox-like algorithm with weighted space distribution. + +### Widget Catalog (32 types) + +#### Containers + +| Widget | Constructor | Description | +|--------|-------------|-------------| +| VBox | `wgtVBox(parent)` | Vertical stack layout | +| HBox | `wgtHBox(parent)` | Horizontal stack layout | +| Frame | `wgtFrame(parent, title)` | Titled groupbox with bevel border | +| TabControl | `wgtTabControl(parent)` | Tabbed page container with scrollable headers | +| TabPage | `wgtTabPage(tabs, title)` | Single page within a TabControl | +| ScrollPane | `wgtScrollPane(parent)` | Scrollable container with automatic scrollbars | +| Splitter | `wgtSplitter(parent, vertical)` | Draggable divider between two child regions | +| Toolbar | `wgtToolbar(parent)` | Horizontal raised container for toolbar buttons | +| StatusBar | `wgtStatusBar(parent)` | Horizontal sunken container for status text | + +#### Basic Widgets + +| Widget | Constructor | Description | +|--------|-------------|-------------| +| Label | `wgtLabel(parent, text)` | Static text display with optional alignment | +| Button | `wgtButton(parent, text)` | Beveled push button with press animation | +| Checkbox | `wgtCheckbox(parent, text)` | Toggle checkbox with checkmark glyph | +| RadioGroup | `wgtRadioGroup(parent)` | Container for mutually exclusive radio buttons | +| Radio | `wgtRadio(group, text)` | Individual radio button with diamond indicator | + +#### Text Input Widgets + +| Widget | Constructor | Description | +|--------|-------------|-------------| +| TextInput | `wgtTextInput(parent, maxLen)` | Single-line text field with selection, undo, clipboard | +| PasswordInput | `wgtPasswordInput(parent, maxLen)` | Bullet-masked text field (no copy/cut) | +| MaskedInput | `wgtMaskedInput(parent, mask)` | Formatted input (e.g. `"(###) ###-####"`) | +| TextArea | `wgtTextArea(parent, maxLen)` | Multi-line text editor with scrollbars | + +#### Selection Widgets + +| Widget | Constructor | Description | +|--------|-------------|-------------| +| ListBox | `wgtListBox(parent)` | Scrollable item list with optional multi-select and drag reorder | +| Dropdown | `wgtDropdown(parent)` | Non-editable selection with popup list | +| ComboBox | `wgtComboBox(parent, maxLen)` | Editable text field with dropdown suggestions | +| TreeView | `wgtTreeView(parent)` | Hierarchical tree with expand/collapse, multi-select, drag reorder | +| TreeItem | `wgtTreeItem(parent, text)` | Item node within a TreeView | +| ListView | `wgtListView(parent)` | Multi-column list with sortable/resizable headers, multi-select | + +#### Value Widgets + +| Widget | Constructor | Description | +|--------|-------------|-------------| +| Slider | `wgtSlider(parent, min, max)` | Draggable track bar (horizontal or vertical) | +| Spinner | `wgtSpinner(parent, min, max, step)` | Numeric up/down input with increment buttons | +| ProgressBar | `wgtProgressBar(parent)` | Percentage display bar (horizontal or vertical) | + +#### Visual Widgets + +| Widget | Constructor | Description | +|--------|-------------|-------------| +| Image | `wgtImage(parent, data, w, h, pitch)` | Static image display (BMP/PNG/JPEG/GIF) | +| ImageButton | `wgtImageButton(parent, data, w, h, pitch)` | Clickable image button with press animation | +| Canvas | `wgtCanvas(parent, w, h)` | Drawable bitmap surface with programmatic drawing API | +| Spacer | `wgtSpacer(parent)` | Invisible flexible space (weight=100) | +| Separator | `wgtHSeparator(parent)` / `wgtVSeparator(parent)` | Beveled divider line | + +#### Special Widgets + +| Widget | Constructor | Description | +|--------|-------------|-------------| +| AnsiTerm | `wgtAnsiTerm(parent, cols, rows)` | VT100/ANSI terminal emulator with 16-color CGA palette, scrollback, blink | +| Timer | `wgtTimer(parent, intervalMs, repeat)` | Invisible callback timer (one-shot or repeating) | + + +### Widget Event Model + +All widget types share a universal set of event callbacks set directly +on the WidgetT struct: + +| Callback | Signature | Description | +|----------|-----------|-------------| +| `onClick` | `void (WidgetT *w)` | Mouse click completed on the widget | +| `onDblClick` | `void (WidgetT *w)` | Double-click on the widget | +| `onChange` | `void (WidgetT *w)` | Value or state changed (checkbox toggle, text edit, slider move, timer fire) | +| `onFocus` | `void (WidgetT *w)` | Widget received keyboard focus | +| `onBlur` | `void (WidgetT *w)` | Widget lost keyboard focus | + +Type-specific handlers (button press animation, listbox selection +highlight) run first, then these universal callbacks fire. + +Additional per-widget fields: `userData` (void pointer), `tooltip` +(hover text), `contextMenu` (right-click menu). + +### Initialization + +```c +WidgetT *wgtInitWindow(AppContextT *ctx, WindowT *win); +``` + +Attach the widget system to a window. Returns the root VBox container. +Installs `onPaint`, `onMouse`, `onKey`, and `onResize` handlers. Build +the UI by adding child widgets to the returned root. + +Call `wgtInvalidate(root)` after the tree is fully built to trigger the +initial layout and paint. + +### Layout System + +The layout engine runs in two passes: +1. **Measure** (bottom-up): compute minimum sizes for every widget +2. **Arrange** (top-down): distribute available space according to + weights, respecting min/max constraints + +#### Size Specifications + +Size fields (`minW`, `minH`, `maxW`, `maxH`, `prefW`, `prefH`, +`spacing`, `padding`) accept tagged values: + +```c +wgtPixels(120) // 120 pixels +wgtChars(15) // 15 character widths (15 * 8 = 120 pixels) +wgtPercent(50) // 50% of parent dimension +``` + +A raw `0` means auto (use the widget's natural/computed size). + +#### Weight + +The `weight` field controls extra-space distribution among siblings: +- `weight = 0` -- fixed size, does not stretch (default for most widgets) +- `weight = 100` -- normal stretch (default for spacers, text inputs) +- Relative ratios work: weights of 100, 200, 100 give a 1:2:1 split + +When all children have weight 0, the container's `align` property +(`AlignStartE`, `AlignCenterE`, `AlignEndE`) positions children within +the extra space. + +#### Container Properties + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `align` | `WidgetAlignE` | `AlignStartE` | Main-axis alignment | +| `spacing` | `int32_t` | 0 (4px) | Tagged gap between children | +| `padding` | `int32_t` | 0 (4px) | Tagged internal padding | + +### Widget Operations + +```c +void wgtSetText(WidgetT *w, const char *text); +const char *wgtGetText(const WidgetT *w); +void wgtSetEnabled(WidgetT *w, bool enabled); +void wgtSetReadOnly(WidgetT *w, bool readOnly); +void wgtSetVisible(WidgetT *w, bool visible); +void wgtSetFocused(WidgetT *w); +WidgetT *wgtGetFocused(void); +void wgtSetName(WidgetT *w, const char *name); +WidgetT *wgtFind(WidgetT *root, const char *name); +void wgtDestroy(WidgetT *w); +void wgtInvalidate(WidgetT *w); +void wgtInvalidatePaint(WidgetT *w); +void wgtSetTooltip(WidgetT *w, const char *text); +void wgtSetDebugLayout(AppContextT *ctx, bool enabled); +AppContextT *wgtGetContext(const WidgetT *w); +``` + +- `wgtInvalidate` triggers full relayout + repaint (use after + structural changes) +- `wgtInvalidatePaint` triggers repaint only (use for visual-only + changes like checkbox toggle) +- `wgtFind` searches by name using djb2 hash for fast rejection +- `wgtGetContext` walks up the tree to retrieve the AppContextT + +### Timer Widget + +```c +WidgetT *wgtTimer(WidgetT *parent, int32_t intervalMs, bool repeat); +void wgtTimerStart(WidgetT *w); +void wgtTimerStop(WidgetT *w); +void wgtTimerSetInterval(WidgetT *w, int32_t intervalMs); +bool wgtTimerIsRunning(const WidgetT *w); +``` + +Invisible widget that fires `onChange` at the specified interval. Does +not participate in layout. Set `repeat = false` for one-shot timers +that auto-stop after the first fire. `wgtUpdateTimers()` is called +automatically by `dvxUpdate()` each frame. + +### Layout and Paint Internals + +Available for manual use when embedding in a custom event loop: + +```c +int32_t wgtResolveSize(int32_t taggedSize, int32_t parentSize, int32_t charWidth); +void wgtLayout(WidgetT *root, int32_t availW, int32_t availH, const BitmapFontT *font); +void wgtPaint(WidgetT *root, DisplayT *d, const BlitOpsT *ops, + const BitmapFontT *font, const ColorSchemeT *colors); +``` + + +--- + + +## Font System + +DVX uses a fixed-width 8x16 bitmap font covering the full IBM Code Page +437 character set (256 glyphs). The font data is compiled in as a static +array -- no external font files. + +| Property | Value | +|----------|-------| +| Width | 8 pixels (fixed) | +| Height | 16 pixels (8x14 also available) | +| Encoding | CP437 (full 256 glyphs) | +| Format | 1 bit per pixel, MSB = leftmost, 16 bytes per glyph | +| Box drawing | Characters 176--223 (scrollbar arrows, window gadgets) | + +Character positions are pure multiplication (`x = col * 8`), and each +glyph scanline is exactly one byte, enabling simple per-scanline +rendering without bit shifting across byte boundaries. + + +## Cursor System + +Five software-rendered cursor shapes, stored as 16x16 AND/XOR bitmask +pairs (the standard IBM VGA hardware cursor format): + +| ID | Constant | Shape | +|----|----------|-------| +| 0 | `CURSOR_ARROW` | Standard arrow pointer | +| 1 | `CURSOR_RESIZE_H` | Horizontal resize (left-right) | +| 2 | `CURSOR_RESIZE_V` | Vertical resize (up-down) | +| 3 | `CURSOR_RESIZE_DIAG_NWSE` | NW-SE diagonal resize | +| 4 | `CURSOR_RESIZE_DIAG_NESW` | NE-SW diagonal resize | + +Cursors are painted in software into the backbuffer because VESA VBE +does not provide a hardware sprite. The affected region is flushed to +the LFB each frame. + + +## Window Chrome + +Windows use a Motif/GEOS-style frame: + +- 4px beveled outer border with perpendicular groove breaks +- 20px title bar (dark background when focused) +- Close button on the left edge (single-click opens system menu, + double-click closes) +- Minimize button on the right edge (always present) +- Maximize button to the left of minimize (resizable windows only) +- Optional menu bar below the title bar (20px) +- Optional scrollbars along the right and bottom edges (16px) + +Minimized windows appear as 64x64 beveled icons along the bottom of the +screen. If a window icon is set via `dvxSetWindowIcon()`, that image is +shown; otherwise a scaled thumbnail of the window's content buffer is +used. Thumbnails refresh automatically one per frame (staggered). +Double-click an icon to restore. + + +## Platform Abstraction (platform/dvxPlatform.h) + +All OS-specific code is behind a single interface. To port DVX, +implement these functions in a new `dvxPlatformXxx.c`: | Function | Purpose | |----------|---------| -| `platformInit()` | One-time setup (signal handling, etc.) | +| `platformInit()` | One-time setup (signal handlers, etc.) | | `platformYield()` | Cooperative multitasking yield | | `platformVideoInit()` | Set up video mode, LFB, backbuffer | | `platformVideoShutdown()` | Restore text mode, free resources | +| `platformVideoEnumModes()` | Enumerate available video modes | | `platformVideoSetPalette()` | Set 8-bit palette entries | +| `platformVideoFreeBuffers()` | Free backbuffer without restoring text mode | | `platformFlushRect()` | Copy rectangle from backbuffer to LFB | -| `platformSpanFill8/16/32()` | Optimized pixel fill | -| `platformSpanCopy8/16/32()` | Optimized pixel copy | +| `platformSpanFill8/16/32()` | Optimized pixel fill (rep stosl on DOS) | +| `platformSpanCopy8/16/32()` | Optimized pixel copy (rep movsd on DOS) | | `platformMouseInit()` | Initialize mouse driver | | `platformMousePoll()` | Read mouse position and buttons | +| `platformMouseWheelInit()` | Detect and activate mouse wheel | +| `platformMouseWheelPoll()` | Read wheel delta | +| `platformMouseWarp()` | Move cursor to absolute position | +| `platformMouseSetAccel()` | Set double-speed threshold | | `platformKeyboardGetModifiers()` | Read shift/ctrl/alt state | -| `platformKeyboardRead()` | Read next key from buffer | +| `platformKeyboardRead()` | Non-blocking key read | | `platformAltScanToChar()` | Map Alt+scancode to ASCII | | `platformValidateFilename()` | Check filename for OS constraints | +| `platformGetSystemInfo()` | Gather hardware info string | +| `platformGetMemoryInfo()` | Query total/free RAM | +| `platformMkdirRecursive()` | Create directory tree | +| `platformChdir()` | Change directory (with drive support on DOS) | +| `platformPathDirEnd()` | Find last path separator | +| `platformLineEnding()` | Native line ending string | +| `platformStripLineEndings()` | Remove CR from CR+LF pairs | ---- -## Hardware requirements +## Key Constants (dvxTypes.h) + +### Extended Key Codes + +Non-ASCII keys encoded as `(scancode | 0x100)`: + +``` +KEY_F1 through KEY_F12 +KEY_INSERT, KEY_DELETE +KEY_HOME, KEY_END +KEY_PGUP, KEY_PGDN +``` + +### Chrome Constants + +``` +CHROME_BORDER_WIDTH 4 Border thickness +CHROME_TITLE_HEIGHT 20 Title bar height +CHROME_MENU_HEIGHT 20 Menu bar height +CHROME_CLOSE_BTN_SIZE 16 Close gadget size +SCROLLBAR_WIDTH 16 Scrollbar track width +MAX_WINDOWS 64 Maximum simultaneous windows +MAX_DIRTY_RECTS 128 Dirty rects per frame +``` + +### Mouse Constants + +``` +MOUSE_LEFT 1 Left button bitmask +MOUSE_RIGHT 2 Right button bitmask +MOUSE_MIDDLE 4 Middle button bitmask +MOUSE_WHEEL_STEP 3 Lines per wheel notch +``` + + +## Hardware Requirements - 486 or later CPU -- VESA VBE 2.0+ compatible video card with linear framebuffer support -- PS/2 mouse (or compatible mouse driver) +- VESA VBE 2.0+ compatible video card with linear framebuffer +- PS/2 mouse (or compatible driver) - DPMI host (CWSDPMI, Windows DOS box, DOSBox, 86Box) -Tested on 86Box with PCI video cards. DOSBox is a trusted reference -platform. +Tested on 86Box with PCI video cards. diff --git a/dvxshell/README.md b/dvxshell/README.md index 84dae5e..9149fce 100644 --- a/dvxshell/README.md +++ b/dvxshell/README.md @@ -1,140 +1,362 @@ # DVX Shell -Windows 3.x-style desktop shell for DOS. Loads applications as DXE3 shared -libraries and includes crash -recovery so one bad app doesn't take down the system. - -## Building - -``` -make # builds ../bin/dvx.exe -make clean # removes objects and binary -``` - -Requires `lib/libdvx.a` and `lib/libtasks.a` to be built first. +The DVX Shell (`dvx.exe`) is the host process for the DVX desktop environment. +It initializes the GUI subsystem, loads DXE3 application modules at runtime, +runs the cooperative main loop, and provides crash recovery so a faulting app +does not take down the entire system. Think of it as the Windows 3.1 Program +Manager and kernel combined into one executable. ## Files | File | Purpose | |------|---------| -| `shellMain.c` | Entry point, main loop, crash recovery, logging | -| `shellApp.c` | App loading (dlopen), lifecycle, reaping, resource tracking | -| `shellApp.h` | ShellAppT, AppDescriptorT, AppStateE, DxeAppContextT, shell API | -| `shellExport.c` | DXE export table and wrapper functions | -| `Makefile` | Build rules, links `-ldvx -ltasks -ldxe -lm` | +| `shellMain.c` | Entry point, main loop, crash recovery, logging, desktop update callbacks | +| `shellApp.c` | App loading (dlopen/dlsym), lifecycle state machine, reaping, resource tracking, config dirs | +| `shellApp.h` | `ShellAppT`, `AppDescriptorT`, `AppStateE`, `DxeAppContextT`, public shell API | +| `shellExport.c` | DXE export table (400+ symbols), wrapper functions for resource tracking | +| `shellInfo.c` | System information gathering (delegates to platform layer), caches result | +| `shellInfo.h` | `shellInfoInit()`, `shellGetSystemInfo()` | +| `shellTaskMgr.c` | Task Manager window -- list view, Switch To / End Task / Run buttons | +| `shellTaskMgr.h` | `shellTaskMgrOpen()`, `shellTaskMgrRefresh()` | +| `Makefile` | Cross-compile rules, links `-ldvx -ltasks -lm` | -## Shell Main Loop +## Building -Each iteration of the main loop: - -1. `dvxUpdate()` -- process input events, dispatch callbacks, composite dirty rects -2. `tsYield()` -- give CPU time to main-loop app tasks -3. `shellReapApps()` -- clean up apps that terminated this frame -4. `desktopUpdate()` -- notify the desktop app if anything changed - -An idle callback (`idleYield`) yields to app tasks during quiet periods when -there are no events or dirty rects to process. - -## DXE App Contract - -Every `.app` file must export these symbols: - -```c -// Required: app metadata -AppDescriptorT appDescriptor = { - .name = "My App", - .hasMainLoop = false, - .multiInstance = false, - .stackSize = SHELL_STACK_DEFAULT, - .priority = TS_PRIORITY_NORMAL -}; - -// Required: entry point -int32_t appMain(DxeAppContextT *ctx); - -// Optional: graceful shutdown hook -void appShutdown(void); +``` +make # builds ../bin/dvx.exe (also builds libdvx.a, libtasks.a) +make clean # removes objects and binary ``` -### AppDescriptorT Fields +Requires the DJGPP cross-compiler toolchain and the DXE3 tools (`dxe3gen`). -| Field | Type | Description | -|-------|------|-------------| -| `name` | `char[64]` | Display name shown in Task Manager | -| `hasMainLoop` | `bool` | `true` = gets its own cooperative task; `false` = callback-only | -| `multiInstance` | `bool` | `true` = allow multiple instances via temp file copy | -| `stackSize` | `int32_t` | Task stack size (`SHELL_STACK_DEFAULT` for 8 KB default) | -| `priority` | `int32_t` | Task priority (`TS_PRIORITY_LOW`/`NORMAL`/`HIGH`) | +## Startup Sequence -### DxeAppContextT +`main()` in `shellMain.c` performs initialization in this order: -Passed to `appMain()`: +1. **Change to exe directory** -- resolve the directory containing `dvx.exe` + via `argv[0]` and `platformChdir()` so that relative paths (`CONFIG/`, + `APPS/`, etc.) work regardless of where the user launched from. +2. **Truncate log** -- open `dvx.log` for write to clear it, then close. + All subsequent writes use append-per-write (the file is never held open). +3. **Load preferences** -- `prefsLoad("CONFIG/DVX.INI")`. Missing file or + keys silently fall back to compiled-in defaults. +4. **dvxInit** -- initialize VESA video (LFB), backbuffer, compositor, window + manager, font, cursor, input subsystems. Reads video width/height/bpp from + preferences (default 640x480x16). +5. **Mouse config** -- read wheel direction, double-click speed, acceleration + from `[mouse]` section and call `dvxSetMouseConfig()`. +6. **Color scheme** -- read `[colors]` section (20 RGB triplets), apply via + `dvxApplyColorScheme()`. +7. **Wallpaper** -- read `[desktop]` section for wallpaper path and mode + (stretch/tile/center), load via `dvxSetWallpaper()`. +8. **Video mode log** -- enumerate all available VESA modes to `dvx.log`. +9. **Task system** -- `tsInit()`, set shell task (task 0) to + `TS_PRIORITY_HIGH` so the UI stays responsive under load. +10. **System info** -- `shellInfoInit()` gathers CPU, memory, drive info via + the platform layer and logs it. +11. **DXE exports** -- `shellExportInit()` calls `dlregsym()` to register the + export table. Must happen before any `dlopen()`. +12. **App slot table** -- `shellAppInit()` zeroes the 32-slot fixed array. +13. **Idle/hotkey callbacks** -- wire up `idleYield`, `ctrlEscHandler`, + `titleChangeHandler` on the `AppContextT`. +14. **Desktop app** -- `shellLoadApp(ctx, "apps/progman/progman.app")`. If + this fails, the shell exits. +15. **Crash handlers** -- `installCrashHandler()` registers signal handlers + for SIGSEGV, SIGFPE, SIGILL. Installed last so initialization crashes + get the default DJGPP abort-with-register-dump instead of our recovery + path. -| Field | Type | Description | -|-------|------|-------------| -| `shellCtx` | `AppContextT *` | The shell's GUI context for creating windows, drawing, etc. | -| `appId` | `int32_t` | This app's unique ID (1-based slot index) | -| `appDir` | `char[260]` | Directory containing the `.app` file for relative resource paths | +## Main Loop -## App Types +The main loop runs until `dvxQuit()` sets `ctx->running = false`: -**Callback-only** (`hasMainLoop = false`): -- `appMain` called in shell's task 0, creates windows, registers callbacks, returns 0 -- App lives through event callbacks dispatched by `dvxUpdate()` -- Lifecycle ends when the last window is closed +``` +while (ctx->running) { + dvxUpdate(ctx); // (1) + tsYield(); // (2) + shellReapApps(ctx); // (3) + shellDesktopUpdate(); // (4) +} +``` -**Main-loop** (`hasMainLoop = true`): -- Shell creates a cooperative task via `tsCreate()` -- `appMain` runs in that task with its own loop calling `tsYield()` -- Lifecycle ends when `appMain` returns - -## Multi-Instance Support - -DXE3's `dlopen` is reference-counted per path: loading the same `.app` twice -returns the same handle, sharing all global/static state. For apps that support -multiple instances (`multiInstance = true`), the shell copies the `.app` to a -temp file before loading, giving each instance independent code and data. The -temp file is cleaned up when the app terminates. - -Apps that don't support multiple instances (`multiInstance = false`, the default) -are blocked from loading a second time with an error message. - -Temp file paths use the `TEMP` or `TMP` environment variable if set, falling -back to the current directory. - -## Resource Tracking - -The shell tracks which app owns which windows via `sCurrentAppId`, a global -set before calling any app code. The shell's `dvxCreateWindow` wrapper stamps -`win->appId` with the current app ID. On termination, the shell destroys all -windows belonging to the app. +1. **dvxUpdate** -- processes mouse/keyboard input, dispatches widget and + window callbacks, composites dirty rects, flushes to the LFB. This is the + shell's primary job. When idle (no events, no dirty rects), dvxUpdate + calls the registered `idleCallback` which yields to app tasks. +2. **tsYield** -- explicit yield to give app tasks CPU time even during busy + frames with many repaints. Without this, a flurry of mouse-move events + could starve app tasks because dvxUpdate would keep finding work. +3. **shellReapApps** -- scans the 32-slot app table for apps in + `AppStateTerminatingE`. Calls the shutdown hook (if present), destroys + all windows owned by the app, kills the task, closes the DXE handle, + and cleans up temp files. Returns true if anything was reaped. +4. **shellDesktopUpdate** -- if apps were reaped, iterates the registered + desktop update callback list so listeners (Program Manager, Task Manager) + can refresh their UI. ## Crash Recovery -Signal handlers for SIGSEGV, SIGFPE, and SIGILL `longjmp` back to the shell's -main loop. The scheduler is fixed via `tsRecoverToMain()`, the crashed app is -force-killed, and a diagnostic message is displayed. Register state and app -identity are logged to `dvx.log`. +The shell provides Windows 3.1-style fault tolerance using `setjmp`/`longjmp`: + +1. `installCrashHandler()` registers `crashHandler` for SIGSEGV, SIGFPE, + SIGILL. +2. `setjmp(sCrashJmp)` in `main()` establishes the recovery point. +3. If a signal fires (in any task), `crashHandler` logs the crash details + (signal, app name, full register dump from `__djgpp_exception_state_ptr`), + re-installs the handler (DJGPP uses SysV semantics -- handler resets to + `SIG_DFL` after each delivery), then `longjmp(sCrashJmp, 1)`. +4. `longjmp` restores the main task's stack frame to the `setjmp` point. + This is safe because cooperative switching means the main task's stack + is always intact -- it was cleanly suspended at a yield point. +5. `tsRecoverToMain()` fixes the task scheduler's `currentIdx` so it points + back to task 0 instead of the crashed task. +6. The crashed app is force-killed via `shellForceKillApp()`, and a message + box is displayed: "'AppName' has caused a fault and will be terminated." +7. The desktop update callbacks are notified so the UI refreshes. + +The register dump logged includes EIP, CS, all GPRs (EAX-EDI), segment +registers, and EFLAGS -- invaluable for post-mortem debugging. + +## App Lifecycle + +### DXE3 Loading + +DXE3 is DJGPP's dynamic linking mechanism. Each `.app` file is a DXE3 shared +object. The shell resolves symbols from the export table registered via +`dlregsym()`. The load sequence in `shellLoadApp()`: + +1. Allocate a slot from the 32-entry fixed array (slot 0 is the shell). +2. Check if this DXE path is already loaded (`findLoadedPath`). + - If the existing app's descriptor says `multiInstance = true`, copy + the `.app` to a unique temp file (using `TEMP`/`TMP` env var) so + `dlopen` gets an independent code+data image. + - If `multiInstance = false`, block with an error message. +3. `dlopen(loadPath, RTLD_GLOBAL)` -- load the DXE. +4. `dlsym(handle, "_appDescriptor")` -- resolve the metadata struct. +5. `dlsym(handle, "_appMain")` -- resolve the entry point. +6. `dlsym(handle, "_appShutdown")` -- resolve the optional shutdown hook. +7. Fill in the `ShellAppT` slot (name, path, handle, state, etc.). +8. Derive `appDir` (directory containing the `.app` file) and `configDir` + (`CONFIG//` -- mirrors the app's directory structure under + `CONFIG/`). +9. Set `sCurrentAppId` to the slot index. +10. Launch: + - **Callback-only** (`hasMainLoop = false`): call `entryFn()` directly + in task 0. The app creates windows, registers callbacks, and returns. + - **Main-loop** (`hasMainLoop = true`): call `tsCreate()` to make a + cooperative task with the descriptor's stack size and priority. The + task wrapper sets `sCurrentAppId`, calls `entryFn()`, and marks the + app `AppStateTerminatingE` when it returns. +11. Reset `sCurrentAppId` to 0. Set state to `AppStateRunningE`. Notify + desktop update callbacks. + +### AppDescriptorT + +Every DXE app exports a global `AppDescriptorT`: + +| Field | Type | Description | +|-------|------|-------------| +| `name` | `char[64]` | Display name (Task Manager, title bars) | +| `hasMainLoop` | `bool` | `true` = dedicated cooperative task; `false` = callback-only | +| `multiInstance` | `bool` | `true` = allow multiple simultaneous instances via temp file copy | +| `stackSize` | `int32_t` | Task stack in bytes (`SHELL_STACK_DEFAULT` = use default) | +| `priority` | `int32_t` | `TS_PRIORITY_LOW` / `TS_PRIORITY_NORMAL` / `TS_PRIORITY_HIGH` | + +### Callback vs Main-Loop Apps + +**Callback-only** (`hasMainLoop = false`): +- `appMain()` runs in the shell's task 0, creates windows, registers event + callbacks, and returns immediately. +- All subsequent work happens through callbacks dispatched by `dvxUpdate()`. +- Lifecycle ends when the last window is closed (detected by the + `shellWrapDestroyWindow` wrapper). +- No task stack allocated -- simpler and cheaper. +- Examples: Program Manager, Notepad, Control Panel, DVX Demo, Image Viewer. + +**Main-loop** (`hasMainLoop = true`): +- A cooperative task is created via `tsCreate()`. +- `appMain()` runs in that task with its own loop calling `tsYield()`. +- Needed when the app has ongoing work that cannot be expressed purely as + event callbacks (polling, computation, animation). +- Lifecycle ends when `appMain()` returns. +- Example: Clock (polls `time()` each second). + +### Multi-Instance Support + +DXE3's `dlopen` is reference-counted per path -- loading the same `.app` +twice returns the same handle, sharing all globals and statics. For apps +that set `multiInstance = true`, the shell copies the `.app` to a temp file +(e.g., `C:\TEMP\_dvx02.app`) before `dlopen`, giving each instance its own +code and data segment. Temp files are cleaned up on app termination. + +### App State Machine + +``` +Free -> Loaded -> Running -> Terminating -> Free +``` + +- **Free**: slot available. +- **Loaded**: DXE loaded, entry point not yet called (transient). +- **Running**: entry point called, app is active. +- **Terminating**: app's task returned or last window closed; awaiting reap. + +## Resource Tracking + +`sCurrentAppId` is a global set before calling any app code (entry, shutdown, +callbacks). The shell's `dvxCreateWindow` wrapper stamps every new window with +`win->appId = sCurrentAppId`. This is how the shell knows which windows belong +to which app, enabling: + +- Per-app window cleanup on crash or termination (walk the window stack, + destroy all windows with matching `appId`). +- The last-window-closes-app rule for callback-only apps: when + `shellWrapDestroyWindow` detects that a callback-only app has no remaining + windows, it marks the app `AppStateTerminatingE`. + +`sCurrentAppId` is a simple global (not thread-local) because cooperative +multitasking means only one task runs at a time. + +## Task Manager + +`shellTaskMgr.c` implements a shell-level Task Manager accessible via +**Ctrl+Esc** regardless of which app is focused or whether the desktop app +is running. It is owned by the shell (appId = 0), not by any DXE app. + +Features: +- **ListView** with 5 columns: Name, Title, File, Type (Task/Callback), Status. +- **Switch To** -- find the app's topmost window, restore if minimized, raise + and focus it. +- **End Task** -- force-kill the selected app via `shellForceKillApp()`. +- **Run...** -- open a file dialog filtered to `*.app`, load the selected file. +- **Status bar** -- shows running app count and memory usage (total/free MB). +- Registers with `shellRegisterDesktopUpdate` so the list auto-refreshes when + apps load, terminate, or crash. + +## Desktop Update Callbacks + +`shellRegisterDesktopUpdate(fn)` adds a function pointer to a dynamic array +(managed via stb_ds `arrput`/`arrdel`). `shellDesktopUpdate()` iterates the +array and calls each registered function. + +This is the mechanism by which the shell notifies interested parties (Program +Manager status bar, Task Manager list) when app state changes -- without +polling. Multiple listeners are supported. Listeners should call +`shellUnregisterDesktopUpdate()` before they are destroyed. + +## App Config Storage + +Each app gets a dedicated writable config directory under `CONFIG/`. The path +mirrors the app's location under `APPS/`: + +``` +App path: APPS/PROGMAN/progman.app +Config dir: CONFIG/PROGMAN/ +``` + +API: +- `shellEnsureConfigDir(ctx)` -- create the directory tree via + `platformMkdirRecursive()`. Returns 0 on success. +- `shellConfigPath(ctx, "settings.ini", buf, sizeof(buf))` -- build a full + path to a file in the config directory. + +Apps use the standard preferences system (`prefsLoad`/`prefsSave`) pointed at +their config directory for persistent settings. ## DXE Export Table -The shell registers a symbol export table via `dlregsym()` before loading any -apps. Most symbols (all `dvx*`, `wgt*`, `ts*`, drawing functions, and required -libc functions) are exported directly. `dvxCreateWindow` and `dvxDestroyWindow` -are exported as wrappers that add resource tracking. +`shellExport.c` contains the ABI contract between the shell and apps. Three +categories of exports: -## Shell API +1. **Wrapped functions** (3): `dvxCreateWindow`, `dvxCreateWindowCentered`, + `dvxDestroyWindow`. These are intercepted to stamp `win->appId` for + resource ownership tracking. Apps see them under their original names -- + the wrapping is transparent. + +2. **Direct exports** (200+): all other `dvx*`, `wgt*`, `wm*`, `ts*`, + drawing, preferences, platform, and shell API functions. Safe to call + without interception. + +3. **libc / libm / runtime exports** (200+): DXE3 modules are relocatable + objects, not fully linked executables. Every C library function a DXE + calls must be explicitly listed so the loader can resolve it at dlopen + time. This includes: + - Memory (malloc, calloc, realloc, free) + - String operations (str*, mem*) + - Formatted I/O (printf, snprintf, fprintf, sscanf, etc.) + - File I/O (fopen, fread, fwrite, fclose, etc.) + - Directory operations (opendir, readdir, closedir, mkdir) + - Time (time, localtime, clock, strftime) + - Math (sin, cos, sqrt, pow, floor, ceil, etc.) + - stb_ds internals (arrput/arrfree/hm* macro backends) + - stb_image / stb_image_write + - libgcc 64-bit integer helpers (__divdi3, __moddi3, etc.) + - DJGPP stdio internals (__dj_stdin, __dj_stdout, __dj_stderr) + +The table is registered once via `dlregsym()` before any `dlopen()`. + +## System Hotkeys + +These are always active regardless of which app is focused: + +| Hotkey | Action | +|--------|--------| +| Alt+Tab | Cycle windows forward (rotate top to bottom of stack) | +| Shift+Alt+Tab | Cycle windows backward (pull bottom to top) | +| Alt+F4 | Close the focused window (calls its onClose callback) | +| Ctrl+F12 | Full screen screenshot -- prompts for save path (PNG/BMP/JPG) | +| Ctrl+Shift+F12 | Focused window screenshot -- prompts for save path | +| Ctrl+Esc | Open/raise the Task Manager (shell-level, always available) | +| F10 | Activate/toggle the focused window's menu bar | +| Alt+Space | Open/close the system menu on the focused window | + +## Screenshot System + +Two screenshot functions are available, both accessible from the system menu +(Alt+Space) or via hotkeys: + +- `interactiveScreenshot(ctx)` -- captures the full screen (composited + backbuffer), then opens a Save As file dialog filtered to PNG/BMP/JPG/TGA. + Called by Ctrl+F12 or the "Screenshot..." system menu item. +- `interactiveWindowScreenshot(ctx, win)` -- captures just the focused + window's content buffer. Called by Ctrl+Shift+F12 or the "Window Shot..." + system menu item. + +The system menu also includes standard window operations: Restore, Move, Size, +Minimize, Maximize, and Close. + +## Logging + +`shellLog(fmt, ...)` appends a printf-formatted line to `dvx.log`. The file +is opened, written, and closed on each call (append-per-write) so: +- The file is never held open, allowing Notepad to read it while the shell + runs. +- Writes are flushed immediately, important for crash diagnostics. +- The file is truncated once at startup. + +Log output includes: startup sequence, preferences applied, video modes +enumerated, system information, app load/reap events, crash details with +full register dumps. + +## Shell API Summary | Function | Description | |----------|-------------| -| `shellAppInit()` | Initialize the app slot table | -| `shellLoadApp(ctx, path)` | Load and start an app from a `.app` file | -| `shellReapApps(ctx)` | Clean up terminated apps (call each frame) | -| `shellReapApp(ctx, app)` | Gracefully shut down a single app | -| `shellForceKillApp(ctx, app)` | Forcibly kill an app (skip shutdown hook) | +| `shellAppInit()` | Zero the 32-slot app table | +| `shellLoadApp(ctx, path)` | Load and start a DXE app. Returns app ID (>= 1) or -1 | +| `shellReapApps(ctx)` | Clean up terminated apps (call each frame). Returns true if any reaped | +| `shellReapApp(ctx, app)` | Gracefully shut down one app (calls shutdown hook) | +| `shellForceKillApp(ctx, app)` | Forcibly kill an app -- skips shutdown hook | | `shellTerminateAllApps(ctx)` | Kill all running apps (shell shutdown) | -| `shellGetApp(appId)` | Get app slot by ID | -| `shellRunningAppCount()` | Count running apps | -| `shellLog(fmt, ...)` | Write to `dvx.log` | +| `shellGetApp(appId)` | Get app slot by ID (NULL if invalid/free) | +| `shellRunningAppCount()` | Count running apps (excluding the shell) | +| `shellLog(fmt, ...)` | Append to dvx.log | +| `shellEnsureConfigDir(ctx)` | Create an app's config directory tree | +| `shellConfigPath(ctx, name, buf, size)` | Build path to file in app's config dir | +| `shellExportInit()` | Register DXE symbol export table via dlregsym() | | `shellRegisterDesktopUpdate(fn)` | Register callback for app state changes | -| `shellExportInit()` | Register DXE symbol export table | +| `shellUnregisterDesktopUpdate(fn)` | Remove a previously registered callback | +| `shellDesktopUpdate()` | Notify all registered listeners | +| `shellTaskMgrOpen(ctx)` | Open or raise the Task Manager window | +| `shellTaskMgrRefresh()` | Refresh the Task Manager list and status | +| `shellInfoInit(ctx)` | Gather and log system hardware information | +| `shellGetSystemInfo()` | Return cached system info text | diff --git a/packet/README.md b/packet/README.md index ae5bb99..fc04b1f 100644 --- a/packet/README.md +++ b/packet/README.md @@ -1,77 +1,191 @@ -# Packet -- Reliable Serial Transport +# Packet -- Reliable Serial Transport with HDLC Framing + +Packetized serial transport providing reliable, ordered delivery over an +unreliable serial link. Uses HDLC-style byte-stuffed framing, CRC-16-CCITT +error detection, and a Go-Back-N sliding window protocol for automatic +retransmission. + +This layer sits on top of an already-open rs232 COM port. It does not +open or close the serial port itself. -Packetized serial transport with HDLC-style framing, CRC-16 error -detection, and a Go-Back-N sliding window protocol for reliable, -ordered delivery over an unreliable serial link. ## Architecture ``` - Application - | - [packet] framing, CRC, retransmit, ordering - | - [rs232] raw byte I/O +Application + | + | pktSend() queue a packet for reliable delivery + | pktPoll() receive, process ACKs/NAKs, check retransmit timers + | +[Packet Layer] framing, CRC, sequencing, sliding window ARQ + | +[rs232] raw byte I/O via ISR-driven ring buffers + | +UART ``` -The packet layer sits on top of an already-open rs232 COM port. It -does not open or close the serial port itself. +The packet layer adds framing, error detection, and reliability to the +raw byte stream provided by rs232. The caller provides a receive callback +that is invoked synchronously from `pktPoll()` for each complete, CRC-verified, +in-order data packet. + ## Frame Format -Before byte stuffing: +Before byte stuffing, each frame has the following layout: ``` [0x7E] [SEQ] [TYPE] [LEN] [PAYLOAD...] [CRC_LO] [CRC_HI] ``` -| Field | Size | Description | -|---------|---------|--------------------------------------| -| `0x7E` | 1 byte | Frame delimiter (flag byte) | -| `SEQ` | 1 byte | Sequence number (wrapping uint8) | -| `TYPE` | 1 byte | Frame type (see below) | -| `LEN` | 1 byte | Payload length (0-255) | -| Payload | 0-255 | Application data | -| `CRC` | 2 bytes | CRC-16-CCITT over SEQ+TYPE+LEN+payload | +| Field | Size | Description | +|-----------|-----------|----------------------------------------------| +| `0x7E` | 1 byte | Flag byte -- frame delimiter | +| `SEQ` | 1 byte | Sequence number (wrapping uint8_t, 0-255) | +| `TYPE` | 1 byte | Frame type (DATA, ACK, NAK, RST) | +| `LEN` | 1 byte | Payload length (0-255) | +| Payload | 0-255 | Application data | +| `CRC` | 2 bytes | CRC-16-CCITT, little-endian, over SEQ..payload | + +The header is 3 bytes (SEQ + TYPE + LEN), the CRC is 2 bytes, so the +minimum frame size (no payload) is 5 bytes. The maximum frame size (255-byte +payload) is 260 bytes before byte stuffing. ### Frame Types -| Type | Value | Description | -|--------|-------|----------------------------------------------| -| `DATA` | 0x00 | Data frame carrying application payload | -| `ACK` | 0x01 | Cumulative acknowledgment (next expected seq) | -| `NAK` | 0x02 | Negative ack (request retransmit from seq) | -| `RST` | 0x03 | Connection reset | +| Type | Value | Description | +|--------|-------|----------------------------------------------------| +| `DATA` | 0x00 | Carries application payload | +| `ACK` | 0x01 | Cumulative acknowledgment -- next expected sequence | +| `NAK` | 0x02 | Negative ack -- request retransmit from this seq | +| `RST` | 0x03 | Connection reset -- clear all state | ### Byte Stuffing -The flag byte (`0x7E`) and escape byte (`0x7D`) are escaped within -frame data: +HDLC transparency encoding ensures the flag byte (0x7E) and escape byte +(0x7D) never appear in the frame body. Within the frame data (everything +between flags), these two bytes are escaped by prefixing with 0x7D and +XORing the original byte with 0x20: - `0x7E` becomes `0x7D 0x5E` - `0x7D` becomes `0x7D 0x5D` -## Reliability +In the worst case, every byte in the frame is escaped, doubling the wire +size. In practice, these byte values are uncommon in typical data and the +overhead is minimal. -The protocol uses Go-Back-N with a configurable sliding window -(1-8 slots, default 4): +### Why HDLC Framing + +HDLC's flag-byte + byte-stuffing scheme is the simplest way to delimit +variable-length frames on a raw byte stream. The 0x7E flag byte +unambiguously marks frame boundaries. This is proven, lightweight, and +requires zero buffering at the framing layer. + +The alternative -- length-prefixed framing -- is fragile on noisy links +because a corrupted length field permanently desynchronizes the receiver. +With HDLC framing, the receiver can always resynchronize by hunting for +the next flag byte. + + +## CRC-16-CCITT + +Error detection uses CRC-16-CCITT (polynomial 0x1021, initial value +0xFFFF). The CRC covers the SEQ, TYPE, LEN, and payload fields. It is +stored little-endian in the frame (CRC low byte first, then CRC high byte). + +The CRC is computed via a 256-entry lookup table (512 bytes of `.rodata`). +Table-driven CRC is approximately 10x faster than bit-by-bit computation +on a 486 -- a worthwhile trade for a function called on every frame +transmitted and received. + + +## Go-Back-N Sliding Window Protocol + +### Why Go-Back-N + +Go-Back-N ARQ is simpler than Selective Repeat -- the receiver does not +need an out-of-order reassembly buffer and only tracks a single expected +sequence number. This works well for the low bandwidth-delay product of a +serial link. On a 115200 bps local connection, the round-trip time is +negligible, so the window rarely fills. + +Go-Back-N's retransmit-all-from-NAK behavior wastes bandwidth on lossy +links, but serial links are nearly lossless. The CRC check is primarily a +safety net for electrical noise, not a routine error recovery mechanism. + +### Protocol Details + +The sliding window is configurable from 1 to 8 slots (default 4). +Sequence numbers are 8-bit unsigned integers that wrap naturally at 256. +The sequence space (256) is much larger than 2x the maximum window (16), +so there is no ambiguity between old and new frames. + +**Sender behavior:** +- Assigns a monotonically increasing sequence number to each DATA frame +- Retains a copy of each sent frame in a retransmit slot until it is + acknowledged +- When the window is full (`txCount >= windowSize`), blocks or returns + `PKT_ERR_TX_FULL` depending on the `block` parameter + +**Receiver behavior:** +- Accepts frames strictly in order (`seq == rxExpectSeq`) +- On in-order delivery, increments `rxExpectSeq` and sends an ACK + carrying the new expected sequence number (cumulative acknowledgment) +- Out-of-order frames within the window trigger a NAK for the expected + sequence number +- Duplicate and out-of-window frames are silently discarded + +**ACK processing:** +- ACKs carry the next expected sequence number (cumulative) +- On receiving an ACK, the sender frees all retransmit slots with + sequence numbers less than the ACK's sequence number + +**NAK processing:** +- A NAK requests retransmission from a specific sequence number +- The sender retransmits that frame AND all subsequent unacknowledged + frames (the Go-Back-N property) +- Each retransmitted slot has its timer reset + +**RST processing:** +- Resets all sequence numbers and buffers to zero on both sides +- The remote side also sends a RST in response + +### Timer-Based Retransmission + +Each retransmit slot tracks the time it was last (re)transmitted. If +500ms elapses without an ACK, the slot is retransmitted and the timer +is reset. This handles the case where an ACK or NAK was lost on the +wire -- without this safety net, the connection would stall permanently. + +The 500ms timeout is conservative for a local serial link (RTT is under +1ms) but accounts for the remote side being busy processing. On BBS +connections through the Linux proxy, the round-trip includes TCP latency, +making the generous timeout appropriate. + +### Receive State Machine + +Incoming bytes from the serial port are fed through a three-state HDLC +deframing state machine: + +| State | Description | +|----------|------------------------------------------------------| +| `HUNT` | Discarding bytes until a flag (0x7E) is seen | +| `ACTIVE` | Accumulating frame bytes; flag ends frame, ESC escapes | +| `ESCAPE` | Previous byte was 0x7D; XOR this byte with 0x20 | + +The flag byte serves double duty: it ends the current frame AND starts +the next one. Back-to-back frames share a single flag byte, saving +bandwidth. A frame is only processed if it meets the minimum size +requirement (5 bytes), so spurious flags produce harmless zero-length +"frames" that are discarded. -- **Sender** assigns sequential numbers to each DATA frame and retains - a copy in the retransmit buffer until acknowledged. -- **Receiver** delivers frames in order. Out-of-order frames trigger a - NAK for the expected sequence number. -- **ACK** carries the next expected sequence number (cumulative). -- **NAK** triggers retransmission of the requested frame and all - subsequent unacknowledged frames. -- **Timer-based retransmit** fires after 500 poll cycles if no ACK or - NAK has been received. ## API Reference ### Types ```c -// Receive callback -- called for each verified, in-order packet +// Receive callback -- called for each verified, in-order data packet typedef void (*PktRecvCallbackT)(void *ctx, const uint8_t *data, int len); // Opaque connection handle @@ -80,20 +194,21 @@ typedef struct PktConnS PktConnT; ### Constants -| Name | Value | Description | -|-----------------------|-------|-------------------------------------| -| `PKT_MAX_PAYLOAD` | 255 | Max payload bytes per packet | -| `PKT_DEFAULT_WINDOW` | 4 | Default sliding window size | -| `PKT_MAX_WINDOW` | 8 | Maximum sliding window size | -| `PKT_SUCCESS` | 0 | Success | -| `PKT_ERR_INVALID_PORT`| -1 | Invalid COM port | -| `PKT_ERR_NOT_OPEN` | -2 | Connection not open | -| `PKT_ERR_ALREADY_OPEN`| -3 | Connection already open | -| `PKT_ERR_WOULD_BLOCK` | -4 | Operation would block | -| `PKT_ERR_OVERFLOW` | -5 | Buffer overflow | -| `PKT_ERR_INVALID_PARAM`| -6 | Invalid parameter | -| `PKT_ERR_TX_FULL` | -7 | Transmit window full | -| `PKT_ERR_NO_DATA` | -8 | No data available | +| Name | Value | Description | +|------------------------|-------|--------------------------------------| +| `PKT_MAX_PAYLOAD` | 255 | Maximum payload bytes per packet | +| `PKT_DEFAULT_WINDOW` | 4 | Default sliding window size | +| `PKT_MAX_WINDOW` | 8 | Maximum sliding window size | +| `PKT_SUCCESS` | 0 | Success | +| `PKT_ERR_INVALID_PORT` | -1 | Invalid COM port | +| `PKT_ERR_NOT_OPEN` | -2 | Connection not open | +| `PKT_ERR_ALREADY_OPEN` | -3 | Connection already open | +| `PKT_ERR_WOULD_BLOCK` | -4 | Operation would block | +| `PKT_ERR_OVERFLOW` | -5 | Buffer overflow | +| `PKT_ERR_INVALID_PARAM`| -6 | Invalid parameter | +| `PKT_ERR_TX_FULL` | -7 | Transmit window full (non-blocking) | +| `PKT_ERR_NO_DATA` | -8 | No data available | +| `PKT_ERR_DISCONNECTED` | -9 | Serial port disconnected or error | ### Functions @@ -106,12 +221,14 @@ PktConnT *pktOpen(int com, int windowSize, Creates a packetized connection over an already-open COM port. -- `com` -- RS232 port index (`RS232_COM1`..`RS232_COM4`) -- `windowSize` -- sliding window size (1-8), 0 for default (4) -- `callback` -- called from `pktPoll()` for each received packet -- `callbackCtx` -- user pointer passed to callback +- `com` -- RS232 port index (`RS232_COM1` through `RS232_COM4`) +- `windowSize` -- sliding window size (1-8), or 0 for the default (4) +- `callback` -- called from `pktPoll()` for each received, verified, + in-order data packet. The `data` pointer is valid only during the + callback. +- `callbackCtx` -- user pointer passed through to the callback -Returns a connection handle, or `NULL` on failure. +Returns a connection handle, or `NULL` on failure (allocation error). #### pktClose @@ -120,6 +237,7 @@ void pktClose(PktConnT *conn); ``` Frees the connection state. Does **not** close the underlying COM port. +The caller is responsible for calling `rs232Close()` separately. #### pktSend @@ -127,12 +245,17 @@ Frees the connection state. Does **not** close the underlying COM port. int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block); ``` -Sends a packet. `len` must be 1..`PKT_MAX_PAYLOAD`. +Sends a data packet. `len` must be in the range 1 to `PKT_MAX_PAYLOAD` +(255). The data is copied into a retransmit slot before transmission, so +the caller can reuse its buffer immediately. -- `block = true` -- waits for window space, polling for ACKs internally -- `block = false` -- returns `PKT_ERR_TX_FULL` if the window is full +- `block = true` -- If the transmit window is full, polls internally + (calling `pktPoll()` in a tight loop) until an ACK frees a slot. Returns + `PKT_ERR_DISCONNECTED` if the serial port drops during the wait. +- `block = false` -- Returns `PKT_ERR_TX_FULL` immediately if the window + is full. -The packet is stored in the retransmit buffer until acknowledged. +Returns `PKT_SUCCESS` on success. #### pktPoll @@ -140,11 +263,21 @@ The packet is stored in the retransmit buffer until acknowledged. int pktPoll(PktConnT *conn); ``` -Reads available serial data, processes received frames, sends ACKs and -NAKs, and checks retransmit timers. Returns the number of DATA packets -delivered to the callback. +The main work function. Must be called frequently (every iteration of +your main loop or event loop). It performs three tasks: -Must be called frequently (e.g. in your main loop). +1. **Drain serial RX** -- reads all available bytes from the rs232 port + and feeds them through the HDLC deframing state machine +2. **Process frames** -- verifies CRC, handles DATA/ACK/NAK/RST frames, + delivers data packets to the callback +3. **Check retransmit timers** -- resends any slots that have timed out + +Returns the number of DATA packets delivered to the callback this call, +or `PKT_ERR_DISCONNECTED` if the serial port returned an error, or +`PKT_ERR_INVALID_PARAM` if `conn` is NULL. + +The callback is invoked synchronously, so the caller should be prepared +for re-entrant calls to `pktSend()` from within the callback. #### pktReset @@ -152,8 +285,19 @@ Must be called frequently (e.g. in your main loop). int pktReset(PktConnT *conn); ``` -Resets all sequence numbers and buffers to zero. Sends a RST frame to -the remote side so it resets as well. +Resets all sequence numbers, TX slots, and RX state to zero. Sends a RST +frame to the remote side so it resets as well. Useful for recovering from +a desynchronized state. + +#### pktCanSend + +```c +bool pktCanSend(PktConnT *conn); +``` + +Returns `true` if there is room in the transmit window for another +packet. Useful for non-blocking send loops to avoid calling `pktSend()` +when it would return `PKT_ERR_TX_FULL`. #### pktGetPending @@ -162,32 +306,37 @@ int pktGetPending(PktConnT *conn); ``` Returns the number of unacknowledged packets currently in the transmit -window. Useful for throttling sends in non-blocking mode. +window. Ranges from 0 (all sent packets acknowledged) to `windowSize` +(window full). Useful for throttling sends and monitoring link health. -## Example + +## Usage Example ```c #include "packet.h" #include "../rs232/rs232.h" void onPacket(void *ctx, const uint8_t *data, int len) { - // process received packet + // process received packet -- data is valid only during this callback } int main(void) { - // Open serial port first + // Open serial port first (packet layer does not manage it) rs232Open(RS232_COM1, 115200, 8, 'N', 1, RS232_HANDSHAKE_NONE); - // Create packet connection with default window size + // Create packet connection with default window size (4) PktConnT *conn = pktOpen(RS232_COM1, 0, onPacket, NULL); - // Send a packet (blocking) + // Send a packet (blocking -- waits for window space if needed) uint8_t msg[] = "Hello, packets!"; pktSend(conn, msg, sizeof(msg), true); - // Main loop + // Main loop -- must call pktPoll() frequently while (1) { int delivered = pktPoll(conn); + if (delivered == PKT_ERR_DISCONNECTED) { + break; + } // delivered = number of packets received this iteration } @@ -197,11 +346,55 @@ int main(void) { } ``` -## CRC +### Non-Blocking Send Pattern + +```c +// Send as fast as the window allows, doing other work between sends +while (bytesLeft > 0) { + pktPoll(conn); // process ACKs, free window slots + + if (pktCanSend(conn)) { + int chunk = bytesLeft; + if (chunk > PKT_MAX_PAYLOAD) { + chunk = PKT_MAX_PAYLOAD; + } + + if (pktSend(conn, data + offset, chunk, false) == PKT_SUCCESS) { + offset += chunk; + bytesLeft -= chunk; + } + } + + // do other work here (update UI, check for cancel, etc.) +} + +// Drain remaining ACKs +while (pktGetPending(conn) > 0) { + pktPoll(conn); +} +``` + + +## Internal Data Structures + +### Connection State (PktConnT) + +The connection handle contains: + +- **COM port index** and **window size** (configuration) +- **Callback** function pointer and context +- **TX state**: next sequence to assign, oldest unacked sequence, array + of retransmit slots, count of slots in use +- **RX state**: next expected sequence, deframing state machine state, + frame accumulation buffer + +### Retransmit Slots (TxSlotT) + +Each slot holds a copy of the sent payload, its sequence number, payload +length, and a `clock_t` timestamp of when it was last transmitted. The +retransmit timer compares this timestamp against the current time to +detect timeout. -CRC-16-CCITT (polynomial 0x1021, init 0xFFFF) computed via a 256-entry -lookup table (512 bytes). The CRC covers the SEQ, TYPE, LEN, and -payload fields. ## Building @@ -210,6 +403,26 @@ make # builds ../lib/libpacket.a make clean # removes objects and library ``` -Requires `librs232.a` at link time. +Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler +flags: `-O2 -Wall -Wextra -march=i486 -mtune=i586`. -Target: DJGPP cross-compiler, 486+ CPU. +Objects are placed in `../obj/packet/`, the library in `../lib/`. + +Requires `librs232.a` at link time (for `rs232Read()` and `rs232Write()`). + + +## Files + +- `packet.h` -- Public API header (types, constants, function prototypes) +- `packet.c` -- Complete implementation (framing, CRC, ARQ, state machine) +- `Makefile` -- DJGPP cross-compilation build rules + + +## Dependencies + +- `rs232/` -- Serial port I/O (must be linked: `-lrs232`) + +## Used By + +- `seclink/` -- Secure serial link (adds channel multiplexing and encryption) +- `proxy/` -- Linux serial proxy (uses a socket-based adaptation) diff --git a/proxy/README.md b/proxy/README.md index 3c6df3c..a43e9e4 100644 --- a/proxy/README.md +++ b/proxy/README.md @@ -1,16 +1,20 @@ -# SecLink Proxy +# SecLink Proxy -- Linux Serial Bridge Linux-hosted proxy that bridges an 86Box emulated serial port to a -remote telnet BBS. The 86Box side communicates using the secLink -protocol (packet framing, DH key exchange, XTEA encryption). The BBS -side is plain telnet over TCP. +remote telnet BBS. Part of the DVX GUI project. + +The 86Box side communicates using the SecLink protocol (HDLC packet +framing, CRC-16, DH key exchange, XTEA-CTR encryption). The BBS side +is plain telnet over TCP. All crypto is transparent to the BBS -- it +sees a normal telnet client. + ## Architecture ``` 86Box (DOS terminal) Remote BBS | | - emulated modem telnet:23 + emulated modem telnet | | TCP:2323 TCP:23 | | @@ -20,10 +24,11 @@ side is plain telnet over TCP. ``` The proxy accepts a single TCP connection from 86Box, performs the -secLink handshake (DH key exchange), then connects to the BBS. All -traffic between 86Box and the proxy is encrypted via XTEA-CTR on -channel 0. Traffic between the proxy and the BBS is unencrypted -telnet. +SecLink handshake (Diffie-Hellman key exchange), then connects to the +BBS. All traffic between 86Box and the proxy is encrypted via XTEA-CTR +on channel 0. Traffic between the proxy and the BBS is unencrypted +telnet with IAC negotiation handling. + ## Usage @@ -31,11 +36,13 @@ telnet. secproxy [listen_port] [bbs_host] [bbs_port] ``` -| Argument | Default | Description | -|---------------|------------------------|---------------------------------| -| `listen_port` | 2323 | TCP port for 86Box connection | -| `bbs_host` | bbs.duensing.digital | BBS hostname | -| `bbs_port` | 23 | BBS TCP port | +| Argument | Default | Description | +|---------------|--------------|---------------------------------| +| `listen_port` | 2323 | TCP port for 86Box connection | +| `bbs_host` | 10.1.0.244 | BBS hostname or IP | +| `bbs_port` | 2023 | BBS TCP port | + +Examples: ``` secproxy # all defaults @@ -44,15 +51,20 @@ secproxy 2323 bbs.example.com 23 # different BBS secproxy --help # show usage ``` + ## Startup Sequence -1. Listen on the configured TCP port -2. Wait for 86Box to connect (blocks on accept) -3. Connect to the remote BBS -4. Seed the RNG from `/dev/urandom` -5. Open secLink and perform the DH handshake (blocks until the DOS - side completes its handshake) -6. Enter the proxy loop +1. Listen on the configured TCP port. +2. Wait for 86Box to connect (polls with Ctrl+C support). +3. Map the TCP socket to COM0 via the socket shim. +4. Seed the RNG from `/dev/urandom`. +5. Open SecLink and perform the DH handshake (blocks until the DOS + side completes its handshake). +6. Wait for the terminal to send ENTER (so the user can confirm the + connection is working before BBS output starts). +7. Connect to the remote BBS. +8. Enter the proxy loop. + ## Proxy Loop @@ -62,71 +74,110 @@ the two TCP connections: - **86Box -> BBS**: `secLinkPoll()` reads from the 86Box socket via the socket shim, decrypts incoming packets, and the receive callback writes plaintext to the BBS socket. -- **BBS -> 86Box**: `read()` from the BBS socket, then - `secLinkSend()` encrypts and sends to 86Box via the socket shim. -- **Maintenance**: `secLinkPoll()` also handles packet-layer retransmit - timers on every iteration. +- **BBS -> 86Box**: `read()` from the BBS socket, then the telnet + filter strips IAC sequences, then `secLinkSend()` encrypts and sends + to 86Box via the socket shim. If the send window is full, the loop + retries with ACK processing until the data goes through. +- **Maintenance**: `secLinkPoll()` also handles packet-layer + retransmit timers on every iteration. The proxy exits cleanly on Ctrl+C (SIGINT), SIGTERM, or when either side disconnects. -## 86Box Configuration -Configure the 86Box serial port to use a telnet connection: +## Telnet Negotiation + +The proxy handles RFC 854 telnet IAC sequences from the BBS so they +do not corrupt the terminal display. A state machine parser strips +IAC sequences from the data stream, persisting state across TCP +segment boundaries. + +Accepted options: + +- ECHO (option 1) -- server echoes characters +- SGA (option 3) -- suppress go-ahead for character-at-a-time mode +- TTYPE (option 24) -- terminal type negotiation +- NAWS (option 31) -- window size negotiation + +All other options are refused. Subnegotiations (SB...SE) are consumed +silently. -1. In 86Box settings, set a COM port to "TCP (server)" or - "TCP (client)" mode pointing at the proxy's listen port -2. Enable "No telnet negotiation" to send raw bytes -3. The DOS terminal application running inside 86Box uses secLink - over this serial port ## Socket Shim -The proxy reuses the same packet, security, and secLink source code -as the DOS build. A socket shim (`sockShim.h`/`sockShim.c`) provides -rs232-compatible `rs232Read()`/`rs232Write()` functions backed by TCP -sockets instead of UART hardware: +The proxy reuses the same packet, security, and secLink source code as +the DOS build. A socket shim (`sockShim.h` / `sockShim.c`) provides +rs232-compatible functions backed by TCP sockets instead of UART +hardware: | rs232 function | Socket shim behavior | |----------------|-----------------------------------------------| -| `rs232Open()` | No-op (socket already connected) | -| `rs232Close()` | Marks port closed (socket managed by caller) | +| `rs232Open()` | Validates socket assigned; ignores serial params | +| `rs232Close()` | Marks port closed (socket lifecycle is caller's) | | `rs232Read()` | Non-blocking `recv()` with `MSG_DONTWAIT` | | `rs232Write()` | Blocking `send()` loop with `MSG_NOSIGNAL` | The shim maps COM port indices (0-3) to socket file descriptors via -`sockShimSetFd()`, which must be called before opening the secLink -layer. +`sockShimSetFd()`, which must be called before opening the SecLink +layer. Up to 4 ports are supported. + +The Makefile uses `-include sockShim.h` when compiling the packet and +secLink layers, which defines `RS232_H` to prevent the real `rs232.h` +from being included. + + +## DJGPP Stubs + +DOS-specific headers required by the security library are replaced by +minimal stubs in `stubs/`: + +| Stub | Replaces DJGPP header | Contents | +|-------------------|------------------------|-------------------| +| `stubs/pc.h` | `` | No-op definitions | +| `stubs/go32.h` | `` | No-op definitions | +| `stubs/sys/farptr.h` | `` | No-op definitions | + +The security library's hardware entropy function returns zeros on +Linux, which is harmless since the proxy seeds the RNG from +`/dev/urandom` before the handshake. + + +## 86Box Configuration + +Configure the 86Box serial port to connect to the proxy: + +1. In 86Box settings, set a COM port to TCP client mode pointing at + the proxy's listen port (default 2323). +2. Enable "No telnet negotiation" to send raw bytes. +3. The DOS terminal application running inside 86Box uses SecLink + over this serial port. -DOS-specific headers (``, ``, ``) are -replaced by minimal stubs in `stubs/` that provide no-op -implementations. The security library's hardware entropy function -returns zeros on Linux, which is harmless since the proxy seeds the -RNG from `/dev/urandom` before the handshake. ## Building ``` -make # builds ../bin/secproxy -make clean # removes objects and binary +make # builds ../bin/secproxy +make clean # removes objects and binary ``` Objects are placed in `../obj/proxy/`, the binary in `../bin/`. Requires only a standard Linux C toolchain (gcc, libc). No external -dependencies. +dependencies beyond the project's own packet, security, and secLink +source files, which are compiled directly from their source directories. + ## Files ``` proxy/ - proxy.c main proxy program - sockShim.h rs232-compatible socket API (header) - sockShim.c socket shim implementation - Makefile Linux build + proxy.c main proxy program + sockShim.h rs232-compatible socket API (header) + sockShim.c socket shim implementation + Makefile Linux native build stubs/ - pc.h stub for DJGPP - go32.h stub for DJGPP + pc.h stub for DJGPP + go32.h stub for DJGPP sys/ - farptr.h stub for DJGPP + farptr.h stub for DJGPP ``` diff --git a/rs232/README.md b/rs232/README.md index 1a95f04..19ec4be 100644 --- a/rs232/README.md +++ b/rs232/README.md @@ -1,136 +1,277 @@ -# RS232 -- Serial Port Library for DJGPP +# RS232 -- ISR-Driven Serial Port Library for DJGPP -ISR-driven UART communication library supporting up to 4 simultaneous -COM ports with ring buffers and hardware/software flow control. +Interrupt-driven UART communication library supporting up to 4 simultaneous +COM ports with ring buffers and hardware/software flow control. Targets +486-class DOS hardware running under DJGPP/DPMI. Ported from the DOS Serial Library 1.4 by Karl Stenerud (MIT License), -stripped to DJGPP-only codepaths and restyled. +stripped to DJGPP-only codepaths and restyled for the DVX project. -## Features -- ISR-driven receive and transmit with 2048-byte ring buffers -- Auto-detected IRQ from BIOS data area -- UART type detection (8250, 16450, 16550, 16550A) -- 16550 FIFO detection and configurable trigger threshold -- XON/XOFF, RTS/CTS, and DTR/DSR flow control -- DPMI memory locking for ISR safety -- Speeds from 50 to 115200 bps -- 5-8 data bits, N/O/E/M/S parity, 1-2 stop bits +## Architecture + +The library is built around a single shared ISR (`comGeneralIsr`) that +services all open COM ports. This design is necessary because COM1/COM3 +typically share IRQ4 and COM2/COM4 share IRQ3 -- a single handler that +polls all ports avoids the complexity of per-IRQ dispatch. + +``` +Application + | + | rs232Read() non-blocking drain from RX ring buffer + | rs232Write() blocking polled write directly to UART THR + | rs232WriteBuf() non-blocking write into TX ring buffer + | +[Ring Buffers] 2048-byte RX + TX per port, power-of-2 bitmask indexing + | +[ISR] comGeneralIsr -- shared handler for all open ports + | +[UART] 8250 / 16450 / 16550 / 16550A hardware +``` + +### ISR Design + +The ISR follows a careful protocol to remain safe under DPMI while +keeping the system responsive: + +1. **Mask** all COM port IRQs on the PIC to prevent ISR re-entry +2. **STI** to allow higher-priority interrupts (timer tick, keyboard) through +3. **Loop** over all open ports, draining each UART's pending interrupt + conditions (data ready, TX hold empty, modem status, line status) +4. **CLI**, send EOI to the PIC, re-enable COM IRQs, **STI** before IRET + +This mask-then-STI pattern is standard for slow device ISRs on PC +hardware. It prevents the same IRQ from re-entering while allowing the +system timer and keyboard to function during UART processing. + +### Ring Buffers + +Both RX and TX buffers are 2048 bytes, sized as a power of 2 so that +head/tail wraparound is a single AND operation (bitmask indexing) rather +than an expensive modulo -- critical for ISR-speed code on a 486. + +The buffers use a one-slot-wasted design to distinguish full from empty: +`head == tail` means empty, `(head + 1) & MASK == tail` means full. + +### Flow Control + +Flow control operates entirely within the ISR using watermark thresholds. +When the RX buffer crosses 80% full, the ISR signals the remote side to +stop sending; when it drops below 20%, the ISR allows the remote to +resume. This prevents buffer overflow without any application involvement. + +Three modes are supported: + +| Mode | Stop Signal | Resume Signal | +|------------|-------------------|-------------------| +| XON/XOFF | Send XOFF (0x13) | Send XON (0x11) | +| RTS/CTS | Deassert RTS | Assert RTS | +| DTR/DSR | Deassert DTR | Assert DTR | + +On the TX side, the ISR monitors incoming XON/XOFF bytes and the CTS/DSR +modem status lines to pause and resume transmission from the TX ring +buffer. + +### DPMI Memory Locking + +The ISR code and all per-port state structures (`sComPorts` array) are +locked in physical memory via `__dpmi_lock_linear_region`. This prevents +page faults during interrupt handling -- a hard requirement for any ISR +running under a DPMI host (DOS extender, Windows 3.x, OS/2 VDM, etc.). +An IRET wrapper is allocated by DPMI to handle the real-mode to +protected-mode transition on hardware interrupt entry. + + +## UART Type Detection + +`rs232GetUartType()` probes the UART hardware to identify the chip: + +1. **Scratch register test** -- Writes two known values (0xAA, 0x55) to + UART register 7 and reads them back. The 8250 lacks this register, so + readback fails. If both values read back correctly, the UART is at + least a 16450. + +2. **FIFO test** -- Enables the FIFO via the FCR (FIFO Control Register), + then reads bits 7:6 of the IIR (Interrupt Identification Register): + - `0b11` = 16550A (working 16-byte FIFO) + - `0b10` = 16550 (broken FIFO -- present in hardware but unusable) + - `0b00` = 16450 (no FIFO at all) + + The original FCR value is restored after probing. + +| Constant | Value | Description | +|---------------------|-------|----------------------------------------| +| `RS232_UART_UNKNOWN`| 0 | Unknown or undetected | +| `RS232_UART_8250` | 1 | Original IBM PC -- no FIFO, no scratch | +| `RS232_UART_16450` | 2 | Scratch register present, no FIFO | +| `RS232_UART_16550` | 3 | Broken FIFO (rare, unusable) | +| `RS232_UART_16550A` | 4 | Working 16-byte FIFO (most common) | + +On 16550A UARTs, the FIFO trigger threshold is configurable via +`rs232SetFifoThreshold()` with levels of 1, 4, 8, or 14 bytes. The +default is 14, which minimizes interrupt overhead at high baud rates. + + +## IRQ Auto-Detection + +When `rs232Open()` is called without a prior `rs232SetIrq()` override, +the library auto-detects the UART's IRQ by: + +1. Saving the current PIC interrupt mask registers (IMR) +2. Enabling all IRQ lines on both PICs +3. Generating a TX Hold Empty interrupt on the UART +4. Reading the PIC's Interrupt Request Register (IRR) to see which line + went high +5. Disabling the interrupt, reading IRR again to mask out persistent bits +6. Re-enabling once more to confirm the detection +7. Restoring the original PIC mask + +If auto-detection fails (common on virtualized hardware that does not +model the IRR accurately), the library falls back to the default IRQ for +the port (IRQ4 for COM1/COM3, IRQ3 for COM2/COM4). + +The BIOS Data Area (at segment 0x0040) is read to determine each port's +I/O base address. Ports not configured in the BDA are unavailable. + + +## COM Port Support + +| Constant | Value | Default IRQ | Default Base | +|--------------|-------|-------------|--------------| +| `RS232_COM1` | 0 | IRQ 4 | 0x3F8 | +| `RS232_COM2` | 1 | IRQ 3 | 0x2F8 | +| `RS232_COM3` | 2 | IRQ 4 | 0x3E8 | +| `RS232_COM4` | 3 | IRQ 3 | 0x2E8 | + +Base addresses are read from the BIOS Data Area at runtime. The default +IRQ values are used only as a fallback when auto-detection fails. Both +the base address and IRQ can be overridden before opening with +`rs232SetBase()` and `rs232SetIrq()`. + + +## Supported Baud Rates + +All standard rates from 50 to 115200 bps are supported. The baud rate +divisor is computed from the standard 1.8432 MHz UART crystal: + +| Rate | Divisor | Rate | Divisor | +|--------|---------|--------|---------| +| 50 | 2304 | 4800 | 24 | +| 75 | 1536 | 7200 | 16 | +| 110 | 1047 | 9600 | 12 | +| 150 | 768 | 19200 | 6 | +| 300 | 384 | 38400 | 3 | +| 600 | 192 | 57600 | 2 | +| 1200 | 96 | 115200 | 1 | +| 1800 | 64 | | | +| 2400 | 48 | | | +| 3800 | 32 | | | + +Data bits (5-8), parity (N/O/E/M/S), and stop bits (1-2) are configured +by writing the appropriate LCR (Line Control Register) bits. + ## API Reference -### Types +### Error Codes -All functions take a COM port index (`int com`) as their first argument: - -| Constant | Value | Description | -|--------------|-------|-------------| -| `RS232_COM1` | 0 | COM1 | -| `RS232_COM2` | 1 | COM2 | -| `RS232_COM3` | 2 | COM3 | -| `RS232_COM4` | 3 | COM4 | - -### UART Types - -| Constant | Value | Description | -|---------------------|-------|--------------------------------------| -| `RS232_UART_UNKNOWN`| 0 | Unknown or undetected | -| `RS232_UART_8250` | 1 | 8250 -- no FIFO, no scratch register | -| `RS232_UART_16450` | 2 | 16450 -- scratch register, no FIFO | -| `RS232_UART_16550` | 3 | 16550 -- broken FIFO (unusable) | -| `RS232_UART_16550A` | 4 | 16550A -- working 16-byte FIFO | +| Constant | Value | Description | +|-------------------------------|-------|--------------------------| +| `RS232_SUCCESS` | 0 | Success | +| `RS232_ERR_UNKNOWN` | -1 | Unknown error | +| `RS232_ERR_NOT_OPEN` | -2 | Port not open | +| `RS232_ERR_ALREADY_OPEN` | -3 | Port already open | +| `RS232_ERR_NO_UART` | -4 | No UART detected at base | +| `RS232_ERR_INVALID_PORT` | -5 | Bad port index | +| `RS232_ERR_INVALID_BASE` | -6 | Bad I/O base address | +| `RS232_ERR_INVALID_IRQ` | -7 | Bad IRQ number | +| `RS232_ERR_INVALID_BPS` | -8 | Unsupported baud rate | +| `RS232_ERR_INVALID_DATA` | -9 | Bad data bits (not 5-8) | +| `RS232_ERR_INVALID_PARITY` | -10 | Bad parity character | +| `RS232_ERR_INVALID_STOP` | -11 | Bad stop bits (not 1-2) | +| `RS232_ERR_INVALID_HANDSHAKE` | -12 | Bad handshaking mode | +| `RS232_ERR_INVALID_FIFO` | -13 | Bad FIFO threshold | +| `RS232_ERR_NULL_PTR` | -14 | NULL pointer argument | +| `RS232_ERR_IRQ_NOT_FOUND` | -15 | Could not detect IRQ | +| `RS232_ERR_LOCK_MEM` | -16 | DPMI memory lock failed | ### Handshaking Modes -| Constant | Value | Description | -|--------------------------|-------|---------------------------| -| `RS232_HANDSHAKE_NONE` | 0 | No flow control | -| `RS232_HANDSHAKE_XONXOFF`| 1 | Software (XON/XOFF) | -| `RS232_HANDSHAKE_RTSCTS` | 2 | Hardware (RTS/CTS) | -| `RS232_HANDSHAKE_DTRDSR` | 3 | Hardware (DTR/DSR) | +| Constant | Value | Description | +|---------------------------|-------|----------------------| +| `RS232_HANDSHAKE_NONE` | 0 | No flow control | +| `RS232_HANDSHAKE_XONXOFF` | 1 | Software (XON/XOFF) | +| `RS232_HANDSHAKE_RTSCTS` | 2 | Hardware (RTS/CTS) | +| `RS232_HANDSHAKE_DTRDSR` | 3 | Hardware (DTR/DSR) | -### Error Codes - -| Constant | Value | Description | -|-------------------------------|-------|---------------------------| -| `RS232_SUCCESS` | 0 | Success | -| `RS232_ERR_UNKNOWN` | -1 | Unknown error | -| `RS232_ERR_NOT_OPEN` | -2 | Port not open | -| `RS232_ERR_ALREADY_OPEN` | -3 | Port already open | -| `RS232_ERR_NO_UART` | -4 | No UART detected | -| `RS232_ERR_INVALID_PORT` | -5 | Bad port index | -| `RS232_ERR_INVALID_BASE` | -6 | Bad I/O base address | -| `RS232_ERR_INVALID_IRQ` | -7 | Bad IRQ number | -| `RS232_ERR_INVALID_BPS` | -8 | Unsupported baud rate | -| `RS232_ERR_INVALID_DATA` | -9 | Bad data bits (not 5-8) | -| `RS232_ERR_INVALID_PARITY` | -10 | Bad parity character | -| `RS232_ERR_INVALID_STOP` | -11 | Bad stop bits (not 1-2) | -| `RS232_ERR_INVALID_HANDSHAKE` | -12 | Bad handshaking mode | -| `RS232_ERR_INVALID_FIFO` | -13 | Bad FIFO threshold | -| `RS232_ERR_NULL_PTR` | -14 | NULL pointer argument | -| `RS232_ERR_IRQ_NOT_FOUND` | -15 | Could not detect IRQ | -| `RS232_ERR_LOCK_MEM` | -16 | DPMI memory lock failed | - -### Functions - -#### Open / Close +### Open / Close ```c int rs232Open(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake); ``` -Opens a COM port. Detects the UART base address from the BIOS data -area, auto-detects the IRQ, installs the ISR, and configures the port. +Opens a COM port. Reads the UART base address from the BIOS data area, +auto-detects the IRQ, locks ISR memory via DPMI, installs the ISR, and +configures the UART for the specified parameters. Returns `RS232_SUCCESS` +or an error code. -- `bps` -- baud rate (50, 75, 110, 150, 300, 600, 1200, 1800, 2400, - 3800, 4800, 7200, 9600, 19200, 38400, 57600, 115200) -- `dataBits` -- 5, 6, 7, or 8 -- `parity` -- `'N'` (none), `'O'` (odd), `'E'` (even), `'M'` (mark), +- `com` -- port index (`RS232_COM1` through `RS232_COM4`) +- `bps` -- baud rate (50 through 115200) +- `dataBits` -- 5, 6, 7, or 8 +- `parity` -- `'N'` (none), `'O'` (odd), `'E'` (even), `'M'` (mark), `'S'` (space) -- `stopBits` -- 1 or 2 -- `handshake` -- `RS232_HANDSHAKE_*` constant +- `stopBits` -- 1 or 2 +- `handshake` -- `RS232_HANDSHAKE_*` constant ```c int rs232Close(int com); ``` -Closes the port, removes the ISR, and restores the original interrupt -vector. +Closes the port, disables UART interrupts, removes the ISR, restores the +original interrupt vector, and unlocks DPMI memory (when the last port +closes). -#### Read / Write +### Read / Write ```c int rs232Read(int com, char *data, int len); ``` -Reads up to `len` bytes from the receive buffer. Returns the number of -bytes actually read (0 if the buffer is empty). +Non-blocking read. Drains up to `len` bytes from the RX ring buffer. +Returns the number of bytes actually read (0 if the buffer is empty). +If flow control is active and the buffer drops below the low-water mark, +the ISR will re-enable receive from the remote side. ```c int rs232Write(int com, const char *data, int len); ``` -Blocking write. Sends `len` bytes, waiting for transmit buffer space -as needed. Returns `RS232_SUCCESS` or an error code. +Blocking polled write. Sends `len` bytes directly to the UART THR +(Transmit Holding Register), bypassing the TX ring buffer entirely. +Polls LSR for THR empty before each byte. Returns `RS232_SUCCESS` or +an error code. ```c int rs232WriteBuf(int com, const char *data, int len); ``` -Non-blocking write. Copies as many bytes as will fit into the transmit -buffer. Returns the number of bytes actually queued. +Non-blocking buffered write. Copies as many bytes as will fit into the +TX ring buffer. The ISR drains the TX buffer to the UART automatically. +Returns the number of bytes actually queued. If the buffer is full, some +bytes may be dropped. -#### Buffer Management +### Buffer Management ```c int rs232ClearRxBuffer(int com); int rs232ClearTxBuffer(int com); ``` -Discard all data in the receive or transmit ring buffer. +Discard all data in the receive or transmit ring buffer by resetting +head and tail pointers to zero. -#### Getters +### Getters ```c int rs232GetBase(int com); // UART I/O base address @@ -141,50 +282,46 @@ int rs232GetDsr(int com); // DSR line state (0 or 1) int rs232GetDtr(int com); // DTR line state (0 or 1) int rs232GetHandshake(int com); // Handshaking mode int rs232GetIrq(int com); // IRQ number -int rs232GetLsr(int com); // Line status register -int rs232GetMcr(int com); // Modem control register -int rs232GetMsr(int com); // Modem status register +int rs232GetLsr(int com); // Line Status Register +int rs232GetMcr(int com); // Modem Control Register +int rs232GetMsr(int com); // Modem Status Register char rs232GetParity(int com); // Parity setting ('N','O','E','M','S') int rs232GetRts(int com); // RTS line state (0 or 1) -int rs232GetRxBuffered(int com); // Bytes in receive buffer +int rs232GetRxBuffered(int com); // Bytes waiting in RX buffer int rs232GetStop(int com); // Stop bits setting -int rs232GetTxBuffered(int com); // Bytes in transmit buffer -int rs232GetUartType(int com); // UART type (RS232_UART_* constant) +int rs232GetTxBuffered(int com); // Bytes waiting in TX buffer +int rs232GetUartType(int com); // UART type (RS232_UART_* constant) ``` -`rs232GetUartType` probes the UART hardware to identify the chip: +Most getters return cached register values from the per-port state +structure, avoiding unnecessary I/O port reads. `rs232GetUartType()` +actively probes the hardware (see UART Type Detection above). -1. **Scratch register test** -- writes two values to register 7 and - reads them back. The 8250 lacks this register, so readback fails. -2. **FIFO test** -- enables the FIFO via the FCR, then reads IIR bits - 7:6. `0b11` = 16550A (working FIFO), `0b10` = 16550 (broken FIFO), - `0b00` = 16450 (no FIFO). The original FCR value is restored after - probing. - -#### Setters +### Setters ```c int rs232Set(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake); ``` -Reconfigure all port parameters at once (port must be open). +Reconfigure all port parameters at once. The port must already be open. ```c -int rs232SetBase(int com, int base); // Override I/O base address +int rs232SetBase(int com, int base); // Override I/O base (before open) int rs232SetBps(int com, int32_t bps); // Change baud rate int rs232SetData(int com, int dataBits); // Change data bits int rs232SetDtr(int com, bool dtr); // Assert/deassert DTR -int rs232SetFifoThreshold(int com, int thr); // FIFO trigger level (1,4,8,14) +int rs232SetFifoThreshold(int com, int thr); // FIFO trigger (1, 4, 8, 14) int rs232SetHandshake(int com, int handshake); // Change flow control mode -int rs232SetIrq(int com, int irq); // Override IRQ (before Open) -int rs232SetMcr(int com, int mcr); // Write modem control register +int rs232SetIrq(int com, int irq); // Override IRQ (before open) +int rs232SetMcr(int com, int mcr); // Write Modem Control Register int rs232SetParity(int com, char parity); // Change parity int rs232SetRts(int com, bool rts); // Assert/deassert RTS int rs232SetStop(int com, int stopBits); // Change stop bits ``` -## Example + +## Usage Example ```c #include "rs232.h" @@ -196,14 +333,19 @@ int main(void) { return 1; } - // Identify UART chip + // Identify the UART chip int uartType = rs232GetUartType(RS232_COM1); - // uartType == RS232_UART_16550A on most systems + // uartType == RS232_UART_16550A on most 486+ systems + + // Enable 16550A FIFO with trigger at 14 bytes + if (uartType == RS232_UART_16550A) { + rs232SetFifoThreshold(RS232_COM1, 14); + } // Blocking send rs232Write(RS232_COM1, "Hello\r\n", 7); - // Non-blocking receive + // Non-blocking receive loop char buf[128]; int n; while ((n = rs232Read(RS232_COM1, buf, sizeof(buf))) > 0) { @@ -215,17 +357,38 @@ int main(void) { } ``` + ## Implementation Notes -- The ISR handles all four COM ports from a single shared handler. - On entry it disables UART interrupts for all open ports, then - re-enables CPU interrupts so higher-priority devices are serviced. +- The single shared ISR handles all four COM ports. On entry it disables + UART interrupts for all open ports on the PIC, then re-enables CPU + interrupts (STI) so higher-priority devices (timer, keyboard) are + serviced promptly. + - Ring buffers use power-of-2 sizes (2048 bytes) with bitmask indexing - for zero-branch wraparound. -- Flow control watermarks are at 80% (assert) and 20% (deassert) of - buffer capacity. -- DPMI `__dpmi_lock_linear_region` is used to pin the ISR, ring - buffers, and port state in physical memory. + for zero-branch wraparound. Each port uses 4KB total (2KB RX + 2KB TX). + +- Flow control watermarks are at 80% (assert stop) and 20% (deassert + stop) of buffer capacity. These percentages are defined as compile-time + constants and apply to both RX and TX directions. + +- DPMI `__dpmi_lock_linear_region` is used to pin the ISR code, ring + buffers, and port state in physical memory. The ISR code region is + locked for 2048 bytes starting at the `comGeneralIsr` function address. + +- `rs232Write()` is a blocking polled write that bypasses the TX ring + buffer entirely. It writes directly to the UART THR register, polling + LSR for readiness between each byte. `rs232WriteBuf()` is the + non-blocking alternative that queues into the TX ring buffer for ISR + draining. + +- Per-port state is stored in a static array of `Rs232StateT` structures + (`sComPorts[4]`). This array is locked in physical memory alongside the + ISR code. + +- The BIOS Data Area (real-mode address 0040:0000) is read via DJGPP's + far pointer API (`_farpeekw`) to obtain port base addresses at runtime. + ## Building @@ -234,4 +397,21 @@ make # builds ../lib/librs232.a make clean # removes objects and library ``` -Target: DJGPP cross-compiler, 486+ CPU. +Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler +flags: `-O2 -Wall -Wextra -march=i486 -mtune=i586`. + +Objects are placed in `../obj/rs232/`, the library in `../lib/`. + + +## Files + +- `rs232.h` -- Public API header +- `rs232.c` -- Complete implementation (ISR, DPMI, ring buffers, UART I/O) +- `Makefile` -- DJGPP cross-compilation build rules + + +## Used By + +- `packet/` -- Packetized serial transport layer (HDLC framing, CRC, ARQ) +- `seclink/` -- Secure serial link (opens and closes the COM port) +- `proxy/` -- Linux serial proxy (uses a socket-based shim of this API) diff --git a/seclink/README.md b/seclink/README.md index 841b922..146cce4 100644 --- a/seclink/README.md +++ b/seclink/README.md @@ -1,76 +1,153 @@ -# SecLink -- Secure Serial Link Library +# SecLink -- Secure Serial Link Library -SecLink is a convenience wrapper that ties together three lower-level -libraries into a single API for reliable, optionally encrypted serial -communication: +SecLink is the top-level API for the DVX serial/networking stack. It +composes three lower-level libraries into a single interface for reliable, +optionally encrypted, channel-multiplexed serial communication: + +- **rs232** -- ISR-driven UART I/O with ring buffers and flow control +- **packet** -- HDLC framing, CRC-16, Go-Back-N sliding window ARQ +- **security** -- 1024-bit Diffie-Hellman key exchange, XTEA-CTR encryption + +SecLink adds channel multiplexing and per-packet encryption control on +top of the packet layer's reliable delivery. -- **rs232** -- ISR-driven UART I/O with ring buffers and flow control -- **packet** -- HDLC-style framing with CRC-16 and sliding window reliability -- **security** -- 1024-bit Diffie-Hellman key exchange and XTEA-CTR encryption ## Architecture ``` - Application - | - [secLink] channels, optional encryption - | - [packet] framing, CRC, retransmit, ordering - | - [rs232] ISR-driven UART, ring buffers, flow control - | - UART +Application + | + | secLinkSend() send data on a channel, optionally encrypted + | secLinkPoll() receive, decrypt, deliver to callback + | secLinkHandshake() DH key exchange (blocking) + | +[SecLink] channel header, encrypt/decrypt, key management + | +[Packet] HDLC framing, CRC-16, Go-Back-N ARQ + | +[RS232] ISR-driven UART, 2048-byte ring buffers + | +UART Hardware ``` -SecLink adds a one-byte header to every packet: +### Channel Multiplexing + +SecLink prepends a one-byte header to every packet's payload before +handing it to the packet layer: ``` - Bit 7 Bits 6..0 - ----- --------- - Encrypt Channel (0-127) + Bit 7 Bits 6..0 + ----- --------- + Encrypt Channel (0-127) ``` -This allows mixing encrypted and cleartext traffic on up to 128 -independent logical channels over a single serial link. +This allows up to 128 independent logical channels over a single serial +link. Each channel can carry a different type of traffic (terminal data, +file transfer, control messages, etc.) without needing separate framing +or sequencing per stream. The receive callback includes the channel +number so the application can dispatch accordingly. + +The encrypt flag (bit 7) tells the receiver whether the payload portion +of this packet is encrypted. The channel header byte itself is always +sent in the clear. + +### Mixed Clear and Encrypted Traffic + +Unencrypted packets can be sent before or after the DH handshake. This +enables a startup protocol (version negotiation, capability exchange) +before keys are established. Encrypted packets require a completed +handshake -- attempting to send an encrypted packet before the handshake +returns `SECLINK_ERR_NOT_READY`. + +On the receive side, encrypted packets arriving before the handshake is +complete are silently dropped. Cleartext packets are delivered regardless +of handshake state. + ## Lifecycle ``` secLinkOpen() Open COM port and packet layer -secLinkHandshake() DH key exchange (blocks until complete) -secLinkSend() Send a packet (encrypted or clear) +secLinkHandshake() DH key exchange (blocks until both sides complete) +secLinkSend() Send data on a channel (encrypted or cleartext) secLinkPoll() Receive and deliver packets to callback -secLinkClose() Tear down everything +secLinkClose() Tear down everything (ciphers, packet, COM port) ``` -The handshake is only required if you intend to send encrypted packets. -Cleartext packets can be sent immediately after `secLinkOpen()`. +### Handshake Protocol + +The DH key exchange uses the packet layer's reliable delivery, so lost +packets are automatically retransmitted. Both sides can send their public +key simultaneously -- there is no initiator/responder distinction. + +1. Both sides generate a DH keypair (256-bit private, 1024-bit public) +2. Both sides send their 128-byte public key as a single packet +3. On receiving the remote's public key, each side immediately computes + the shared secret (`remote^private mod p`) +4. Each side derives separate TX and RX cipher keys from the master key +5. Cipher contexts are created and the link transitions to READY state +6. The DH context (containing the private key) is destroyed immediately + +**Directional key derivation:** + +The side with the lexicographically lower public key uses +`masterKey XOR 0xAA` for TX and `masterKey XOR 0x55` for RX. The other +side uses the reverse assignment. This is critical for CTR mode security: +if both sides used the same key and counter, they would produce identical +keystreams, and XORing two ciphertexts would reveal the XOR of the +plaintexts. The XOR-derived directional keys ensure each direction has a +unique keystream even though both sides start their counters at zero. + +**Forward secrecy:** + +The DH context (containing the private key and shared secret) is +destroyed immediately after deriving the session cipher keys. Even if +the application's long-term state is compromised later, past session +keys cannot be recovered from memory. + + +## Payload Size + +The maximum payload per `secLinkSend()` call is `SECLINK_MAX_PAYLOAD` +(254 bytes). This is the packet layer's 255-byte maximum minus the +1-byte channel header that SecLink prepends. + +For sending data larger than 254 bytes, use `secLinkSendBuf()` which +automatically splits the data into 254-byte chunks and sends each one +with blocking delivery. + ## API Reference ### Types ```c -// Receive callback -- called for each incoming packet with plaintext -typedef void (*SecLinkRecvT)(void *ctx, const uint8_t *data, int len, uint8_t channel); +// Receive callback -- delivers plaintext with channel number +typedef void (*SecLinkRecvT)(void *ctx, const uint8_t *data, int len, + uint8_t channel); // Opaque connection handle typedef struct SecLinkS SecLinkT; ``` +The receive callback is invoked from `secLinkPoll()` for each incoming +packet. Encrypted packets are decrypted before delivery -- the callback +always receives plaintext regardless of whether encryption was used on +the wire. The `data` pointer is valid only during the callback. + ### Constants -| Name | Value | Description | -|-------------------------|-------|----------------------------------------| -| `SECLINK_MAX_PAYLOAD` | 254 | Max bytes per `secLinkSend()` call | -| `SECLINK_MAX_CHANNEL` | 127 | Highest valid channel number | -| `SECLINK_SUCCESS` | 0 | Operation succeeded | -| `SECLINK_ERR_PARAM` | -1 | Invalid parameter | -| `SECLINK_ERR_SERIAL` | -2 | Serial port error | -| `SECLINK_ERR_ALLOC` | -3 | Memory allocation failed | -| `SECLINK_ERR_HANDSHAKE` | -4 | Key exchange failed | -| `SECLINK_ERR_NOT_READY` | -5 | Encryption requested before handshake | -| `SECLINK_ERR_SEND` | -6 | Packet layer send failed | +| Name | Value | Description | +|-------------------------|-------|---------------------------------------| +| `SECLINK_MAX_PAYLOAD` | 254 | Max bytes per `secLinkSend()` call | +| `SECLINK_MAX_CHANNEL` | 127 | Highest valid channel number | +| `SECLINK_SUCCESS` | 0 | Operation succeeded | +| `SECLINK_ERR_PARAM` | -1 | Invalid parameter or NULL pointer | +| `SECLINK_ERR_SERIAL` | -2 | Serial port open failed | +| `SECLINK_ERR_ALLOC` | -3 | Memory allocation failed | +| `SECLINK_ERR_HANDSHAKE` | -4 | DH key exchange failed | +| `SECLINK_ERR_NOT_READY` | -5 | Encryption requested before handshake | +| `SECLINK_ERR_SEND` | -6 | Packet layer send failed or window full | ### Functions @@ -82,9 +159,22 @@ SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity, SecLinkRecvT callback, void *ctx); ``` -Opens the COM port via rs232, creates the packet layer, and returns a -link handle. Returns `NULL` on failure. The callback is invoked from -`secLinkPoll()` for each received packet. +Opens the COM port via rs232, creates the packet layer with default +window size (4), and returns a link handle. The callback is invoked from +`secLinkPoll()` for each received packet (decrypted if applicable). + +Returns `NULL` on failure (serial port error, packet layer allocation +error, or memory allocation failure). On failure, all partially +initialized resources are cleaned up. + +- `com` -- RS232 port index (`RS232_COM1` through `RS232_COM4`) +- `bps` -- baud rate (50 through 115200) +- `dataBits` -- 5, 6, 7, or 8 +- `parity` -- `'N'`, `'O'`, `'E'`, `'M'`, or `'S'` +- `stopBits` -- 1 or 2 +- `handshake` -- `RS232_HANDSHAKE_*` constant +- `callback` -- receive callback function +- `ctx` -- user pointer passed through to the callback #### secLinkClose @@ -92,8 +182,9 @@ link handle. Returns `NULL` on failure. The callback is invoked from void secLinkClose(SecLinkT *link); ``` -Destroys cipher contexts, closes the packet layer and COM port, and -frees all memory. +Full teardown in order: destroys TX and RX cipher contexts (secure zero), +destroys the DH context if still present, closes the packet layer, closes +the COM port, zeroes the link structure, and frees memory. #### secLinkHandshake @@ -105,41 +196,17 @@ Performs a Diffie-Hellman key exchange. Blocks until both sides have exchanged public keys and derived cipher keys. The RNG must be seeded (via `secRngSeed()` or `secRngAddEntropy()`) before calling this. -Each side derives separate TX and RX keys from the shared secret, -using public key ordering to determine directionality. This prevents -CTR counter collisions. +Internally: +1. Creates a DH context and generates keys +2. Sends the 128-byte public key via the packet layer (blocking) +3. Polls the packet layer in a loop until the remote's public key arrives +4. Computes the shared secret and derives directional cipher keys +5. Destroys the DH context (forward secrecy) +6. Transitions the link to READY state -#### secLinkGetPending - -```c -int secLinkGetPending(SecLinkT *link); -``` - -Returns the number of unacknowledged packets in the transmit window. -Useful for non-blocking send loops to determine if there is room to -send more data. - -#### secLinkIsReady - -```c -bool secLinkIsReady(SecLinkT *link); -``` - -Returns `true` if the handshake is complete and the link is ready for -encrypted communication. - -#### secLinkPoll - -```c -int secLinkPoll(SecLinkT *link); -``` - -Reads available serial data, processes received frames, handles ACKs -and retransmits. Decrypts encrypted packets and delivers plaintext to -the receive callback. Returns the number of packets delivered, or -negative on error. - -Must be called frequently (e.g. in your main loop). +Returns `SECLINK_SUCCESS` or `SECLINK_ERR_HANDSHAKE` on failure +(DH key generation failure, send failure, or serial disconnect during +the exchange). #### secLinkSend @@ -148,14 +215,24 @@ int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt, bool block); ``` -Sends up to `SECLINK_MAX_PAYLOAD` (255) bytes on the given channel. +Sends up to `SECLINK_MAX_PAYLOAD` (254) bytes on the given channel. -- `channel` -- logical channel number (0-127) -- `encrypt` -- if `true`, encrypts the payload (requires completed handshake) -- `block` -- if `true`, waits for transmit window space; if `false`, - returns `SECLINK_ERR_SEND` when the window is full +- `channel` -- logical channel number (0-127) +- `encrypt` -- if `true`, encrypts the payload before sending. Requires + a completed handshake; returns `SECLINK_ERR_NOT_READY` otherwise. +- `block` -- if `true`, waits for transmit window space. If `false`, + returns `SECLINK_ERR_SEND` when the packet layer's window is full. -Cleartext packets (`encrypt = false`) can be sent before the handshake. +**Cipher counter safety:** The function checks transmit window space +BEFORE encrypting the payload. If it encrypted first and then the send +failed, the cipher counter would advance without the data being sent, +permanently desynchronizing the TX cipher state from the remote's RX +cipher. This ordering is critical for correctness. + +The channel header byte is prepended to the data, and only the payload +portion (not the header) is encrypted. + +Returns `SECLINK_SUCCESS` or an error code. #### secLinkSendBuf @@ -165,12 +242,54 @@ int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len, ``` Sends an arbitrarily large buffer by splitting it into -`SECLINK_MAX_PAYLOAD`-byte chunks. Always blocks until all data is -sent. Returns `SECLINK_SUCCESS` or the first error encountered. +`SECLINK_MAX_PAYLOAD`-byte (254-byte) chunks. Always blocks until all +data is sent. The receiver sees multiple packets on the same channel and +must reassemble if needed. -## Examples +Returns `SECLINK_SUCCESS` or the first error encountered. -### Basic encrypted link +#### secLinkPoll + +```c +int secLinkPoll(SecLinkT *link); +``` + +Delegates to `pktPoll()` to read serial data, process frames, handle +ACKs and retransmits. Received packets are routed through an internal +callback that: + +- During handshake: expects a 128-byte DH public key +- When ready: strips the channel header, decrypts the payload if the + encrypt flag is set, and forwards plaintext to the user callback + +Returns the number of packets delivered, or a negative error code. + +Must be called frequently (every iteration of your main loop). + +#### secLinkGetPending + +```c +int secLinkGetPending(SecLinkT *link); +``` + +Returns the number of unacknowledged packets in the transmit window. +Delegates directly to `pktGetPending()`. Useful for non-blocking send +loops to determine when there is room to send more data. + +#### secLinkIsReady + +```c +bool secLinkIsReady(SecLinkT *link); +``` + +Returns `true` if the DH handshake is complete and the link is ready for +encrypted communication. Cleartext sends do not require the link to be +ready. + + +## Usage Examples + +### Basic Encrypted Link ```c #include "secLink.h" @@ -186,13 +305,14 @@ int main(void) { secRngGatherEntropy(entropy, sizeof(entropy)); secRngSeed(entropy, sizeof(entropy)); - // Open link on COM1 at 115200 8N1 - SecLinkT *link = secLinkOpen(0, 115200, 8, 'N', 1, 0, onRecv, NULL); + // Open link on COM1 at 115200 8N1, no flow control + SecLinkT *link = secLinkOpen(RS232_COM1, 115200, 8, 'N', 1, + RS232_HANDSHAKE_NONE, onRecv, NULL); if (!link) { return 1; } - // Key exchange (blocks until both sides complete) + // DH key exchange (blocks until both sides complete) if (secLinkHandshake(link) != SECLINK_SUCCESS) { secLinkClose(link); return 1; @@ -212,31 +332,31 @@ int main(void) { } ``` -### Mixed encrypted and cleartext channels +### Mixed Encrypted and Cleartext Channels ```c #define CHAN_CONTROL 0 // cleartext control channel #define CHAN_DATA 1 // encrypted data channel -// Send a cleartext status message (no handshake needed) +// Cleartext status message (no handshake needed) secLinkSend(link, statusMsg, statusLen, CHAN_CONTROL, false, true); -// Send encrypted payload (requires completed handshake) +// Encrypted payload (requires completed handshake) secLinkSend(link, payload, payloadLen, CHAN_DATA, true, true); ``` -### Non-blocking file transfer +### Non-Blocking File Transfer ```c int sendFile(SecLinkT *link, const uint8_t *fileData, int fileSize, - uint8_t channel, bool encrypt, int windowSize) { + uint8_t channel, bool encrypt) { int offset = 0; int bytesLeft = fileSize; while (bytesLeft > 0) { secLinkPoll(link); // process ACKs, free window slots - if (secLinkGetPending(link) < windowSize) { + if (secLinkGetPending(link) < 4) { // window has room int chunk = bytesLeft; if (chunk > SECLINK_MAX_PAYLOAD) { chunk = SECLINK_MAX_PAYLOAD; @@ -248,7 +368,7 @@ int sendFile(SecLinkT *link, const uint8_t *fileData, int fileSize, offset += chunk; bytesLeft -= chunk; } - // SECLINK_ERR_SEND means window full, just retry next iteration + // SECLINK_ERR_SEND means window full, retry next iteration } // Application can do other work here: @@ -264,13 +384,34 @@ int sendFile(SecLinkT *link, const uint8_t *fileData, int fileSize, } ``` -### Blocking bulk transfer +### Blocking Bulk Transfer ```c // Send an entire file in one call (blocks until complete) secLinkSendBuf(link, fileData, fileSize, CHAN_DATA, true); ``` + +## Internal State Machine + +SecLink maintains a three-state internal state machine: + +| State | Value | Description | +|--------------|-------|----------------------------------------------| +| `STATE_INIT` | 0 | Link open, no handshake attempted yet | +| `STATE_HANDSHAKE` | 1 | DH key exchange in progress | +| `STATE_READY` | 2 | Handshake complete, ciphers ready | + +Transitions: +- `INIT -> HANDSHAKE`: when `secLinkHandshake()` is called +- `HANDSHAKE -> READY`: when the remote's public key is received and + cipher keys are derived +- Any state -> cleanup: when `secLinkClose()` is called + +Cleartext packets can be sent and received in any state. Encrypted +packets require `STATE_READY`. + + ## Building ``` @@ -278,18 +419,32 @@ make # builds ../lib/libseclink.a make clean # removes objects and library ``` -Link against all four libraries: +Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler +flags: `-O2 -Wall -Wextra -march=i486 -mtune=i586`. + +Objects are placed in `../obj/seclink/`, the library in `../lib/`. + +Link against all four libraries in this order: ``` -lseclink -lpacket -lsecurity -lrs232 ``` + +## Files + +- `secLink.h` -- Public API header (types, constants, function prototypes) +- `secLink.c` -- Complete implementation (handshake, send, receive, state + machine) +- `Makefile` -- DJGPP cross-compilation build rules + + ## Dependencies -SecLink requires these libraries (all in `../lib/`): +SecLink requires these libraries (all built into `../lib/`): -- `librs232.a` -- serial port driver -- `libpacket.a` -- packet framing and reliability -- `libsecurity.a` -- DH key exchange and XTEA cipher - -Target: DJGPP cross-compiler, 486+ CPU. +| Library | Purpose | +|------------------|---------------------------------------------| +| `librs232.a` | Serial port driver (ISR, ring buffers) | +| `libpacket.a` | HDLC framing, CRC-16, Go-Back-N ARQ | +| `libsecurity.a` | DH key exchange, XTEA-CTR cipher, RNG | diff --git a/security/README.md b/security/README.md index 8e7c50d..d5080bb 100644 --- a/security/README.md +++ b/security/README.md @@ -1,57 +1,224 @@ -# Security -- DH Key Exchange and XTEA-CTR Cipher +# Security -- Diffie-Hellman Key Exchange and XTEA-CTR Cipher + +Cryptographic library providing Diffie-Hellman key exchange, XTEA +symmetric encryption in CTR mode, and a DRBG-based pseudo-random number +generator. Optimized for 486-class DOS hardware running under DJGPP/DPMI. + +This library has no dependencies on the serial stack and can be used +independently for any application requiring key exchange, encryption, +or random number generation. -Cryptographic library providing Diffie-Hellman key exchange and XTEA -symmetric encryption, optimized for 486-class DOS hardware running -under DJGPP/DPMI. ## Components -### Diffie-Hellman Key Exchange +### 1. XTEA Cipher (CTR Mode) -- 1024-bit MODP group (RFC 2409 Group 2 safe prime) -- 256-bit private exponents for fast computation on 486 CPUs -- Montgomery multiplication (CIOS variant) for modular exponentiation -- Lazy-initialized Montgomery constants (R^2 mod p, -p0^-1 mod 2^32) +XTEA (eXtended Tiny Encryption Algorithm) is a 64-bit block cipher with a +128-bit key and 32 Feistel rounds. In CTR (counter) mode, it operates as +a stream cipher: an incrementing counter is encrypted with the key to +produce a keystream, which is XOR'd with the plaintext. Because XOR is +its own inverse, the same operation encrypts and decrypts. -### XTEA Cipher (CTR Mode) +**Why XTEA instead of AES or DES:** -- 128-bit key, 64-bit block size, 32 rounds -- CTR mode -- encrypt and decrypt are the same XOR operation -- No lookup tables, no key schedule -- just shifts, adds, and XORs -- Ideal for constrained environments with small key setup cost +XTEA requires zero lookup tables, no key schedule, and compiles to +approximately 20 instructions per round (shifts, adds, and XORs only). +This makes it ideal for a 486 where the data cache is tiny (8KB) and +AES's 4KB S-boxes would thrash it. DES is similarly table-heavy and has +a complex key schedule. XTEA has no library dependencies -- the entire +cipher fits in about a dozen lines of C. At 32 rounds, XTEA provides +128-bit security with negligible per-byte overhead even on the slowest +target hardware. -### Pseudo-Random Number Generator +**CTR mode properties:** + +- Encrypt and decrypt are the same function (XOR is symmetric) +- No padding required -- operates on arbitrary-length data +- Random access possible (set the counter to any value) +- CRITICAL: the same counter value must never be reused with the same key. + Reuse reveals the XOR of two plaintexts. The secLink layer prevents this + by deriving separate TX/RX cipher keys for each direction. + +**XTEA block cipher internals:** + +The Feistel network uses the golden-ratio constant (delta = 0x9E3779B9) +as a round key mixer. Each round combines the two 32-bit halves using +shifts, additions, and XORs. The delta ensures each round uses a +different effective subkey, preventing slide attacks. No S-boxes or lookup +tables are involved anywhere in the computation. + +### 2. Diffie-Hellman Key Exchange (1024-bit) + +Uses the RFC 2409 Group 2 safe prime (1024-bit MODP group) with a +generator of 2. Private exponents are 256 bits for fast computation on +486-class hardware. + +**Why 1024-bit DH with 256-bit private exponents:** + +RFC 2409 Group 2 provides a well-audited, interoperable safe prime. +256-bit private exponents (versus full 1024-bit) reduce the modular +exponentiation from approximately 1024 squarings+multiplies to approximately +256 squarings + approximately 128 multiplies (half the exponent bits are 1 on +average). This makes key generation feasible on a 486 in under a second +rather than minutes. The security reduction is negligible -- Pollard's +rho on a 256-bit exponent requires approximately 2^128 operations, matching +XTEA's key strength. + +**Key validation:** + +`secDhComputeSecret()` validates that the remote public key is in the +range [2, p-2] to prevent small-subgroup attacks. Keys of 0, 1, or p-1 +would produce trivially guessable shared secrets. + +**Key derivation:** + +The 128-byte shared secret is reduced to a symmetric key via XOR-folding: +each byte of the secret is XOR'd into the output key at position +`i % keyLen`. For a 16-byte XTEA key, each output byte is the XOR of +8 secret bytes, providing thorough mixing. A proper KDF (HKDF, etc.) +would be more rigorous but adds complexity and code size for marginal +benefit in this use case. + +### 3. Pseudo-Random Number Generator + +XTEA-CTR based DRBG (Deterministic Random Bit Generator). The RNG +encrypts a monotonically increasing 64-bit counter with a 128-bit XTEA +key, producing 8 bytes of pseudorandom output per block. The counter +never repeats (64-bit space is sufficient for any practical session +length), so the output is a pseudorandom stream as long as the key has +sufficient entropy. + +**Hardware entropy sources:** + +- PIT (Programmable Interval Timer) -- runs at 1.193182 MHz. Its LSBs + change rapidly and provide approximately 10 bits of entropy per read, + depending on timing jitter. Two readings with intervening code execution + provide additional jitter. +- BIOS tick count -- 18.2 Hz timer at real-mode address 0040:046C. Adds + a few more bits of entropy. + +Total from hardware: roughly 20 bits of real entropy per call to +`secRngGatherEntropy()`. This is not enough on its own for +cryptographic use but is sufficient to seed the DRBG when supplemented +by user interaction timing (keyboard, mouse jitter). + +**Seeding and mixing:** + +The seed function (`secRngSeed()`) XOR-folds the entropy into the XTEA +key, derives the initial counter from the key bits, and then generates and +discards 64 bytes to advance past any weak initial output. This discard +step is standard DRBG practice -- it ensures the first bytes the caller +receives do not leak information about the seed material. + +Additional entropy can be stirred in at any time via `secRngAddEntropy()` +without resetting the RNG state. This function XOR-folds new entropy into +the key and then re-mixes by encrypting the key with itself, diffusing +the new entropy across all key bits. + +Auto-seeding: if `secRngBytes()` is called before `secRngSeed()`, it +automatically gathers hardware entropy and seeds itself as a safety net. + + +## BigNum Arithmetic + +All modular arithmetic uses a 1024-bit big number type (`BigNumT`) +stored as 32 x `uint32_t` words in little-endian order. Operations: + +| Function | Description | +|----------------|------------------------------------------------------| +| `bnAdd` | Add two bignums, return carry | +| `bnSub` | Subtract two bignums, return borrow | +| `bnCmp` | Compare two bignums (-1, 0, +1) | +| `bnBit` | Test a single bit by index | +| `bnBitLength` | Find the highest set bit position | +| `bnShiftLeft1` | Left-shift by 1, return carry | +| `bnClear` | Zero all words | +| `bnSet` | Set to a 32-bit value (clear upper words) | +| `bnCopy` | Copy from source to destination | +| `bnFromBytes` | Convert big-endian byte array to little-endian words | +| `bnToBytes` | Convert little-endian words to big-endian byte array | +| `bnMontMul` | Montgomery multiplication (CIOS variant) | +| `bnModExp` | Modular exponentiation via Montgomery multiply | + + +## Montgomery Multiplication + +The CIOS (Coarsely Integrated Operand Scanning) variant computes +`a * b * R^(-1) mod m` in a single pass without explicit division by the +modulus. This replaces the expensive modular reduction step (division by a +1024-bit number) with cheaper additions and right-shifts. + +For each of the 32 outer iterations (one per word of operand `a`): +1. Accumulate `a[i] * b` into the temporary product `t` +2. Compute the Montgomery reduction factor `u = t[0] * m0inv mod 2^32` +3. Add `u * mod` to `t` and shift right by 32 bits (implicit division) + +After all iterations, the result is in the range [0, 2m), so a single +conditional subtraction brings it into [0, m). + +**Montgomery constants** (computed once, lazily on first DH use): + +- `R^2 mod p` -- computed via 2048 iterations of shift-left-1 with + conditional subtraction. This is the Montgomery domain conversion + factor. +- `-p[0]^(-1) mod 2^32` -- computed via Newton's method (5 iterations, + doubling precision each step: 1->2->4->8->16->32 correct bits). This + is the Montgomery reduction constant. + +**Modular exponentiation** uses left-to-right binary square-and-multiply +scanning. For a 256-bit private exponent, this requires approximately 256 +squarings plus approximately 128 multiplies (half the bits are 1 on average), +where each operation is a Montgomery multiplication on 32-word numbers. + + +## Secure Zeroing + +Key material (private keys, shared secrets, cipher contexts) is erased +using a volatile-pointer loop: + +```c +static void secureZero(void *ptr, int len) { + volatile uint8_t *p = (volatile uint8_t *)ptr; + for (int i = 0; i < len; i++) { + p[i] = 0; + } +} +``` + +The `volatile` qualifier prevents the compiler from optimizing away the +zeroing as a dead store. Without it, the compiler would see that the +buffer is about to be freed and remove the memset entirely. This is +critical for preventing sensitive key material from lingering in freed +memory where a later `malloc` could expose it. -- XTEA-CTR based DRBG (deterministic random bit generator) -- Hardware entropy from PIT counter (~10 bits) and BIOS tick count -- Supports additional entropy injection (keyboard timing, mouse, etc.) -- Auto-seeds from hardware on first use if not explicitly seeded ## Performance -At serial port speeds, encryption overhead is minimal: +At serial port speeds, XTEA-CTR encryption overhead is minimal: -| Speed | Blocks/sec | CPU cycles/sec | % of 33 MHz 486 | +| Speed | Blocks/sec | CPU Cycles/sec | % of 33 MHz 486 | |----------|------------|----------------|------------------| | 9600 | 120 | ~240K | < 1% | | 57600 | 720 | ~1.4M | ~4% | | 115200 | 1440 | ~2.9M | ~9% | -DH key exchange takes approximately 0.3s at 66 MHz or 0.6s at 33 MHz -(256-bit private exponent, 1024-bit modulus). +DH key exchange takes approximately 0.3 seconds at 66 MHz or 0.6 seconds +at 33 MHz (256-bit private exponent, 1024-bit modulus, Montgomery +multiplication). + ## API Reference ### Constants -| Name | Value | Description | -|---------------------|-------|--------------------------------| -| `SEC_DH_KEY_SIZE` | 128 | DH public key size (bytes) | -| `SEC_XTEA_KEY_SIZE` | 16 | XTEA key size (bytes) | -| `SEC_SUCCESS` | 0 | Success | -| `SEC_ERR_PARAM` | -1 | Invalid parameter | -| `SEC_ERR_NOT_READY` | -2 | Keys not yet generated/derived | -| `SEC_ERR_ALLOC` | -3 | Memory allocation failed | +| Name | Value | Description | +|---------------------|-------|-----------------------------------| +| `SEC_DH_KEY_SIZE` | 128 | DH public key size in bytes | +| `SEC_XTEA_KEY_SIZE` | 16 | XTEA key size in bytes | +| `SEC_SUCCESS` | 0 | Success | +| `SEC_ERR_PARAM` | -1 | Invalid parameter or NULL pointer | +| `SEC_ERR_NOT_READY` | -2 | Keys not yet generated/derived | +| `SEC_ERR_ALLOC` | -3 | Memory allocation failed | ### Types @@ -66,31 +233,35 @@ typedef struct SecCipherS SecCipherT; // Opaque cipher context int secRngGatherEntropy(uint8_t *buf, int len); ``` -Reads hardware entropy sources (PIT counter, BIOS tick count). Returns -the number of bytes written. Provides roughly 20 bits of true entropy. +Reads hardware entropy from the PIT counter and BIOS tick count. Returns +the number of bytes written (up to 8). Provides roughly 20 bits of true +entropy -- not sufficient alone, but enough to seed the DRBG when +supplemented by user interaction timing. ```c void secRngSeed(const uint8_t *entropy, int len); ``` Initializes the DRBG with the given entropy. XOR-folds the input into -the XTEA key, derives the counter, and mixes state by generating and -discarding 64 bytes. +the XTEA key, derives the initial counter, and generates and discards 64 +bytes to advance past weak initial output. ```c void secRngAddEntropy(const uint8_t *data, int len); ``` -Mixes additional entropy into the running RNG state without resetting -it. Use this to stir in keyboard timing, mouse jitter, or other -runtime entropy. +Mixes additional entropy into the running RNG state without resetting it. +XOR-folds data into the key and re-mixes by encrypting the key with +itself. Use this to stir in keyboard timing, mouse jitter, or other +runtime entropy sources. ```c void secRngBytes(uint8_t *buf, int len); ``` -Generates `len` pseudo-random bytes. Auto-seeds from hardware if not -previously seeded. +Generates `len` pseudorandom bytes. Auto-seeds from hardware entropy if +not previously seeded. Produces 8 bytes per XTEA block encryption of the +internal counter. ### Diffie-Hellman Functions @@ -98,42 +269,48 @@ previously seeded. SecDhT *secDhCreate(void); ``` -Allocates a new DH context. Returns `NULL` on allocation failure. +Allocates a new DH context. Returns `NULL` on allocation failure. The +context must be destroyed with `secDhDestroy()` when no longer needed. ```c int secDhGenerateKeys(SecDhT *dh); ``` Generates a 256-bit random private key and computes the corresponding -1024-bit public key (g^private mod p). The RNG should be seeded first. +1024-bit public key (`g^private mod p`). Lazily initializes Montgomery +constants on first call. The RNG should be seeded before calling this. ```c int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len); ``` -Exports the public key into `buf`. On entry, `*len` must be at least -`SEC_DH_KEY_SIZE` (128). On return, `*len` is set to 128. +Exports the public key as a big-endian byte array into `buf`. On entry, +`*len` must be at least `SEC_DH_KEY_SIZE` (128). On return, `*len` is +set to 128. ```c int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len); ``` -Computes the shared secret from the remote side's public key. -Validates that the remote key is in range [2, p-2] to prevent -small-subgroup attacks. +Computes the shared secret from the remote side's public key +(`remote^private mod p`). Validates the remote key is in range [2, p-2]. +Both sides compute this independently and arrive at the same value. ```c int secDhDeriveKey(SecDhT *dh, uint8_t *key, int keyLen); ``` -Derives a symmetric key by XOR-folding the 128-byte shared secret -down to `keyLen` bytes. +Derives a symmetric key by XOR-folding the 128-byte shared secret down +to `keyLen` bytes. Each output byte is the XOR of `128/keyLen` input +bytes. ```c void secDhDestroy(SecDhT *dh); ``` -Securely zeroes and frees the DH context (private key, shared secret). +Securely zeroes the entire DH context (private key, shared secret, public +key) and frees the memory. Must be called to prevent key material from +lingering in the heap. ### Cipher Functions @@ -141,30 +318,37 @@ Securely zeroes and frees the DH context (private key, shared secret). SecCipherT *secCipherCreate(const uint8_t *key); ``` -Creates an XTEA-CTR cipher context with the given 16-byte key. Counter -starts at zero. +Creates an XTEA-CTR cipher context with the given 16-byte key. The +internal counter starts at zero. Returns `NULL` on allocation failure or +NULL key. ```c void secCipherCrypt(SecCipherT *c, uint8_t *data, int len); ``` -Encrypts or decrypts `data` in place. CTR mode is symmetric -- the -same operation encrypts and decrypts. +Encrypts or decrypts `data` in place. CTR mode is symmetric -- the same +function handles both directions. The internal counter advances by one +for every 8 bytes processed (one XTEA block). The counter must never +repeat with the same key; callers are responsible for ensuring this +(secLink handles it by using separate cipher instances per direction). ```c void secCipherSetNonce(SecCipherT *c, uint32_t nonceLo, uint32_t nonceHi); ``` -Sets the 64-bit nonce/counter. Call before encrypting if you need a -specific starting counter value. +Sets the 64-bit nonce/counter to a specific value. Both the nonce +(baseline) and the running counter are set to the same value. Call this +before encrypting if you need a deterministic starting point. ```c void secCipherDestroy(SecCipherT *c); ``` -Securely zeroes and frees the cipher context. +Securely zeroes the cipher context (key and counter state) and frees the +memory. -## Example + +## Usage Examples ### Full Key Exchange @@ -187,12 +371,12 @@ int pubLen = SEC_DH_KEY_SIZE; secDhGetPublicKey(dh, myPub, &pubLen); // ... send myPub to remote, receive remotePub ... -// Compute shared secret and derive a 16-byte key +// Compute shared secret and derive a 16-byte XTEA key secDhComputeSecret(dh, remotePub, SEC_DH_KEY_SIZE); uint8_t key[SEC_XTEA_KEY_SIZE]; secDhDeriveKey(dh, key, SEC_XTEA_KEY_SIZE); -secDhDestroy(dh); +secDhDestroy(dh); // private key no longer needed // Create cipher and encrypt SecCipherT *cipher = secCipherCreate(key); @@ -200,8 +384,7 @@ uint8_t message[] = "Secret message"; secCipherCrypt(cipher, message, sizeof(message)); // message is now encrypted -// Decrypt (same operation -- CTR mode is symmetric) -// Reset counter first if using the same cipher context +// Decrypt (reset counter first, then apply same operation) secCipherSetNonce(cipher, 0, 0); secCipherCrypt(cipher, message, sizeof(message)); // message is now plaintext again @@ -209,10 +392,10 @@ secCipherCrypt(cipher, message, sizeof(message)); secCipherDestroy(cipher); ``` -### Standalone Encryption +### Standalone Encryption (Without DH) ```c -// XTEA-CTR can be used independently of DH +// XTEA-CTR can be used independently of Diffie-Hellman uint8_t key[SEC_XTEA_KEY_SIZE] = { /* your key */ }; SecCipherT *c = secCipherCreate(key); @@ -223,31 +406,24 @@ secCipherCrypt(c, data, sizeof(data)); // encrypt in place secCipherDestroy(c); ``` -## Implementation Details +### Random Number Generation -### BigNum Arithmetic +```c +// Seed from hardware +uint8_t hwEntropy[16]; +secRngGatherEntropy(hwEntropy, sizeof(hwEntropy)); +secRngSeed(hwEntropy, sizeof(hwEntropy)); -All modular arithmetic uses a 1024-bit big number type (`BigNumT`) -stored as 32 x `uint32_t` words in little-endian order. Operations: +// Stir in user-derived entropy (keyboard timing, etc.) +uint8_t userEntropy[4]; +// ... gather from timing events ... +secRngAddEntropy(userEntropy, sizeof(userEntropy)); -- Add, subtract, compare, shift-left-1, bit test -- Montgomery multiplication (CIOS with implicit right-shift) -- Modular exponentiation (left-to-right binary square-and-multiply) +// Generate random bytes +uint8_t randomBuf[32]; +secRngBytes(randomBuf, sizeof(randomBuf)); +``` -### Montgomery Multiplication - -The CIOS (Coarsely Integrated Operand Scanning) variant computes -`a * b * R^-1 mod m` in a single pass with implicit division by the -word base. Constants are computed once on first DH use: - -- `R^2 mod p` -- via 2048 iterations of shift-and-conditional-subtract -- `-p[0]^-1 mod 2^32` -- via Newton's method (5 iterations) - -### Secure Zeroing - -Key material is erased using a volatile-pointer loop that the compiler -cannot optimize away, preventing sensitive data from lingering in -memory. ## Building @@ -256,4 +432,23 @@ make # builds ../lib/libsecurity.a make clean # removes objects and library ``` -Target: DJGPP cross-compiler, 486+ CPU. +Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler +flags: `-O2 -Wall -Wextra -march=i486 -mtune=i586`. + +Objects are placed in `../obj/security/`, the library in `../lib/`. + +No external dependencies -- the library is self-contained. It uses only +DJGPP's ``, ``, and `` for hardware entropy +collection (PIT and BIOS tick count access). + + +## Files + +- `security.h` -- Public API header (types, constants, function prototypes) +- `security.c` -- Complete implementation (bignum, Montgomery, DH, XTEA, RNG) +- `Makefile` -- DJGPP cross-compilation build rules + + +## Used By + +- `seclink/` -- Secure serial link (DH handshake, cipher creation, RNG seeding) diff --git a/tasks/README.md b/tasks/README.md index 195d9cf..67d0752 100644 --- a/tasks/README.md +++ b/tasks/README.md @@ -1,45 +1,57 @@ -# taskswitch -- Cooperative Task Switching Library for DJGPP +# taskswitch -- Cooperative Task Switching Library -A lightweight cooperative multitasking library targeting DJGPP (i386 protected -mode DOS). Tasks voluntarily yield the CPU with `tsYield()`. A credit-based -weighted round-robin scheduler ensures every task runs while giving -higher-priority tasks proportionally more CPU time. +Cooperative (non-preemptive) multitasking library for DJGPP/DPMI (DOS +protected mode). Part of the DVX GUI project. + +Tasks voluntarily yield the CPU by calling `tsYield()`. A credit-based +weighted round-robin scheduler gives higher-priority tasks proportionally +more CPU time while guaranteeing that low-priority tasks are never +starved. A priority-10 task gets 11 turns per scheduling round; a +priority-0 task gets 1 -- but it always runs eventually. + +The task array is backed by stb_ds and grows dynamically. Terminated +task slots are recycled, so there is no fixed upper limit on the number +of tasks created over the lifetime of the application. + + +## Why Cooperative? + +DOS is single-threaded. DPMI provides no timer-based preemption. The +DVX GUI event model is inherently single-threaded: one compositor, one +input queue, one window stack. Preemptive switching would require +locking around every GUI call for no benefit. Cooperative switching +lets each task yield at safe points, avoiding synchronization entirely. -The task array grows dynamically using stb_ds and terminated task slots are -recycled, so there is no fixed upper limit on the number of tasks created -over the lifetime of the application. ## Files -| File | Description | -|-----------------------|------------------------------------------| -| `taskswitch.h` | Public API -- types, constants, functions | -| `taskswitch.c` | Implementation | -| `demo.c` | Example program exercising every feature | -| `thirdparty/stb_ds.h` | Dynamic array/hashmap library (stb) | -| `Makefile` | DJGPP cross-compilation build rules | +| File | Description | +|-----------------------|----------------------------------------------------| +| `taskswitch.h` | Public API -- types, constants, function prototypes | +| `taskswitch.c` | Implementation (scheduler, context switch, slots) | +| `demo.c` | Standalone test harness exercising all features | +| `thirdparty/stb_ds.h` | stb dynamic array/hashmap library (third-party) | +| `Makefile` | DJGPP cross-compilation build rules | + ## Building -Cross-compiling from Linux: +Cross-compile from Linux: ``` -make -``` - -Clean: - -``` -make clean +make # builds ../lib/libtasks.a +make demo # also builds ../bin/tsdemo.exe +make clean # removes objects, library, and demo binary ``` Output: -| Path | Description | -|-------------------|----------------------------| -| `../lib/libtasks.a` | Static library | -| `../obj/tasks/` | Object files | -| `../bin/tsdemo.exe` | Demo executable | +| Path | Description | +|---------------------|----------------------| +| `../lib/libtasks.a` | Static library | +| `../obj/tasks/` | Object files | +| `../bin/tsdemo.exe` | Demo executable | + ## Quick Start @@ -69,90 +81,95 @@ int main(void) { } ``` + ## Lifecycle -1. **`tsInit()`** -- Initialize the task system. The calling context - (typically `main`) becomes task 0 with `TS_PRIORITY_NORMAL`. +1. `tsInit()` -- Initialize the task system. The calling context + (typically `main`) becomes task 0 with `TS_PRIORITY_NORMAL`. No + separate stack is allocated for task 0 -- it uses the process stack. -2. **`tsCreate(...)`** -- Create tasks. Each gets a name, entry function, - argument pointer, stack size (0 for the 8 KB default), and a priority. - Returns the task ID (>= 0) or a negative error code. Terminated task - slots are reused automatically. +2. `tsCreate(...)` -- Create tasks. Each gets a name, entry function, + argument pointer, stack size (0 for the default), and a priority. + Returns the task ID (>= 0) or a negative error code. Terminated + task slots are reused automatically. -3. **`tsYield()`** -- Call from any task (including main) to hand the CPU to - the next eligible task. +3. `tsYield()` -- Call from any task (including main) to hand the CPU + to the next eligible task. This is the sole mechanism for task + switching. -4. **`tsShutdown()`** -- Free all task stacks and the task array. +4. `tsShutdown()` -- Free all task stacks and the task array. Tasks terminate by returning from their entry function or by calling -`tsExit()`. The main task (id 0) must never call `tsExit()`. When a task -terminates, its stack is freed immediately and its slot becomes available -for reuse by the next `tsCreate()` call. +`tsExit()`. The main task (id 0) must never call `tsExit()`. When a +task terminates, its stack is freed immediately and its slot becomes +available for reuse by the next `tsCreate()` call. + ## API Reference -### Initialisation and Teardown +### Initialization and Teardown -| Function | Signature | Description | -|----------------|------------------------------|--------------------------------------------------------------------------------------| -| `tsInit` | `int32_t tsInit(void)` | Initialise the library. Returns `TS_OK` or a negative error code. | -| `tsShutdown` | `void tsShutdown(void)` | Free all resources. Safe to call even if `tsInit` was never called. | +| Function | Signature | Description | +|--------------|-------------------------|--------------------------------------------------------------------| +| `tsInit` | `int32_t tsInit(void)` | Initialize the library. Returns `TS_OK` or a negative error code. | +| `tsShutdown` | `void tsShutdown(void)` | Free all resources. Safe to call even if `tsInit` was never called. | ### Task Creation and Termination -| Function | Signature | Description | -|------------|----------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------| -| `tsCreate` | `int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t ss, int32_t pri)` | Create a ready task. Returns the task ID (>= 0) or a negative error code. Pass 0 for `ss` to use `TS_DEFAULT_STACK_SIZE` (8 KB). Reuses terminated task slots when available. | -| `tsExit` | `void tsExit(void)` | Terminate the calling task. Must not be called from the main task. | -| `tsKill` | `int32_t tsKill(uint32_t taskId)` | Forcibly terminate another task. Frees its stack and marks the slot for reuse. Cannot kill the main task (id 0) or the calling task (use `tsExit` instead). | +| Function | Signature | Description | +|------------|---------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------| +| `tsCreate` | `int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t ss, int32_t pri)` | Create a ready task. Returns the task ID (>= 0) or a negative error code. Pass 0 for `ss` to use `TS_DEFAULT_STACK_SIZE`. Reuses terminated slots. | +| `tsExit` | `void tsExit(void)` | Terminate the calling task. Must not be called from the main task. Never returns. | +| `tsKill` | `int32_t tsKill(uint32_t taskId)` | Forcibly terminate another task. Cannot kill main (id 0) or self (use `tsExit` instead). | ### Scheduling -| Function | Signature | Description | -|-----------|-----------------------|--------------------------------------------------------------------------| -| `tsYield` | `void tsYield(void)` | Voluntarily relinquish the CPU to the next eligible ready task. | +| Function | Signature | Description | +|-----------|----------------------|-----------------------------------------------------------------| +| `tsYield` | `void tsYield(void)` | Voluntarily relinquish the CPU to the next eligible ready task. | ### Pausing and Resuming -| Function | Signature | Description | -|------------|-----------------------------------|--------------------------------------------------------------------------------------------------------------------| -| `tsPause` | `int32_t tsPause(uint32_t id)` | Pause a task. The main task (id 0) cannot be paused. If a task pauses itself, an implicit yield occurs. | -| `tsResume` | `int32_t tsResume(uint32_t id)` | Resume a paused task. Its credits are refilled to `priority + 1` so it is not penalised for having been paused. | +| Function | Signature | Description | +|------------|---------------------------------|------------------------------------------------------------------------------------------------------------| +| `tsPause` | `int32_t tsPause(uint32_t id)` | Pause a task. Main task (id 0) cannot be paused. Self-pause triggers an implicit yield. | +| `tsResume` | `int32_t tsResume(uint32_t id)` | Resume a paused task. Credits are refilled so it is not penalized for having been paused. | ### Priority -| Function | Signature | Description | -|-----------------|--------------------------------------------------|---------------------------------------------------------------------------------------------------| -| `tsSetPriority` | `int32_t tsSetPriority(uint32_t id, int32_t pri)`| Change a task's priority. Credits are reset to `pri + 1` so the change takes effect immediately. | -| `tsGetPriority` | `int32_t tsGetPriority(uint32_t id)` | Return the task's priority, or `TS_ERR_PARAM` on an invalid ID. | +| Function | Signature | Description | +|-----------------|---------------------------------------------------|-----------------------------------------------------------------------------------| +| `tsSetPriority` | `int32_t tsSetPriority(uint32_t id, int32_t pri)` | Change a task's priority. Credits are reset so the change takes effect immediately. | +| `tsGetPriority` | `int32_t tsGetPriority(uint32_t id)` | Return the task's priority, or `TS_ERR_PARAM` on an invalid ID. | ### Crash Recovery -| Function | Signature | Description | -|-------------------|---------------------------------|-----------------------------------------------------------------------------------------------------------------| -| `tsRecoverToMain` | `void tsRecoverToMain(void)` | Reset scheduler state to the main task (id 0) after a `longjmp` from a signal handler. Call before `tsKill` on the crashed task. This fixes the scheduler's bookkeeping when a non-main task crashes and execution is transferred back to main via `longjmp`. | +| Function | Signature | Description | +|-------------------|------------------------------|------------------------------------------------------------------------------------------------------------------| +| `tsRecoverToMain` | `void tsRecoverToMain(void)` | Reset scheduler state to task 0 after a `longjmp` from a signal handler. Call before `tsKill` on the crashed task. The crashed task's slot is NOT freed -- call `tsKill` afterward. | ### Query -| Function | Signature | Description | -|-----------------|-----------------------------------------|---------------------------------------------------------------| -| `tsGetState` | `TaskStateE tsGetState(uint32_t id)` | Return the task's state enum value. | -| `tsCurrentId` | `uint32_t tsCurrentId(void)` | Return the ID of the currently running task. | -| `tsGetName` | `const char *tsGetName(uint32_t id)` | Return the task's name string, or `NULL` on invalid ID. | -| `tsActiveCount` | `uint32_t tsActiveCount(void)` | Return the number of non-terminated tasks. | +| Function | Signature | Description | +|-----------------|--------------------------------------|--------------------------------------------------------| +| `tsGetState` | `TaskStateE tsGetState(uint32_t id)` | Return the task's state enum value. | +| `tsCurrentId` | `uint32_t tsCurrentId(void)` | Return the ID of the currently running task. | +| `tsGetName` | `const char *tsGetName(uint32_t id)` | Return the task's name string, or `NULL` on invalid ID. | +| `tsActiveCount` | `uint32_t tsActiveCount(void)` | Return the number of non-terminated tasks. | + ## Constants ### Error Codes -| Name | Value | Meaning | -|----------------|-------|--------------------------------------| -| `TS_OK` | 0 | Success | -| `TS_ERR_INIT` | -1 | Library not initialised | -| `TS_ERR_PARAM` | -2 | Invalid parameter | +| Name | Value | Meaning | +|----------------|-------|--------------------------------------------------| +| `TS_OK` | 0 | Success | +| `TS_ERR_INIT` | -1 | Library not initialized | +| `TS_ERR_PARAM` | -2 | Invalid parameter | | `TS_ERR_FULL` | -3 | Task table full (unused, kept for compatibility) | -| `TS_ERR_NOMEM` | -4 | Memory allocation failed | -| `TS_ERR_STATE` | -5 | Invalid state transition | +| `TS_ERR_NOMEM` | -4 | Memory allocation failed | +| `TS_ERR_STATE` | -5 | Invalid state transition | ### Priority Presets @@ -162,19 +179,22 @@ for reuse by the next `tsCreate()` call. | `TS_PRIORITY_NORMAL` | 5 | 6 | | `TS_PRIORITY_HIGH` | 10 | 11 | -Any non-negative `int32_t` may be used as a priority. The presets are -provided for convenience. +Any non-negative `int32_t` may be used as a priority. The presets are +provided for convenience. In the DVX Shell, the main task runs at +`TS_PRIORITY_HIGH` to keep the UI responsive; app tasks default to +`TS_PRIORITY_NORMAL`. ### Defaults -| Name | Value | Description | -|-------------------------|-------|-------------------------| -| `TS_DEFAULT_STACK_SIZE` | 8192 | Default stack per task | -| `TS_NAME_MAX` | 32 | Max task name length | +| Name | Value | Description | +|-------------------------|-------|------------------------| +| `TS_DEFAULT_STACK_SIZE` | 32768 | Default stack per task | +| `TS_NAME_MAX` | 32 | Max task name length | + ## Types -### `TaskStateE` +### TaskStateE ```c typedef enum { @@ -185,122 +205,153 @@ typedef enum { } TaskStateE; ``` -### `TaskEntryT` +Only Ready tasks participate in scheduling. Running is cosmetic (marks +the currently executing task). Paused tasks are skipped until explicitly +resumed. Terminated slots are recycled by `tsCreate`. + +### TaskEntryT ```c typedef void (*TaskEntryT)(void *arg); ``` -The signature every task entry function must follow. `arg` is the pointer -passed to `tsCreate`. +The signature every task entry function must follow. The `arg` parameter +is the pointer passed to `tsCreate`. + ## Scheduler Details -The scheduler is a **credit-based weighted round-robin**. +The scheduler is a credit-based weighted round-robin, a variant of the +Linux 2.4 goodness() scheduler. -1. Every ready task holds a credit counter initialised to `priority + 1`. -2. When `tsYield()` is called, the scheduler scans tasks starting one past - the current task (wrapping around) looking for a ready task with - credits > 0. When found, that task's credits are decremented and it +1. Every ready task holds a credit counter initialized to `priority + 1`. +2. When `tsYield()` is called, the scheduler scans tasks starting one + past the current task (wrapping around) looking for a ready task with + credits > 0. When found, that task's credits are decremented and it becomes the running task. -3. When **no** ready task has credits remaining, every ready task is - refilled to `priority + 1` and the scan repeats. +3. When no ready task has credits remaining, every ready task is + refilled to `priority + 1` (one "epoch") and the scan repeats. This means a priority-10 task receives 11 turns for every 1 turn a -priority-0 task receives, but the low-priority task still runs -- it is -never starved. +priority-0 task receives, but the low-priority task still runs -- it +is never starved. Credits are also refilled when: -- A task is **created** (`tsCreate`) -- starts with `priority + 1`. -- A task is **resumed** (`tsResume`) -- refilled so it is not penalised. -- A task's **priority changes** (`tsSetPriority`) -- reset to `new + 1`. +- A task is created (`tsCreate`) -- starts with `priority + 1`. +- A task is resumed (`tsResume`) -- refilled so it runs promptly. +- A task's priority changes (`tsSetPriority`) -- reset to `new + 1`. + ## Task Slot Management -The task array is a stb_ds dynamic array that grows automatically as needed. +The task array is a stb_ds dynamic array that grows automatically. Each slot has an `allocated` flag: -- **`tsCreate()`** scans for the first unallocated slot (starting at index 1, - since slot 0 is always the main task). If no free slot exists, the array - is extended with `arrput()`. -- **`tsExit()`** frees the terminated task's stack immediately and marks the - slot as unallocated, making it available for the next `tsCreate()` call. -- Task IDs are stable array indices. Slots are never removed or reordered, - so a task ID remains valid for queries until the slot is recycled. +- `tsCreate()` scans for the first unallocated slot (starting at index + 1, since slot 0 is always the main task). If no free slot exists, the + array is extended with `arrput()`. +- `tsExit()` and `tsKill()` free the terminated task's stack immediately + and mark the slot as unallocated, making it available for the next + `tsCreate()` call. +- Task IDs are stable array indices. Slots are never removed or + reordered, so a task ID remains valid for queries until the slot is + recycled. + +This supports long-running applications (like the DVX Shell) that +create and destroy many tasks over their lifetime without unbounded +memory growth. -This design supports long-running applications that create and destroy -many tasks over their lifetime without unbounded memory growth. ## Context Switch Internals -Context switching is performed entirely in inline assembly with both i386 -and x86_64 code paths. +Context switching uses inline assembly with both i386 and x86_64 code +paths. The `contextSwitch` function is marked `noinline` to preserve +callee-saved register assumptions. + +Why inline asm instead of setjmp/longjmp: setjmp/longjmp only save +callee-saved registers and do not give control over the stack pointer +in a portable way. New tasks need a fresh stack with the instruction +pointer set to a trampoline -- setjmp cannot bootstrap that. The asm +approach also avoids ABI differences in jmp_buf layout across DJGPP +versions. ### i386 (DJGPP target) Six callee-saved values are saved and restored per switch: -| Register | Offset | Purpose | -|----------|--------|-----------------------------------------| -| EBX | 0 | Callee-saved general purpose | -| ESI | 4 | Callee-saved general purpose | -| EDI | 8 | Callee-saved general purpose | -| EBP | 12 | Frame pointer | -| ESP | 16 | Stack pointer | -| EIP | 20 | Resume address (captured as local label)| +| Register | Offset | Purpose | +|----------|--------|------------------------------------------| +| EBX | 0 | Callee-saved general purpose | +| ESI | 4 | Callee-saved general purpose | +| EDI | 8 | Callee-saved general purpose | +| EBP | 12 | Frame pointer | +| ESP | 16 | Stack pointer | +| EIP | 20 | Resume address (captured as local label) | -### x86_64 (for native testing) +### x86_64 (for native Linux testing) Eight callee-saved values are saved and restored per switch: -| Register | Offset | Purpose | -|----------|--------|-----------------------------------------| -| RBX | 0 | Callee-saved general purpose | -| R12 | 8 | Callee-saved general purpose | -| R13 | 16 | Callee-saved general purpose | -| R14 | 24 | Callee-saved general purpose | -| R15 | 32 | Callee-saved general purpose | -| RBP | 40 | Frame pointer | -| RSP | 48 | Stack pointer | -| RIP | 56 | Resume address (RIP-relative lea) | +| Register | Offset | Purpose | +|----------|--------|------------------------------------------| +| RBX | 0 | Callee-saved general purpose | +| R12 | 8 | Callee-saved general purpose | +| R13 | 16 | Callee-saved general purpose | +| R14 | 24 | Callee-saved general purpose | +| R15 | 32 | Callee-saved general purpose | +| RBP | 40 | Frame pointer | +| RSP | 48 | Stack pointer | +| RIP | 56 | Resume address (RIP-relative lea) | -The save and restore pointers are passed into the assembly block via GCC -register constraints. Segment registers are not saved because DJGPP runs -in a flat protected-mode environment where CS, DS, ES, and SS share the -same base. +Segment registers are not saved because DJGPP runs in a flat +protected-mode environment where CS, DS, ES, and SS share the same +base. + +New tasks have their initial stack pointer set to a 16-byte-aligned +region at the top of a malloc'd stack, with the instruction pointer +set to an internal trampoline that calls the user's entry function +and then `tsExit()`. -New tasks have their initial stack pointer set to a 16-byte-aligned region -at the top of a `malloc`'d stack, with the instruction pointer set to an -internal trampoline that calls the user's entry function and then `tsExit()`. ## Limitations -- **Cooperative only** -- tasks must call `tsYield()` (or `tsPause`/`tsExit`) - to allow other tasks to run. A task that never yields blocks everything. -- **Not interrupt-safe** -- the library uses no locking or `volatile` module - state. Do not call library functions from interrupt handlers. +- **Cooperative only** -- tasks must call `tsYield()` (or + `tsPause`/`tsExit`) to allow other tasks to run. A task that never + yields blocks everything. +- **Not interrupt-safe** -- no locking or volatile module state. Do not + call library functions from interrupt handlers. - **Single-threaded** -- designed for one CPU under DOS protected mode. -- **Stack overflow is not detected** -- size the stack appropriately for each - task's needs. +- **Stack overflow is not detected** -- size the stack appropriately for + each task's needs. + ## Demo `demo.c` exercises five phases: -1. **Priority scheduling** -- creates tasks at low, normal, and high priority. - All tasks run, but the high-priority task gets significantly more turns. -2. **Pause** -- pauses one task mid-run and shows it stops being scheduled. +1. **Priority scheduling** -- creates tasks at low, normal, and high + priority. All tasks run, but the high-priority task gets significantly + more turns. +2. **Pause** -- pauses one task mid-run and shows it stops being + scheduled. 3. **Resume** -- resumes the paused task and shows it picks up where it left off. -4. **Priority boost** -- raises the low-priority task above all others and - shows it immediately gets more turns. -5. **Slot reuse** -- creates three waves of short-lived tasks that terminate - and shows subsequent waves reuse the same task IDs. +4. **Priority boost** -- raises the low-priority task above all others + and shows it immediately gets more turns. +5. **Slot reuse** -- creates three waves of short-lived tasks that + terminate and shows subsequent waves reuse the same task IDs. Build and run: ``` -make +make demo tsdemo ``` + + +## Third-Party Dependencies + +- **stb_ds.h** (Sean Barrett) -- dynamic array and hashmap library. + Located in `thirdparty/stb_ds.h`. Used for the task control block + array. Public domain / MIT licensed. diff --git a/termdemo/README.md b/termdemo/README.md index 2ef02c7..da8a680 100644 --- a/termdemo/README.md +++ b/termdemo/README.md @@ -1,22 +1,30 @@ # SecLink Terminal Demo -DOS terminal emulator combining the DVX windowed GUI with SecLink -encrypted serial communication. Connects to a remote BBS through the -SecLink proxy, providing a full ANSI terminal in a DVX-style -window with encrypted transport. +Standalone DOS terminal emulator combining the DVX windowed GUI with +SecLink encrypted serial communication. Part of the DVX GUI project. + +This is NOT a DXE app -- it is a freestanding program with its own +`main()` that initializes the DVX GUI directly and manages its own +event loop. Unlike the DXE apps (progman, notepad, clock, dvxdemo) +which run inside the DVX Shell, this program demonstrates how to use +the DVX widget system outside the shell framework. + +Connects to a remote BBS through the SecLink proxy, providing a full +ANSI terminal in a DVX-style window with encrypted transport. + ## Architecture ``` termdemo (DOS, 86Box) | - +--- DVX GUI windowed desktop, ANSI terminal widget + +--- DVX GUI windowed desktop, ANSI terminal widget | - +--- SecLink encrypted serial link + +--- SecLink encrypted serial link | | - | +--- packet HDLC framing, CRC, retransmit - | +--- security DH key exchange, XTEA-CTR cipher - | +--- rs232 ISR-driven UART I/O + | +--- packet HDLC framing, CRC-16, Go-Back-N ARQ + | +--- security DH key exchange, XTEA-CTR cipher + | +--- rs232 ISR-driven UART I/O | COM port (86Box emulated modem) | @@ -33,16 +41,19 @@ All traffic between the terminal and the proxy is encrypted via XTEA-CTR on SecLink channel 0. The proxy decrypts and forwards plaintext to the BBS over telnet. + ## Usage ``` termdemo [com_port] [baud_rate] ``` -| Argument | Default | Description | -|-------------|---------|------------------------------| -| `com_port` | 1 | COM port number (1-4) | -| `baud_rate` | 115200 | Serial baud rate | +| Argument | Default | Description | +|-------------|---------|--------------------------| +| `com_port` | 1 | COM port number (1-4) | +| `baud_rate` | 115200 | Serial baud rate | + +Examples: ``` termdemo # COM1 at 115200 @@ -51,25 +62,33 @@ termdemo 1 57600 # COM1 at 57600 termdemo -h # show usage ``` + ## Startup Sequence -1. Seed the RNG from hardware entropy -2. Open SecLink on the specified COM port (8N1, no handshake) -3. Perform DH key exchange (blocks until the proxy completes its side) -4. Initialize the DVX GUI (1024x768, 16bpp VESA) -5. Create a resizable terminal window with menu bar and status bar -6. Enter the main loop +1. Seed the RNG from hardware entropy (PIT-based on DOS). +2. Open SecLink on the specified COM port (8N1, no handshake). +3. Perform DH key exchange (blocks until the proxy completes its side). +4. Initialize the DVX GUI (1024x768, 16bpp VESA). +5. Create a resizable terminal window with menu bar and status bar. +6. Register an idle callback so serial data is polled during GUI idle. +7. Enter the main loop. The handshake completes in text mode before the GUI starts, so the DOS console shows progress messages during connection setup. + ## Main Loop Each iteration: -1. `dvxUpdate()` -- process mouse, keyboard, paint, and window events -2. `secLinkPoll()` -- read serial data, decrypt, deliver to ring buffer -3. `wgtAnsiTermPoll()` -- drain ring buffer into the ANSI parser +1. `secLinkPoll()` -- read serial data, decrypt, deliver to ring buffer. +2. `dvxUpdate()` -- process mouse, keyboard, paint, and window events. + During paint, the terminal widget calls `commRead` to drain the ring + buffer and render new data. + +An idle callback also calls `secLinkPoll()` so incoming data is +processed even when the user is not interacting with the terminal. + ## Data Flow @@ -81,9 +100,13 @@ Keyboard -> widgetAnsiTermOnKey() -> commWrite() -> secLinkSend() -> serial -> proxy -> BBS ``` -A 4KB ring buffer bridges the SecLink receive callback (which fires -during `secLinkPoll()`) and the terminal widget's comm read interface -(which is polled by `wgtAnsiTermPoll()`). +A 4KB ring buffer (`RECV_BUF_SIZE`) bridges the SecLink receive +callback (which fires asynchronously during `secLinkPoll()`) and the +terminal widget's comm read interface (which is polled synchronously +during the widget paint cycle). This decoupling is necessary because +the callback can fire at any time during polling, but the terminal +widget expects to read data synchronously. + ## GUI @@ -97,6 +120,7 @@ cursor control, SGR colors (16-color CGA palette), erase, scroll, insert/delete lines, and DEC private modes (cursor visibility, line wrap). + ## Test Setup 1. Start the SecLink proxy on the Linux host: @@ -106,7 +130,7 @@ wrap). ``` 2. Configure 86Box with a COM port pointing at the proxy's listen port - (TCP client mode, port 2323, no telnet negotiation) + (TCP client mode, port 2323, no telnet negotiation). 3. Run the terminal inside 86Box: @@ -115,31 +139,35 @@ wrap). ``` 4. The handshake completes, the GUI appears, and BBS output is - displayed in the terminal window + displayed in the terminal window. + ## Building ``` -make # builds ../bin/termdemo.exe -make clean # removes objects and binary +make # builds ../bin/termdemo.exe (and all dependency libs) +make clean # removes objects and binary ``` -The Makefile builds all dependency libraries automatically. Objects -are placed in `../obj/termdemo/`, the binary in `../bin/`. +The Makefile automatically builds all dependency libraries before +linking. Objects are placed in `../obj/termdemo/`, the binary in +`../bin/`. + +Target: DJGPP cross-compiler, 486+ CPU, VESA VBE 2.0+ video. + ## Dependencies -All libraries are in `../lib/`: +All libraries are built into `../lib/`: | Library | Purpose | |------------------|--------------------------------------| -| `libdvx.a` | DVX windowed GUI and widget system | +| `libdvx.a` | DVX windowed GUI and widget system | | `libseclink.a` | Secure serial link wrapper | | `libpacket.a` | HDLC framing and reliability | | `libsecurity.a` | DH key exchange and XTEA cipher | | `librs232.a` | ISR-driven UART serial driver | -Target: DJGPP cross-compiler, 486+ CPU, VESA VBE 2.0+ video. ## Files