Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/portable-midi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # verify immutable downloads against their qualified commits
- name: Existing public host suite
run: python3 tools/build.py --target host --cc gcc
- name: Portable MIDI checks
Expand Down
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ Windows 3.0 USER/GDI project named libwintandy.



## 8088 library improvements

Optional additions: [opaque packed tiles](docs/TILE8.md),
[faster clears/spans](docs/GFX-FILL.md),
[four-model IRQ0 support](docs/TIMER-QUALIFICATION.md),
[best-effort orderly DOS guards](docs/DOS-GUARD.md), and an explicit-loss
[AY control-frame adapter](docs/AY-FRAMES.md). All are independently usable;
no application assets, song data or mandatory runtime are added.

See [qualification and hardware limits](docs/IMPROVEMENTS-VALIDATION.md).
Emulator cycle settings are not calibrated physical EX performance.

## Scope and contracts

| Modules | Contract |
Expand All @@ -17,6 +29,9 @@ Windows 3.0 USER/GDI project named libwintandy.
| `lt_sound.h`, `lt_chip.c`, DOS sink | Caller owns hardware exclusion and scheduling. Owner tokens track channels; they do not arbitrate against other applications. Stop mutes before unclaim. |
| `lt_gfx.h`, `lt_gfx.c` | Caller supplies a valid 32 KiB packed four-bank mode-9 aperture. No mode switch, allocation or resident service in the raster core. |
| `lt_dos_app.h` (optional) | Foreground convenience setup for mode 9, the direct video aperture, BIOS input state and the default Tandy sound port. Saves/restores video page and cursor; no allocation or resident service. |
| `lt_gfx_tile8.h` | Optional opaque packed 8x8 tile; clipped fallback and 8088 fast path, no heap/cache. |
| `lt_guard_dos.h` | Optional DOS3+ INT23/24 quit/fail latches; best-effort foreground cleanup, adapter requires `-s -zu`. |
| `lt_ay.h` | Optional AY frame-to-PSG plan and audio-facade apply; explicit clocks/loss flags, caller-resolved envelopes, no player/timer. |
| `lt_gfx_sprite8.h` | Index 0 is transparent; nonzero indices mapped to black remain opaque. Ordered destination overlap is allowed; source/framebuffer aliasing is excluded. |
| `lt_surface.h`, `lt_media.h` | Bounded caller-owned INDEX8 storage, clipping, blits and palettes. No SDL code or compatibility claim. |
| `lt_event.h`, DOS input | Bounded foreground queue; BIOS press events and optional mouse snapshots. No key-up/held-state guarantee, installed ISR or mouse callback (see `lt_keys_dos.h` for held keys). |
Expand Down Expand Up @@ -48,7 +63,22 @@ python tools/build.py --target dos --watcom C:/WATCOM
This compiles the modules for real-mode DOS with Open Watcom `-0 -ms -ecc`,
builds `build/dos/DEMO.EXE`, `build/dos/SFXDEMO.EXE` and the DOS unit-test
executables, and does not run them or start an emulator. The code targets
8086/8088, not 186/286 instructions.
8086/8088, not 186/286 instructions. Use `--model compact`, `--model medium`
or `--model large` to qualify a different DOS ABI; every linked component
must use the same model/calling convention. DOS builds also include `GUARD`
and `TILEBEN` diagnostic/benchmark programs.

After building, optional native qualification is explicit:

```sh
python tools/qualify_dos.py --dosbox /path/to/dosbox-x
python tests/tick_matrix.py --watcom /path/to/watcom --dosbox /path/to/dosbox-x
python tools/qualify_guard.py --watcom /path/to/watcom --dosbox /path/to/dosbox-x
python tests/gfx_far_matrix.py --watcom /path/to/watcom --dosbox /path/to/dosbox-x
```

These tools use installed software and isolated build directories; they do
not download toolchains or modify physical hardware.
In DOS on a compatible Tandy, `DEMO` enters mode 9, polls BIOS key presses,
moves an original square, plays a tone and restores the prior video mode, page
and cursor while muting/releasing its sound channels when Escape is pressed.
Expand Down
3 changes: 2 additions & 1 deletion SOURCE.json
Original file line number Diff line number Diff line change
Expand Up @@ -295,5 +295,6 @@
}
},
"third_party_code": "none; jaws_dos.c (GPL RAVE-derived clock and keyboard) excluded; shared hardware technique flagged in the provenance file"
}
},
"provenance_note": "The source_revision and source_lf_sha256 entries record the historical initial import, not a rolling current-build manifest. Later library qualification is documented in docs/IMPROVEMENTS-VALIDATION.md."
}
6 changes: 6 additions & 0 deletions VALIDATION.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# Additional 8088 library qualification

