Files
gw-basic-2026/platform/hal_dos.c
Eremey Valetov 70ffd39562 v0.17.0: BIOS-routed TUI on DOS, version banner, compiler PulseAudio link
QA findings from a multi-round review of the FreeDOS submission prep work:

- TUI rendering refactor: src/tui.c emitted ANSI escape sequences via
  printf, which displays as raw text on bare FreeDOS (no ANSI.SYS).
  Add four HAL ops (tui_enter, tui_leave, render_run, set_cursor_shape)
  and route per-cell rendering through them.  POSIX backend keeps the
  ANSI path; DOS backend drives BIOS INT 10h via the existing
  bios_set_cursor / bios_write_char helpers.  The TUI's logical cursor
  goes through the saved orig_locate to avoid recursing through the
  swapped-in gw_hal->locate.

- DOS extended-key mapping: dos_getch returns 0x100 | scancode for
  arrows / F-keys; tui_read_key wasn't translating those to its TK_*
  constants, so the editor never saw arrow keys or F1-F10 on DOS.
  Add a __MSDOS__-conditional translation table in tui_read_key.

- Version banner: GW_VERSION was still 0.16.0 even though the v0.17.0
  release prep was already in CHANGES.TXT.  Bump.

- Compiler PulseAudio link: gwbasic-compile -c hardcoded
  '-lgwrt -lm -lpthread' on the gcc command line.  When libgwrt was
  built against libpulse-simple (the default on any host with the
  PulseAudio dev headers installed), the compile workflow failed with
  'undefined reference to pa_simple_drain'.  CMake now passes
  GWRT_HAS_PULSEAUDIO to gwbasic-compile when libpulse is present, and
  the compiler appends -lpulse-simple to the link line.

- FRE("") garbage collection: the interpreter skipped strpool_gc with a
  comment 'unsafe during expression eval', but that's exactly what real
  GW-BASIC's FRE("") does (and the AOT compiler path already did).  Add
  the GC call; strpool_pin/unpin is the existing escape hatch if a
  caller has live pool pointers on the C stack.  Fixes the string_gc
  compat test.

- Test harness normalization: run_tests.sh stripped trailing whitespace
  on the actual output but not the expected file, causing spurious
  mismatches against golden files captured from real GWBASIC.EXE.
  Normalize both sides identically.  Fixes the peek_gfx mismatch.

- Print_using: snprintf into mantissa[32] with %.*f and an unbounded
  dec triggered a -Wformat-truncation warning.  Clamp dec to 20 (IEEE
  double has at most ~17 significant decimal digits).

- Doc/version consistency: 16-bit binary size reported as 127KB in one
  place and 128KB in three; standardize on 128KB.  HAL backend count
  said '1 file' but is now 2.  CI test count said 'all 66 test
  programs' but is 72.  Add a v0.17.0 row to the development.md table.
  Update getting-started.md DOS section to match the BIOS-rendering
  reality and add a manual TUI verification checklist.

- dos_init now writes back BIOS-reported cols/rows to dos_hal struct
  fields (forward-declared so dos_init can reference it).

After these changes: 72/72 interpreter tests pass, compat 68/68
matched, no warnings on the Linux build.
2026-05-03 12:25:41 -04:00

231 lines
5.7 KiB
C

/*
* DOS HAL backend for GW-BASIC 2026.
*
* Uses BIOS/DOS interrupts for terminal I/O, keyboard input,
* and screen control. Selected at compile time via __MSDOS__.
* Linux HAL (hal_posix.c) is unchanged -- full backward compatibility.
*
* Build: wcc386 -bt=dos -mf -ox -za99 -D__MSDOS__ -Iinclude (32-bit)
* wcc -bt=dos -mm -ox -za99 -D__MSDOS__ -Iinclude (16-bit)
*/
#ifdef __MSDOS__
#include "hal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <conio.h>
#include <i86.h>
#include <dos.h>
/* int86 (16-bit real mode) vs int386 (32-bit protected mode) */
#ifdef _M_I86
#define INTX(n, r_in, r_out) int86(n, r_in, r_out)
#else
#define INTX(n, r_in, r_out) int386(n, r_in, r_out)
#endif
static int cursor_row = 0;
static int cursor_col = 0;
static int screen_cols = 80;
static int screen_rows = 25;
/* --- BIOS video services (INT 10h) --- */
static void bios_set_cursor(int row, int col)
{
union REGS r;
memset(&r, 0, sizeof(r));
r.h.ah = 0x02;
r.h.bh = 0;
r.h.dh = (unsigned char)row;
r.h.dl = (unsigned char)col;
INTX(0x10, &r, &r);
cursor_row = row;
cursor_col = col;
}
static void bios_get_cursor(int *row, int *col)
{
union REGS r;
memset(&r, 0, sizeof(r));
r.h.ah = 0x03;
r.h.bh = 0;
INTX(0x10, &r, &r);
*row = r.h.dh;
*col = r.h.dl;
}
static void bios_scroll_up(int lines, int attr, int r1, int c1, int r2, int c2)
{
union REGS r;
memset(&r, 0, sizeof(r));
r.h.ah = 0x06;
r.h.al = (unsigned char)lines;
r.h.bh = (unsigned char)attr;
r.h.ch = (unsigned char)r1;
r.h.cl = (unsigned char)c1;
r.h.dh = (unsigned char)r2;
r.h.dl = (unsigned char)c2;
INTX(0x10, &r, &r);
}
static void bios_write_char(int ch, int attr)
{
union REGS r;
memset(&r, 0, sizeof(r));
r.h.ah = 0x09;
r.h.al = (unsigned char)ch;
r.h.bh = 0;
r.h.bl = (unsigned char)attr;
r.w.cx = 1;
INTX(0x10, &r, &r);
}
/* --- Terminal I/O --- */
static void dos_putch(int ch)
{
if (ch == '\n') {
cursor_col = 0;
cursor_row++;
if (cursor_row >= screen_rows) {
bios_scroll_up(1, 0x07, 0, 0, screen_rows - 1, screen_cols - 1);
cursor_row = screen_rows - 1;
}
bios_set_cursor(cursor_row, cursor_col);
} else if (ch == '\r') {
cursor_col = 0;
bios_set_cursor(cursor_row, cursor_col);
} else if (ch == '\b') {
if (cursor_col > 0) cursor_col--;
bios_set_cursor(cursor_row, cursor_col);
} else {
bios_write_char(ch, 0x07);
cursor_col++;
if (cursor_col >= screen_cols) {
cursor_col = 0;
cursor_row++;
if (cursor_row >= screen_rows) {
bios_scroll_up(1, 0x07, 0, 0, screen_rows - 1, screen_cols - 1);
cursor_row = screen_rows - 1;
}
}
bios_set_cursor(cursor_row, cursor_col);
}
}
static void dos_puts(const char *s)
{
while (*s) dos_putch(*s++);
}
static int dos_getch(void)
{
union REGS r;
memset(&r, 0, sizeof(r));
r.h.ah = 0x00;
INTX(0x16, &r, &r);
return r.h.al ? r.h.al : (0x100 | r.h.ah);
}
static bool dos_kbhit(void)
{
return kbhit() != 0;
}
static void dos_locate(int row, int col)
{
bios_set_cursor(row, col);
}
static int dos_get_cursor_row(void) { return cursor_row; }
static int dos_get_cursor_col(void) { return cursor_col; }
static void dos_cls(void)
{
bios_scroll_up(0, 0x07, 0, 0, screen_rows - 1, screen_cols - 1);
bios_set_cursor(0, 0);
}
static void dos_set_width(int cols) { (void)cols; }
static void dos_enable_raw(void) { }
static void dos_disable_raw(void) { }
static void dos_write_raw(const char *data, int len)
{
for (int i = 0; i < len; i++)
dos_putch(data[i]);
}
static void dos_tui_enter(void)
{
bios_scroll_up(0, 0x07, 0, 0, screen_rows - 1, screen_cols - 1);
bios_set_cursor(0, 0);
}
static void dos_tui_leave(void)
{
bios_scroll_up(0, 0x07, 0, 0, screen_rows - 1, screen_cols - 1);
bios_set_cursor(0, 0);
}
static void dos_render_run(int row, int col,
const uint8_t *chars, const uint8_t *attrs, int len)
{
/* INT 10h AH=09h writes char+attr at the cursor without advancing it. */
for (int i = 0; i < len; i++) {
bios_set_cursor(row, col + i);
unsigned char ch = chars[i];
bios_write_char(ch ? ch : ' ', attrs[i]);
}
}
static void dos_set_cursor_shape(int shape)
{
union REGS r;
memset(&r, 0, sizeof(r));
r.h.ah = 0x01;
switch (shape) {
case 0: r.h.ch = 0x20; r.h.cl = 0; break; /* hide (high bit set) */
case 1: r.h.ch = 0; r.h.cl = 7; break; /* block */
case 2: r.h.ch = 6; r.h.cl = 7; break; /* underline */
default: return;
}
INTX(0x10, &r, &r);
}
/* Forward-declared so dos_init can write back screen size before the struct
* definition's positional initializers populate the rest. */
static hal_ops_t dos_hal;
static void dos_init(void)
{
union REGS r;
memset(&r, 0, sizeof(r));
r.h.ah = 0x0F;
INTX(0x10, &r, &r);
screen_cols = r.h.ah;
screen_rows = 25; /* safe default; BIOS data area read needs far ptr */
dos_hal.screen_width = screen_cols;
dos_hal.screen_height = screen_rows;
bios_get_cursor(&cursor_row, &cursor_col);
}
static void dos_shutdown(void) { }
static hal_ops_t dos_hal = {
dos_putch, dos_puts, dos_getch, dos_kbhit,
dos_locate, dos_get_cursor_row, dos_get_cursor_col,
dos_cls, dos_set_width, dos_enable_raw, dos_disable_raw,
dos_write_raw,
dos_tui_enter, dos_tui_leave, dos_render_run, dos_set_cursor_shape,
80, 25, 1 /* is_tty */,
dos_init, dos_shutdown
};
hal_ops_t *hal_dos_create(void) { return &dos_hal; }
#endif /* __MSDOS__ */