The 2026-11-02 general-purpose improvements have strict host and native DOS
qualification. See [current results and limitations](docs/IMPROVEMENTS-VALIDATION.md).
The historical publication record below is retained separately.

# Initial public source edition validation

Checked 2026-10-18 against the source selection in `SOURCE.json`.
Expand Down
60 changes: 60 additions & 0 deletions adapters/lt_guard_dos.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/* SPDX-License-Identifier: MIT */
/* Optional DOS guard. Direct handlers do not replace the CRT _harderr policy. */
#if !defined(__WATCOMC__) || !defined(__I86__) || !defined(__DOS__)
#error lt_guard_dos requires Open Watcom 16-bit DOS
#endif
#if !defined(__SW_ZU) || !__SW_ZU || !defined(__SW_S) || !__SW_S
#error Compile lt_guard_dos.c separately with -s -zu (foreign interrupt stack)
#endif
#include "lt_guard_dos.h"
#include <dos.h>
#include <i86.h>
#include <stdlib.h>

typedef void (__interrupt __far *ltguard_vector)(void);
static ltguard_vector previous23, previous24;
static unsigned opened;
static volatile unsigned quit_flag, critical_flag;

static void __interrupt __far break_handler(void)
{
quit_flag = 1;
}
static void __interrupt __far critical_handler(union INTPACK regs)
{
quit_flag = 1;
critical_flag = 1;
/* INTPACK names the registers saved by Watcom's interrupt prologue.
* Change only saved AL; IRET restores every other caller register.
* Best effort: DOS may convert FAIL to ABORT if entry AH bit3 is clear.
* Never invent an IGNORE result or retry potentially destructive I/O. */
regs.h.al = 3;
}
int ltguard_open(void)
{
if (opened) return LTGUARD_BUSY;
if (_osmajor < 3) return LTGUARD_DOS_TOO_OLD;
previous23 = _dos_getvect(0x23);
previous24 = _dos_getvect(0x24);
quit_flag = 0;
critical_flag = 0;
_dos_setvect(0x23, break_handler);
_dos_setvect(0x24, (ltguard_vector)critical_handler);
opened = 1;
return LTGUARD_OK;
}
unsigned ltguard_close(void)
{
unsigned result = 0;
if (!opened) return 0;
if (_dos_getvect(0x23) == break_handler)
_dos_setvect(0x23, previous23);
else result |= LTGUARD_LOST_BREAK;
if (_dos_getvect(0x24) == (ltguard_vector)critical_handler)
_dos_setvect(0x24, previous24);
else result |= LTGUARD_LOST_CRITICAL;
opened = 0;
return result;
}
unsigned ltguard_quit(void) { return quit_flag; }
unsigned ltguard_critical(void) { return critical_flag; }
126 changes: 100 additions & 26 deletions adapters/lt_tick_dos.c
Original file line number Diff line number Diff line change
@@ -1,30 +1,40 @@
/* SPDX-License-Identifier: MIT */
/* Fixed-rate IRQ0 update clock; see lt_tick_dos.h. Small or compact model:
* the private stack is in DGROUP so near pointers to the callback's locals
* stay valid whatever stack the interrupt arrived on. */
#if !defined(__WATCOMC__) || !defined(__I86__)
#error lt_tick_dos.c requires Open Watcom 16-bit x86
/* Fixed-rate IRQ0 update clock; see lt_tick_dos.h. Explicit near state
* keeps the private stack and assembly operands in DGROUP in every tested
* memory model, including far-data builds with -zt=0. Compile with -s. */
#if !defined(__WATCOMC__) || !defined(__I86__) || !defined(__DOS__)
#error lt_tick_dos.c requires Open Watcom 16-bit DOS
#endif
#if !defined(__SW_S) || !__SW_S
#error Compile lt_tick_dos.c and all interrupt-reachable C with -s
#endif
#include <conio.h>
#include <dos.h>
#include "lt_tick_dos.h"

typedef void (__interrupt __far *lttk_vector)(void);

static lttk_vector previous;
static lttk_work work_fn;
static void *work_ctx;
static unsigned divisor_now;
static volatile unsigned long tick_count;
static unsigned long bios_acc;
static volatile unsigned waiting, in_work, defer_depth;
static volatile unsigned char running;
static unsigned char is_open;
static unsigned backlog_peak;
static unsigned long late_count;
static unsigned saved_ss, saved_sp, private_sp;
static unsigned char private_stack[LTTK_STACK_BYTES];
static void (*drain_ptr)(void);
/* Keep one object per declaration: __near applies per declarator, not
* to every name in a comma-separated declaration in far-data models. */
static lttk_vector __near previous;
static lttk_work __near work_fn;
static void * __near work_ctx;
static unsigned __near divisor_now;
static volatile unsigned long __near tick_count;
static unsigned long __near bios_acc;
static volatile unsigned __near waiting;
static volatile unsigned __near in_work;
static volatile unsigned __near defer_depth;
static volatile unsigned char __near running;
static unsigned char __near is_open;
static volatile unsigned __near backlog_peak;
static volatile unsigned __near fault_flags;
static volatile unsigned long __near late_count;
static unsigned __near saved_ss;
static unsigned __near saved_sp;
static unsigned __near private_sp;
static unsigned char __near private_stack[LTTK_STACK_BYTES];
static void (__near * __near drain_ptr)(void);

static unsigned lttk_cli(void)
{
Expand All @@ -45,17 +55,28 @@ static void lttk_sti(unsigned f)
}
}

/* On the private stack with IF=1; returns with IF=0 when nothing waits, so
* a tick arriving after the last check is never left behind. */
static void lttk_drain(void)
/* Called with interrupts disabled. A fault is sticky until close/open;
* never silently wrap pending work or nesting into an apparently idle state. */
static void __near lttk_fault(unsigned flag)
{
for (;;) {
fault_flags |= flag;
running = 0;
waiting = 0;
}

/* On the private stack with IF=1; always return with IF=0 before restoring
* SS:SP. A bounded drain retains excess debt for the next timer interrupt. */
static void __near lttk_drain(void)
{
unsigned serviced;
for (serviced = 0; serviced < LTTK_DRAIN_BUDGET; ++serviced) {
_disable();
if (!waiting || defer_depth) return;
if (!waiting || defer_depth || fault_flags) return;
--waiting;
_enable();
if (running) work_fn(work_ctx);
}
_disable();
}

static void __interrupt __far lttk_isr(void)
Expand All @@ -68,6 +89,11 @@ static void __interrupt __far lttk_isr(void)
} else {
outp(0x20, 0x20);
}
if (fault_flags) return;
if (waiting == 65535U) {
lttk_fault(LTTK_FAULT_PENDING);
return;
}
++waiting;
if (in_work) { /* the outer interrupt will run it */
++late_count;
Expand All @@ -86,6 +112,9 @@ static void __interrupt __far lttk_isr(void)
sti
call word ptr drain_ptr
cli
/* Far-data callbacks may return with DS addressing arbitrary data. */
mov ax, ss
mov ds, ax
mov ss, saved_ss
mov sp, saved_sp
}
Expand All @@ -108,6 +137,13 @@ lttk_status lttk_open(unsigned divisor, lttk_work work, void *context)
running = 0;
backlog_peak = 0;
late_count = 0;
fault_flags = 0;
#ifdef LTTK_TEST
{
unsigned i;
for (i = 0; i < LTTK_STACK_BYTES; ++i) private_stack[i] = 0xa5U;
}
#endif
private_sp = (unsigned)(private_stack + LTTK_STACK_BYTES - 2U);
drain_ptr = lttk_drain;
f = lttk_cli();
Expand Down Expand Up @@ -141,6 +177,7 @@ lttk_status lttk_close(void)
void lttk_run(int on)
{
unsigned f = lttk_cli();
if (on && fault_flags) { lttk_sti(f); return; }
if (on && !running) waiting = 0;
running = (unsigned char)(on != 0);
lttk_sti(f);
Expand All @@ -149,7 +186,10 @@ void lttk_run(int on)
void lttk_defer(int on)
{
unsigned f = lttk_cli();
if (on) ++defer_depth;
if (on) {
if (defer_depth == 65535U) lttk_fault(LTTK_FAULT_DEFER);
else ++defer_depth;
}
else if (defer_depth) --defer_depth;
lttk_sti(f);
}
Expand Down Expand Up @@ -193,5 +233,39 @@ unsigned lttk_backlog_max(void)

unsigned long lttk_late(void)
{
return late_count;
unsigned f = lttk_cli();
unsigned long result = late_count;
lttk_sti(f);
return result;
}

unsigned lttk_faults(void)
{
return fault_flags;
}

#ifdef LTTK_TEST
/* Boundary injection and observed stack use, compiled only for native tests. */
void lttk_test_seed(unsigned pending, unsigned depth)
{
unsigned f = lttk_cli();
waiting = pending;
defer_depth = depth;
lttk_sti(f);
}
unsigned lttk_test_pending(void) { return waiting; }
unsigned lttk_test_stack_used(void)
{
unsigned f = lttk_cli(), i = 0;
while (i < LTTK_STACK_BYTES && private_stack[i] == 0xa5U) ++i;
lttk_sti(f);
return LTTK_STACK_BYTES - i;
}
unsigned lttk_test_stack_guard(void)
{
unsigned f = lttk_cli(), i, ok = 1;
for (i = 0; i < 16U; ++i) if (private_stack[i] != 0xa5U) ok = 0;
lttk_sti(f);
return ok;
}
#endif
Loading
Loading