From d66a65bd23bf83e0f2bd9d85444eee2df46aac48 Mon Sep 17 00:00:00 2001 From: Chad Doebelin Date: Mon, 2 Nov 2026 17:37:54 -0600 Subject: [PATCH] Improve 8088 timer ABI, packed graphics and optional DOS/audio adapters --- .github/workflows/portable-midi.yml | 2 + README.md | 32 +++- SOURCE.json | 3 +- VALIDATION.md | 6 + adapters/lt_guard_dos.c | 60 ++++++++ adapters/lt_tick_dos.c | 126 ++++++++++++---- docs/AY-FRAMES.md | 128 ++++++++++++++++ docs/DOS-GUARD.md | 30 ++++ docs/GFX-FAR-QUALIFICATION.md | 43 ++++++ docs/GFX-FILL.md | 14 ++ docs/IMPROVEMENTS-VALIDATION.md | 78 ++++++++++ docs/TILE8.md | 79 ++++++++++ docs/TIMER-QUALIFICATION.md | 98 ++++++++++++ include/lt_ay.h | 65 ++++++++ include/lt_gfx.h | 7 +- include/lt_gfx_tile8.h | 17 +++ include/lt_guard_dos.h | 32 ++++ include/lt_tick_dos.h | 26 +++- src/lt_ay.c | 160 ++++++++++++++++++++ src/lt_gfx.c | 19 ++- src/lt_gfx_tile8.c | 65 ++++++++ tests/ay_core.c | 224 ++++++++++++++++++++++++++++ tests/event_core.c | 2 +- tests/gfx_far_dos.c | 100 +++++++++++++ tests/gfx_far_matrix.py | 75 ++++++++++ tests/gfx_fill.c | 49 ++++++ tests/gfx_fill_bench.c | 26 ++++ tests/gfx_tile8.c | 58 +++++++ tests/guard_dos.c | 75 ++++++++++ tests/tick_dos.c | 117 +++++++++++++++ tests/tick_matrix.py | 89 +++++++++++ tests/tile8_bench.c | 38 +++++ tools/build.py | 31 +++- tools/check_sms_package.py | 25 +++- tools/qualify_dos.py | 111 ++++++++++++++ tools/qualify_guard.py | 87 +++++++++++ 36 files changed, 2149 insertions(+), 48 deletions(-) create mode 100644 adapters/lt_guard_dos.c create mode 100644 docs/AY-FRAMES.md create mode 100644 docs/DOS-GUARD.md create mode 100644 docs/GFX-FAR-QUALIFICATION.md create mode 100644 docs/GFX-FILL.md create mode 100644 docs/IMPROVEMENTS-VALIDATION.md create mode 100644 docs/TILE8.md create mode 100644 docs/TIMER-QUALIFICATION.md create mode 100644 include/lt_ay.h create mode 100644 include/lt_gfx_tile8.h create mode 100644 include/lt_guard_dos.h create mode 100644 src/lt_ay.c create mode 100644 src/lt_gfx_tile8.c create mode 100644 tests/ay_core.c create mode 100644 tests/gfx_far_dos.c create mode 100644 tests/gfx_far_matrix.py create mode 100644 tests/gfx_fill.c create mode 100644 tests/gfx_fill_bench.c create mode 100644 tests/gfx_tile8.c create mode 100644 tests/guard_dos.c create mode 100644 tests/tick_dos.c create mode 100644 tests/tick_matrix.py create mode 100644 tests/tile8_bench.c create mode 100644 tools/qualify_dos.py create mode 100644 tools/qualify_guard.py diff --git a/.github/workflows/portable-midi.yml b/.github/workflows/portable-midi.yml index b286ef1..9650989 100644 --- a/.github/workflows/portable-midi.yml +++ b/.github/workflows/portable-midi.yml @@ -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 diff --git a/README.md b/README.md index 49566c6..9499b5d 100644 --- a/README.md +++ b/README.md @@ -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 | @@ -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). | @@ -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. diff --git a/SOURCE.json b/SOURCE.json index b93faf6..b24db30 100644 --- a/SOURCE.json +++ b/SOURCE.json @@ -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." } diff --git a/VALIDATION.md b/VALIDATION.md index 297d9d6..e7626d3 100644 --- a/VALIDATION.md +++ b/VALIDATION.md @@ -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`. diff --git a/adapters/lt_guard_dos.c b/adapters/lt_guard_dos.c new file mode 100644 index 0000000..44867e1 --- /dev/null +++ b/adapters/lt_guard_dos.c @@ -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 +#include +#include + +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; } diff --git a/adapters/lt_tick_dos.c b/adapters/lt_tick_dos.c index 7672aad..629b0c2 100644 --- a/adapters/lt_tick_dos.c +++ b/adapters/lt_tick_dos.c @@ -1,9 +1,12 @@ /* 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 #include @@ -11,20 +14,27 @@ 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) { @@ -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) @@ -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; @@ -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 } @@ -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(); @@ -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); @@ -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); } @@ -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 diff --git a/docs/AY-FRAMES.md b/docs/AY-FRAMES.md new file mode 100644 index 0000000..7fbfaf0 --- /dev/null +++ b/docs/AY-FRAMES.md @@ -0,0 +1,128 @@ +# Optional AY control-frame adapter + +`lt_ay.h` / `lt_ay.c` adapt an already decoded AY-3-8910-style control frame to +libtandy. This is useful to a register-based music/sound producer without +requiring a particular source machine, file format, tracker, or transport. +It is **not an AY emulator** or a promise of equivalent sound. + +The adapter is optional. Link `lt_ay.c`, `lt_audio.c`, and `lt_chip.c`; add the +existing SFX module only if the application uses clips. It does not change +instrument banks, install a clock, perform I/O, allocate memory, parse files, +or contain any song assets. Call frequency belongs to the application. + +## Two layers, no new lifecycle + +1. `lt_ay_map(config, frame, plan)` is a pure, bounded transformation. It can + be used without a live chip. Invalid or unsupported inputs leave the plan + unchanged. Keep the three objects non-overlapping. +2. `lt_ay_apply(audio, plan)` applies that plan through an existing + `lta_handle`. The handle must own all four channels and this must be its + only base-music writer. Existing SFX overlays, master mute, per-channel + attenuation and music-layer pause continue to work. Changes while an + effect borrows channels update the base state restored when it ends. + The plan must not overlap the handle. Attenuation controls address Tandy + physical channels; resource remapping can change which AY voice uses one. + +The adapter acquires/releases nothing. Use the existing `lta_init`, ACQUIRE, +MUTE, STOP, RELEASE and layer APIs. All calls sharing a chip remain serialized; +foreground work must be deferred/stopped around an interrupt-owned renderer. +Armed auto-stop deadlines conflict with frame-driven music and return BUSY. +Repeated identical plans cause no writes; level-only changes do not rewrite +noise control and therefore do not spuriously restart its generator. +Rejected mapping leaves the previous sound untouched; an application may +explicitly mute/stop on error rather than continuing its previous plan. + +An `lt_dos_app` initially owns low-level chip claims. Hand them off explicitly +before acquiring an `lta_handle`: `lt_stop(&app.chip, +LT_DOS_APP_SOUND_OWNER, LT_DOS_APP_SOUND_MASK)`, then initialize/acquire the +handle. **`lta_acquire` rejects already-owned channels even with the same +token**, unlike the low-level `lt_claim` function. + +```c +lt_ay_config config; +lt_ay_frame frame; +lt_ay_plan plan; +/* audio is already initialized and owns mask 15. */ +lt_ay_config_init(&config); +/* Set period_scale = target PSG clock / (2 * source AY clock). + * The default 1/1 is only correct for a matching clock relationship. */ +config.scale_num = 1; +config.scale_den = 1; +/* Fill frame.reg[0..13], envelope_valid and envelope_level. */ +if (lt_ay_map(&config, &frame, &plan) == LTA_OK) { + /* Inspect plan.losses as appropriate for the application. */ + if (lt_ay_apply(&audio, &plan) != LTA_OK) { /* handle error */ } +} +``` + +Both ratio terms accept 1..65535. Supply a reduced rational or a suitable +approximation if exact clock integers do not fit. Conversion rounds to the +nearest nonzero period. Arithmetic uses at most 32 bits; the maximum product +is `4095 * 65535`. Unity scaling has a fast path. No actual clock detection, +rate conversion, resampling or schedule interpretation occurs. + +## Deliberate approximation policies + +- **Envelopes:** if any volume register enables its envelope, the caller must + provide `envelope_valid=1` and the current resolved AY DAC level 0..15. + Otherwise mapping returns `LTA_UNSUPPORTED`, with no output mutation. + R11–R13 are not interpreted. All shapes therefore use the same explicit + caller-level contract; there is no tune-specific shape-8/RMS shortcut. + Sampling levels at 50/60 Hz cannot reproduce fast hardware-envelope timbre. + A voice with both tone and noise disabled is silent in the plan, including + envelope-only waveforms; the latter sets `LTAY_LOSS_ENVELOPE_ONLY`. +- **Levels:** fixed AY DAC levels are rounded to SN 2 dB attenuation steps. + Quiet nonzero levels saturate at the SN floor; only AY level zero maps + directly to silence. This is an AY DAC approximation, not a YM2149 profile. +- **Range:** select octave folding, clamping to 1023, or muting for periods + outside the Tandy tone range. Folding is the default and preserves pitch + class, not octave. The maximum input requires at most 19 halvings. +- **Noise:** optional coupled noise uses a silent tone 2 as its clock when + no more than two tones are audible and the scaled noise divider fits. + Otherwise the nearest logarithmic fixed divider 16/32/64 is selected. + AY and Tandy noise feedback circuits differ even when rates match. +- **Mixing:** AY gates tone/noise per voice; Tandy has one additive noise + output. A mixed voice is split into tone/noise at 6 dB attenuation each, + and shared noise takes the loudest requested level. This changes timbre + and relative levels; it is not a reconstruction of the AY gate waveform. +- **Pulse bass:** disabled by default. `LTAY_PERIODIC_BASS` assigns one low + voice to Tandy NCR's 15-stage periodic noise when noise is otherwise unused. + Tone 2 becomes a silent clock and the other voices use tone 0/1. The narrow + pulse waveform, nearest-multiple-of-15 tuning, and up-to-6 dB compensation + differ from AY square waves. Do not use this profile for an incompatible + PSG variant with a different periodic-noise ring length. + +`plan.losses` reports volume, envelope, mixer, noise, fixed-noise-rate, range, +pulse-bass and envelope-only compromises. These flags help diagnostics; a +zero mask is **not** a certification of analog or phase equivalence. Integer +period rounding and the chosen clock-ratio approximation also affect tuning. +Tone slots may change when a clock is reserved. Tone 2 is muted before +becoming a noise clock, and noise is muted before a mode transition. + +Reference basis: [TI SN76489AN data sheet](https://map.grauw.nl/resources/sound/texas_instruments_sn76489an.pdf), +[AY-3-8910/8912 original manual](https://pub.intvprime.com/ba4ef/ie/Programming/AY-3-8910-8912-Programmable-Sound-Generator-Data-Manual.pdf), +[Ayumi's AY DAC reference values](https://github.com/true-grue/ayumi/blob/master/ayumi.c), +and the [DOSBox-X NCR/SN implementation](https://dosbox-x.com/doxygen/html/sn76496_8cpp_source.html). +The attenuation table is a small derived integer lookup, not a bundled +third-party synthesizer or recording. + +## Verification + +`tests/ay_core.c` uses synthetic register frames only. It covers all 4,096 AY +periods and all three range policies; mixer, envelope/volume, noise and policy +combinations; extreme clock ratios; unchanged output on rejected mapping; +ownership/deadline rejection; byte-stream round trips; repeat-write caching; +mute, attenuation, layer pause and tone2/noise SFX restoration. + +Strict C89 host, ASan/UBSan and native Open Watcom 8088 runs agree on the +synthetic emitted-byte hash `85731ebb` (48,309 writes; maximum observed 12 per +apply). Configuration/frame/plan sizes are 6/16/14 bytes on the validated host +and 16-bit models. No persistent player state is added beyond the existing +handle. With Open Watcom `-0 -ml -ecc -ox -s -zt=8192`, the adapter adds +2,089 code bytes and a 16-byte constant lookup; existing audio/chip code is +excluded. Tests use no original demo assets and execute no foreign binaries. + +These checks establish functional behavior, not real-hardware audibility, +chip-write spacing, analog level matching, full interrupt-stack high-water, +or a guaranteed frame-time budget. The native test's accelerated emulator +setting is not a calibrated 8088 performance measurement. diff --git a/docs/DOS-GUARD.md b/docs/DOS-GUARD.md new file mode 100644 index 0000000..679e44b --- /dev/null +++ b/docs/DOS-GUARD.md @@ -0,0 +1,30 @@ +# Optional orderly DOS exit guards + +`lt_guard_dos.h` and `adapters/lt_guard_dos.c` are opt-in Open Watcom 16-bit DOS3+ services. They do not run at startup and do not own the graphics, timer, PSG or atexit lifecycle. + +```c +if (ltguard_open() != LTGUARD_OK) return 1; +/* Acquire application resources, then poll ltguard_quit() in foreground. */ +/* On quit/error: stop IRQ work, mute/release audio, restore video and vectors. */ +lost_vectors = ltguard_close(); +``` + +Compile **only the adapter** with `-s -zu`, and use the same memory model/calling convention as its callers. The adapter rejects compilation without those flags. `-zu` is essential because DOS may invoke INT24 with SS different from DGROUP; `-s` prevents unsafe stack-check runtime calls from the interrupt. Ordinary callers need not use `-zu`. + +INT23 latches a quit flag. INT24 latches quit/critical flags and changes only saved AL to 3 (**Fail**). **This is best effort:** if entry AH bit 3 says Fail is not permitted, DOS may convert that response to Abort and bypass foreground cleanup. The adapter does not fake a successful Ignore or repeatedly retry unsafe I/O. See the [DOS INT24 response contract](https://fd.lod.bz/rbil/interrup/dos_kernel/24.html). Neither handler calls DOS, allocates, prints, retries or exits. A failed I/O operation must still be handled normally by its caller. Flags persist after close; only successful open clears them. DOS before3 is rejected before vectors change. + +Only one process-local owner is supported. Open while active returns BUSY. Close restores only vectors still pointing to this adapter; it reports a bitmask and leaves any later foreign handler installed. Repeated close is harmless. A later handler which chains to this module still requires this code to remain resident; the guard is not a TSR manager and cannot remove itself from a foreign chain. Restore application resources before closing it. This is not crash recovery, NMI handling, power-failure protection, or a guarantee every DOS break/error returns to the main loop. + +The direct INT24 handler deliberately does **not** call `_harderr`: replacing the CRT callback and restoring only the interrupt vector would leave the process-global CRT policy changed. + +## Qualification + +Run with installed tools (nothing is downloaded): + +``` +python tools/qualify_guard.py --watcom /path/to/watcom --dosbox /path/to/dosbox-x +``` + +Open Watcom2026-10-01, `-0 -ecc -ox -s`, adapter additionally `-zu -zt=0`, passed55 native checks in **small, compact, medium and large** models. Tests exercised synthetic INT23/INT24, saved AX/BX/CX/DX/SI/DI, a separately allocated foreign SS, existing `_harderr` callback restoration, all foreign-vector combinations, repeated open/close, busy and simulated DOS2 rejection. Missing `-s` and missing `-zu` builds were rejected. The tests restore the original vectors. + +DOSBox-X2025.02.01, Tandy640KB, `8086_prefetch`, normal core, fixed20,000cycles. These are ABI/behavior checks, not physical EX timing. No actual disk/device failure was injected; synthetic INT24 tests cannot validate DOS post-handler Fail-to-Abort behavior. Logs and command/hash manifest are written under `build/guard-matrix/`. diff --git a/docs/GFX-FAR-QUALIFICATION.md b/docs/GFX-FAR-QUALIFICATION.md new file mode 100644 index 0000000..8bd4054 --- /dev/null +++ b/docs/GFX-FAR-QUALIFICATION.md @@ -0,0 +1,43 @@ +# Far-pointer boundary qualification + +`tests/gfx_far_dos.c` tests graphics in allocated DOS RAM, without switching +video modes or touching video memory. It allocates three separate 64-KiB +blocks (source, destination, independent expected image). The test deliberately +uses non-normalized pointers at the highest valid offsets: + +* Packed 32-byte tile at `segment:FFE0` and odd `segment:FFDF`. +* Full 32,768-byte surface at `segment:8000` and odd `segment:7FFF`. +* Fast aligned tiles, unaligned rows/columns, clipped edges, wholly rejected + extreme coordinates, bottom-right pixels, full clears, and odd/even spans. + +Every destination comparison covers all 65,536 bytes, so unused bank padding, +pixels outside a primitive, and all available before/after guards are checked. +The entire source block is checked for mutation. At `FFE0`/`8000` there is no +post-buffer guard inside the segment; the window ends exactly at `FFFF`. +The odd-offset cases leave one post-buffer guard byte. This does not authorize +pointers whose buffers cross a 64-KiB offset wrap; those remain out of contract. + +Reproduce using installed Linux-host Open Watcom and DOSBox-X (no downloads): + +``` +python3 tests/gfx_far_matrix.py --watcom /path/to/watcom \ + --dosbox /path/to/dosbox-x +``` + +An environment-setting executable wrapper may replace the DOSBox-X path. +The runner compiles small, compact, medium and large model executables, with +`-0 -ecc -ox -s -zt=0 -w4 -we`. Each model is tested twice: the native tile +assembly/CRT fill implementation and forced portable C using +`LT_GFX_TILE8_C_ONLY` plus `LT_GFX_C_FILL`. Each executable uses the independent +per-pixel expected-image model rather than comparing the two implementations +only to one another. + +Results and source/executable hashes are written under `build/gfx-far-matrix`. +The emulator configuration is Tandy, 640 KiB, no XMS/EMS, `8086_prefetch`, fixed +20,000 cycles. These checks qualify pointer-window arithmetic and byte results, +not physical EX performance, actual video-memory timing, or arbitrary aliasing. + +Recorded 2026-11-02 with Open Watcom 2.0 beta 2026-10-01 and DOSBox-X: +**all eight executables passed 64 cases each, zero failures** (512 total +case executions). Data pointers were 2 bytes in small/medium and 4 bytes in +compact/large; framebuffer/source pointers are explicitly far in every model. diff --git a/docs/GFX-FILL.md b/docs/GFX-FILL.md new file mode 100644 index 0000000..f9754a8 --- /dev/null +++ b/docs/GFX-FILL.md @@ -0,0 +1,14 @@ +# Packed span and clear acceleration + +On Open Watcom16-bit, `lt_gfx_clear()` and the byte-aligned middle of spans now use the compiler runtime's `_fmemset` far-byte fill. Odd left/right nibbles and clipping retain their original behavior. Short spans under8 complete bytes retain the inline C loop to avoid setup overhead. `-dLT_GFX_C_FILL` forces the portable baseline for comparisons. No new API, cache, heap, mutable global state, or hardware instruction is required by callers. + +`tests/gfx_fill.c` compares the entire32KiB surface plus guards against an independent per-pixel rectangle oracle: all256 input clear colors,128 mixed-edge rectangles and9 extreme/clipped/empty cases. All393 cases pass strict hostC89, ASan/UBSan, and native8088 small/compact/large models. Existing graphics/line/clip tests remain part of `tools/build.py`. + +`tests/gfx_fill_bench.c` is a RAM benchmark; compile once normally and once with `-dLT_GFX_C_FILL`. At DOSBox-X2025.02.01 fixed20,000cycles,100 operations: + +| Operation | C baseline | Far fill | +|---|---:|---:| +| Full32KiB clear |1153ms|54ms| +| Clipped filled rectangle|1538ms|275ms| + +Both produce framebuffer FNV`803b1dc5`. DOS `clock()` reports milliseconds but has roughly55ms granularity; the54ms measurement is near one clock tick. These figures show a large reduction in loop overhead, **not** calibrated8088 speed, VRAM-contention behavior or physical EX frame rate. Raw logs are under `build/fill/`. diff --git a/docs/IMPROVEMENTS-VALIDATION.md b/docs/IMPROVEMENTS-VALIDATION.md new file mode 100644 index 0000000..9936dc0 --- /dev/null +++ b/docs/IMPROVEMENTS-VALIDATION.md @@ -0,0 +1,78 @@ +# General-purpose 8088 improvements: qualification + +Base: `94f4f4fb2e42c60363eb0737547c294cc5de053c`; tested 2026-11-02. +All additions are optional MIT library code, synthetic tests and documentation. +No new application assets, music, executable downloads or source-machine +conversion framework are included. + +## Verified changes + +- IRQ0 supports Watcom small, compact, medium and large models. Explicit + near assembly operands/private drain ABI and DS restoration after far-data + callbacks fix actual model failures. Eight-call drain budgeting retains + debt; checked overflow stops work with sticky faults. All four models pass + 2,028 native checks at fixed 20,000 and 720 cycles. +- Optional INT23/24 guards preserve prior CRT policy and foreign vector + owners. All four models pass 55 native checks, including foreign SS, + saved registers, close/reopen and simulated pre-DOS3 rejection. These are + **best effort**: DOS can turn disallowed FAIL into ABORT; no actual device + failure was injected and foreground cleanup cannot always be guaranteed. +- Opaque packed tile8: 10,224 cases pass independent framebuffer/guard + comparisons in small/compact/large, optimized and forced-C builds. + The tested RAM-copy loop is about 74% faster than the generic packed blit + in two emulator profiles, not a whole-scene or physical-hardware claim. +- Far fill: all 393 independent full-buffer clear/span/edge cases pass + native small/compact/large and host sanitizers. +- Far-pointer limits: all four models and both optimized/portable kernels + pass 64 cases each at source FFE0/FFDF and surface 8000/7FFF, with complete + 64 KiB comparisons. Surface and source window requirements are explicit. +- AY control-frame adapter: 622,802 checks, host/native hash `85731ebb`. + Explicit clock/range/loss policies, caller-resolved envelopes, existing + mute/attenuation/SFX ownership preserved. This is not waveform emulation. + +The candidate-only checkout also passes strict C89 host checks, the existing +portable MIDI suite, and 20 noninteractive DOS programs in small and large +models. The near-pointer assertion in the event test is explicitly `__near` +rather than incorrectly assuming every model's implicit pointer is near. +Native timer, guard and far-boundary matrices use separate runners so their +special flags/hooks do not enter normal builds. + +## Reproduce and interpret + +Use installed Open Watcom and DOSBox-X; the scripts download nothing: + +``` +python tools/build.py --target host +python sound/tests/run.py --cc gcc +python tools/build.py --target dos --watcom /path/to/watcom +python tools/qualify_dos.py --dosbox /path/to/dosbox-x +python tools/build.py --target dos --model large --watcom /path/to/watcom +python tools/qualify_dos.py --model large --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 +``` + +Observed with Open Watcom 2.0 beta 2026-10-01 and DOSBox-X 2025.02.01, +Tandy 640 KiB, normal `8086_prefetch`, no XMS/EMS/UMB. Fixed-cycle profiles +are **not** MHz equivalents or physical Tandy EX certification. No physical +sound/listening, VRAM contention, arbitrary callback stack or BIOS/TSR safety +claim is made. Native runners write command/hash manifests and logs under +`build/`; host execution alone does not qualify 16-bit ABI behavior. + +Detailed contracts/results: [timer](TIMER-QUALIFICATION.md), +[guard](DOS-GUARD.md), [tile8](TILE8.md), [fill](GFX-FILL.md), +[far boundaries](GFX-FAR-QUALIFICATION.md), [AY frames](AY-FRAMES.md). + +## Existing downloadable example provenance + +No existing download is rebuilt or replaced by this library change. The SMS +package's existing manifest pins its qualified source to commit +`3184232cdcb05fd50c9d70eac29a7328d207f96f`. Its checker now verifies that +immutable commit's source hashes, plus the unchanged ZIP and native EXE +receipts, instead of misleadingly requiring the evolving library to remain +byte-identical to an old build. It reports current-source differences. +A full Git history (or the pinned commit fetched locally) is required for +this offline check; CI checks out history. No stored hash or native receipt +is relaxed, rewritten or relabeled as current. Existing engine package +checks remain unchanged and pass. diff --git a/docs/TILE8.md b/docs/TILE8.md new file mode 100644 index 0000000..b890ea6 --- /dev/null +++ b/docs/TILE8.md @@ -0,0 +1,79 @@ +# Packed 8x8 tiles + +`include/lt_gfx_tile8.h` / `src/lt_gfx_tile8.c` add a small optional primitive +for independently positioned opaque tiles. They do not introduce a tile engine, +emulated device, framebuffer cache, allocation, interrupt, or hardware owner. + +```c +#include "lt_gfx_tile8.h" +/* 32 caller-owned FAR bytes: eight rows, four packed RGBI bytes each. */ +lt_gfx_tile8(&surface, x, y, packed_tile); +``` + +The left pixel is the high nibble. Every color, including zero, is opaque. +Signed-16 coordinates clip safely, including extremes. Null arguments return +zero; valid arguments return one even for a wholly clipped tile. Source and +surface descriptor must not alias destination storage. Source must fit one +valid 32-byte far-pointer window; destination has the normal complete 32 KiB +mode-9 surface contract. + +## Target optimization and choices + +For an in-bounds tile at an even x and y divisible by four, Open Watcom's +8088 build uses sixteen `MOVSW` instructions across the four video banks. +Other positions use portable clipped pixel composition. Define +`LT_GFX_TILE8_C_ONLY` to disable the assembly path without changing behavior. +The kernel preserves DS/ES and follows the C ABI's clear direction flag. +Generated listings and native runs verify Watcom's nonuniform register-pair +allocation; the source pointer is segment DX / offset SI, but destination is +offset AX / segment DI. This is not guessed from the pragma's textual order. + +Prefer the existing `lt_gfx_blit_packed` for larger contiguous rectangles or +batched rows. Tile8 is not a universal replacement for that API. A framebuffer +or artwork cache belongs to the caller. No automatic sprite transparency or +background restoration is supplied; `lt_gfx_sprite8_planes` remains the separate +2-bitplane transparent-sprite operation. + +## Evidence (2026-11-02) + +`tests/gfx_tile8.c` passes 10,224 draws on strict C89 host and ASan/UBSan builds +and in native DOS small, compact and large memory models. Both the target ASM +and forced-C builds pass. Tests cover every aligned fast-path start, all source +byte values, all destination parity/bank phases, signed extremes, clipping, +opaque-zero behavior, null arguments, and source/framebuffer guards. The oracle +uses independent long-coordinate pixel addressing rather than the copy kernel. + +The target flags used for the focused matrix are +`-0 -ms|-mc|-ml -ecc -bt=dos -ox -s -zt=0 -w4 -we -k8192`. +The usual aggregate builds also run the test with their `-os` settings. + +`tests/tile8_bench.c` compares fixed in-RAM aligned 8x8 copies against the +existing generic packed blit. It resets the buffer and verifies the same final +FNV hash (`dcf7e385`) for both methods. DOSBox-X 2025.02.01, Tandy/640 KiB, +normal core / 8086_prefetch, native large model: + +| Fixed cycles | Copies | Packed blit ms | Tile8 ms | Reduction | +|---:|---:|---:|---:|---:| +| 20,000 | 200,000 | 5,163 | 1,318 | 74.5% | +| 720 | 20,000 | 14,610 | 3,845 | 73.7% | + +These are elapsed DOS `clock()` measurements with roughly 55 ms granularity. +They are an instruction-profile-relative **RAM-buffer** comparison, not a +physical Tandy EX frame rate, VRAM-contention benchmark, or guarantee for +unaligned tiles. The source has no private data/table allocation; the measured +large-model `-ox` routine is 424 bytes. Compiler flags/models may change that. + +## Reproduce + +The normal `tools/build.py` host/DOS suites include `tile8`; DOS also builds +`TILEBEN.EXE`. Run `tools/qualify_dos.py --program tile8` against an installed +DOSBox-X. Use `--model large` consistently on both build and qualification to +check that model. For the forced-C comparison compile the same source/test +pair with `-dLT_GFX_TILE8_C_ONLY`; no other source changes are needed. Benchmark +`LT_TILE8_BENCH_FRAMES` defaults to 200; define it as 20 for the low-cycle run. + +The new primitive is independently authored generic code. It includes no +application-specific effects, music, graphics or conversion tools. + +High/odd far offsets and the framebuffer window contract are additionally +qualified in [GFX-FAR-QUALIFICATION.md](GFX-FAR-QUALIFICATION.md). diff --git a/docs/TIMER-QUALIFICATION.md b/docs/TIMER-QUALIFICATION.md new file mode 100644 index 0000000..e098f3d --- /dev/null +++ b/docs/TIMER-QUALIFICATION.md @@ -0,0 +1,98 @@ +# DOS timer ABI and overload qualification + +The IRQ0 adapter now supports Open Watcom's 16-bit **small, compact, medium +and large** memory models, including far-data builds with `-zt=0`. Match the +model and calling convention across the adapter, application, and callback +call graph. This change does not add a second timer or an audio driver. + +## Correctness changes + +* The ISR switches to a 3,072-byte explicitly near DGROUP stack before calling + C work. Its indirect drain target is explicitly a **near function pointer** + to a near function; ordinary application callbacks retain their model's + near/far function-pointer ABI. +* Every assembly-visible state object is declared near separately. With + Watcom, `unsigned __near a, b` does **not** place both objects near in a + far-data model: `b` follows the default model. The initial multi-declarator + implementation failed this test and was corrected before qualification. +* Compact/large callbacks can leave DS pointing to arbitrary far data. After + the drain returns, the ISR reloads DS from the still-active private SS + **before** reading the saved SS:SP. Without this restoration, compact-model + execution failed after callbacks began. Real IRQ tests now cover it. +* Work is drained at most eight calls per outer interrupt. Remaining debt is + retained for subsequent interrupts, rather than starving the foreground + indefinitely in an unbounded drain loop. One slow callback can still stall + the foreground; this is not a scheduler or a deadline guarantee. +* Pending-work and defer-depth counters never silently wrap. At 16-bit + overflow the callback service stops, pending debt is cleared, and + `lttk_faults()` exposes sticky `LTTK_FAULT_PENDING`/`LTTK_FAULT_DEFER` bits. + BIOS/clock IRQ service continues. Close/open clears the fault; `lttk_run(1)` + alone cannot restart a faulted timer. +* The 32-bit late diagnostic is read with interrupt-state preservation; + IRQ-shared diagnostic state is volatile. + +All interrupt-reachable C must have stack probes disabled (`-s`). The adapter +rejects a compilation with probes enabled. Watcom's `-ox` also implies `-s`, +but the qualified command spells out both. Callback code must avoid DOS, +blocking I/O and unbounded work, and its complete call graph plus nested +interrupts must fit the private stack. Open/close are foreground-only. + +## Reproduce with installed tools + +No downloads or application assets are required by the runner: + +``` +python3 tests/tick_matrix.py --watcom /path/to/watcom \ + --dosbox /path/to/dosbox-x +``` + +`--dosbox` may instead name an environment-setting executable wrapper when +an emulator needs a private shared-library path. The runner supplies a +headless Tandy/640-KiB/no-XMS configuration and an isolated build directory. +Use `--build`, `--cycles`, and `--timeout` to customize it. SDL dummy support +is required for this headless invocation. Install toolchains yourself; this +runner does not install software or alter physical hardware. + +Compilation uses `-0 -ecc -bt=dos -ox -s -zt=0 -w4 -we -dLTTK_TEST -k8192` +with each of `-ms`, `-mc`, `-mm`, `-ml`. Only test builds expose the controlled +counter/canary hooks; they are absent without `LTTK_TEST`. + +The test exercises real IRQs, a separately allocated interrupted SS stack, +address-taken callback locals through a non-inlined helper, a separately +allocated far context in compact/large, nested IRQ debt, nested foreground +deferral, stopped/start behavior, eight-call budget retention, seeded +16-bit overflow boundaries, fault recovery, vector restoration, preservation +of a replacement owner's vector, and 2,000 sampled monotonic clock reads. +Test hooks only seed impractically long counter-overflow boundaries; they +do not replace IRQ entry, callback dispatch, or stack switching. + +## Recorded results (2026-11-02) + +Official installed Open Watcom 2.0 beta dated 2026-10-01; DOSBox-X's +`8086_prefetch` CPU, fixed 20,000 emulated cycles, Tandy, 640 KiB: + +| Model | Checks | Data/callback pointer bytes | Observed stack written-depth | +|---|---:|---:|---:| +| small | 2,028 PASS | 2 / 2 | 92 / 3,072 | +| compact | 2,028 PASS | 4 / 2 | 84 / 3,072 | +| medium | 2,028 PASS | 2 / 4 | 96 / 3,072 | +| large | 2,028 PASS | 4 / 4 | 98 / 3,072 | + +Each deliberately slow callback produced three late ticks; all lower stack +canaries survived. `build/tick-matrix/` holds compile logs, native logs, +expected stack-probe rejection, emulator configuration and SHA-256 source/ +executable manifest. These are native DOS ABI/correctness tests, **not** an +8088 instruction-cycle benchmark, real EX timing certification, or proof +that arbitrary callback stacks fit. Canary written-depth can undercount +untouched stack allocations; it is evidence for this test workload only. + +The monotonic clock test is sampled, not an exhaustive proof of every PIT +latch/IRQ boundary. Physical hardware, arbitrary TSR/BIOS combinations, +real audio audibility, and other compiler/model options remain unqualified. + +A separate conservative `--cycles 720 --build build/tick-matrix-720` repeat +also passed all four models (2,028 checks each, three induced late ticks). +Per-model written-depth observations are retained in the native logs. +Written-depth varies with interrupt phase; these measurements are not a +universal stack bound. +This is an emulator stress setting, not a calibrated 7.16-MHz EX equivalent. diff --git a/include/lt_ay.h b/include/lt_ay.h new file mode 100644 index 0000000..eb55bcb --- /dev/null +++ b/include/lt_ay.h @@ -0,0 +1,65 @@ +/* SPDX-License-Identifier: MIT */ +#ifndef LT_AY_H +#define LT_AY_H +#include "lt_audio.h" + +/* Optional, approximate AY control-frame adapter. Not an emulator, file + * decoder, tracker, timer, PCM renderer, or MSX-specific interface. C89. */ +#define LTAY_COUPLED_NOISE 1U +#define LTAY_PERIODIC_BASS 2U +#define LTAY_RANGE_FOLD 0U +#define LTAY_RANGE_CLAMP 1U +#define LTAY_RANGE_MUTE 2U + +#define LTAY_LOSS_VOLUME 1U +#define LTAY_LOSS_ENVELOPE 2U +#define LTAY_LOSS_MIXER 4U +#define LTAY_LOSS_NOISE 8U +#define LTAY_LOSS_FIXED_NOISE 16U +#define LTAY_LOSS_RANGE 32U +#define LTAY_LOSS_PULSE_BASS 64U +#define LTAY_LOSS_ENVELOPE_ONLY 128U + +typedef struct { + /* Tandy period / AY period = target_clock / (2 * source_clock). + * Supply a reduced positive rational; no clock detection is performed. + * Both terms are 1..65535; all intermediate products fit 32 bits. */ + lt_u16 scale_num, scale_den; + lt_u8 flags, range_policy; +} lt_ay_config; + +typedef struct { + lt_u8 reg[14]; + /* If any AY voice enables its hardware envelope, the caller must supply + * the current resolved 4-bit AY DAC level. Missing level is UNSUPPORTED. + * R11..R13 are not interpreted; no shape-8 or fixed-RMS assumption. + * A low-rate sample cannot preserve fast envelope timbre. */ + lt_u8 envelope_valid, envelope_level; +} lt_ay_frame; + +typedef struct { + /* Every tone is initialized, including silent tone2 when clocking noise. + * Attenuation: 0 loudest, 15 silent (same convention as lta_handle). */ + lt_u16 period[3]; + lt_u8 attenuation[4], noise_mode; + lt_u16 losses; +} lt_ay_plan; + +/* Unity ratio, coupled noise allowed, pulse bass disabled, octave folding. + * Initialize/configure in foreground; state is entirely caller-owned. */ +void lt_ay_config_init(lt_ay_config *config); +/* Pure bounded mapping: no allocation, I/O, ownership or mutable globals. + * Invalid/unsupported input leaves plan unchanged. Objects must not overlap. + * AY high register bits are masked; tone/noise period zero means one. + * A successful result may have loss flags: callers must not label it lossless. */ +lta_status lt_ay_map(const lt_ay_config *config, const lt_ay_frame *frame, + lt_ay_plan *plan); +/* Apply through an initialized, active lta_handle owning all four channels. + * This is the only base-music writer; existing SFX overlays may share that + * handle. Armed auto-stop deadlines are rejected with BUSY. No claims are + * acquired/released and no timer/transport is installed. Serialize like all + * lta calls. Plan and handle must not overlap. Existing mute, attenuation + * and overlay restoration are honored. + * Stop/mute/pause/release use the existing lta API, not a second lifecycle. */ +lta_status lt_ay_apply(lta_handle *audio, const lt_ay_plan *plan); +#endif diff --git a/include/lt_gfx.h b/include/lt_gfx.h index bff2af1..0a1c9b4 100644 --- a/include/lt_gfx.h +++ b/include/lt_gfx.h @@ -26,8 +26,11 @@ typedef struct { lt_gfx_u8 LT_GFX_FAR *pixels; } lt_gfx_surface; -/* The caller supplies at least LT_GFX_BYTES bytes. A NULL pointer makes the - * surface invalid; drawing operations then do nothing. Colors use low 4 bits. */ +/* The caller supplies at least LT_GFX_BYTES bytes. On 16-bit Watcom the + * whole buffer must fit in one 64 KiB FAR-pointer window (base offset <= + * 0x8000); these APIs do not normalize segment-crossing or HUGE pointers. + * A NULL pointer makes the surface invalid; drawing operations then do + * nothing. Colors use low 4 bits. */ void lt_gfx_init(lt_gfx_surface *surface, lt_gfx_u8 LT_GFX_FAR *pixels); void lt_gfx_clear(lt_gfx_surface *surface, lt_gfx_u8 color); int lt_gfx_get(const lt_gfx_surface *surface, lt_gfx_i16 x, lt_gfx_i16 y); diff --git a/include/lt_gfx_tile8.h b/include/lt_gfx_tile8.h new file mode 100644 index 0000000..46e2a6d --- /dev/null +++ b/include/lt_gfx_tile8.h @@ -0,0 +1,17 @@ +/* SPDX-License-Identifier: MIT */ +#ifndef LT_GFX_TILE8_H +#define LT_GFX_TILE8_H +#include "lt_gfx.h" + +/* Opaque 8x8 tile: 32 packed bytes, four per row, high nibble leftmost. + * RGBI index zero is opaque. Coordinates accept all signed-16 values and + * clip to the 320x200 surface. Returns 0 for null arguments, otherwise 1 + * (also for a wholly clipped tile). No heap, I/O, timer or hidden cache. + * Source is FAR: all32 bytes must fit in one 64KiB pointer window + * (16-bit Watcom offset <=0xFFE0). Odd source offsets are supported; + * it and the surface descriptor must not overlap the destination buffer. + * Aligned in-bounds tiles (even x, y divisible by four) use an optional + * audited 8088 word-copy kernel. LT_GFX_TILE8_C_ONLY selects portable C. */ +int lt_gfx_tile8(lt_gfx_surface *surface, lt_gfx_i16 x, lt_gfx_i16 y, + const lt_gfx_u8 LT_GFX_FAR *packed32); +#endif diff --git a/include/lt_guard_dos.h b/include/lt_guard_dos.h new file mode 100644 index 0000000..cd0ea93 --- /dev/null +++ b/include/lt_guard_dos.h @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: MIT */ +#ifndef LT_GUARD_DOS_H +#define LT_GUARD_DOS_H + +/* Optional foreground DOS 3+ orderly-exit service, Open Watcom 16-bit. + * Compile adapters/lt_guard_dos.c separately with -s -zu. No compiler flags + * are required on callers beyond a consistent memory model/calling convention. + * Install before owning video, timers or sound. Handlers only latch flags; + * foreground must poll quit, restore its resources, then call close. Neither + * handler calls DOS, retries an operation, allocates, prints or exits. + * One active owner per process; do not nest or call from an IRQ. */ +#define LTGUARD_OK 0 +#define LTGUARD_BUSY 1 +#define LTGUARD_DOS_TOO_OLD 2 +#define LTGUARD_LOST_BREAK 1 +#define LTGUARD_LOST_CRITICAL 2 + +/* Open clears flags and installs INT23/INT24. BUSY leaves existing state alone. + * DOS <3 is rejected before changing vectors: INT24 FAIL requires DOS 3+. */ +int ltguard_open(void); +/* Restore only vectors still owned by this service. Return a bitmask of lost + * ownership; foreign replacements remain installed. Repeated close returns 0. + * This does not restore the caller's video/PIT/PSG; do that BEFORE close. */ +unsigned ltguard_close(void); +/* Flags remain latched after close for diagnostics; a successful open clears. + * INT23 latches quit; INT24 latches both and requests AL=3 (Fail). + * If DOS disallows Fail (entry AH bit3 clear), DOS may instead Abort and + * bypass foreground cleanup. This guard cannot guarantee orderly recovery. + * A failed I/O call still requires normal caller error handling and cleanup. */ +unsigned ltguard_quit(void); +unsigned ltguard_critical(void); +#endif diff --git a/include/lt_tick_dos.h b/include/lt_tick_dos.h index 95f8d1b..1a36757 100644 --- a/include/lt_tick_dos.h +++ b/include/lt_tick_dos.h @@ -3,7 +3,11 @@ #define LT_TICK_DOS_H /* Fixed-rate update clock for one foreground DOS program (Open Watcom, - * 16-bit): the only IRQ0 (PIT channel 0) handler a game installs. + * 16-bit small, compact, medium or large model): the only IRQ0 (PIT channel 0) + * handler a game installs. Use the same model/calling convention throughout. + * Compile this adapter and ALL interrupt-reachable C with -s (no runtime + * stack probes). IRQ state/stack are explicitly near; -zt=0 is supported. + * User callback stack use, including nested interrupts, must fit 3 KiB. * * Rate: PIT channel 0 runs in mode 2 with the given divisor, so updates come * at 1,193,182 / divisor Hz exactly (LTTK_DIVISOR_60HZ: 60.0001 Hz; @@ -19,8 +23,11 @@ * Work: the callback runs once per tick, inside the interrupt, on a private * 3 KiB stack in DGROUP with interrupts enabled. If a tick arrives while the * callback is still running it is counted and run right afterwards by the - * outer interrupt, in order, so updates are never lost or reordered, only - * delayed. Put game updates (input sampling, logic, lt_sfx_tick/lt_score + * outer interrupt, in order. Each interrupt drains at most LTTK_DRAIN_BUDGET + * callbacks; remaining work is retained until a later IRQ. A callback that + * continually exceeds its period is not made realtime by this queue. Pending + * count or defer-nesting overflow stops callbacks and sets a sticky fault; + * the timer/BIOS clock continues until foreground cleanup. Put game updates (input sampling, logic, lt_sfx_tick/lt_score * service) in the callback and draw in the foreground as fast as it goes: * logic speed is then independent of drawing speed. * @@ -35,6 +42,9 @@ #define LTTK_DIVISOR_60HZ 19886U #define LTTK_DIVISOR_NES 19854U #define LTTK_STACK_BYTES 3072U +#define LTTK_DRAIN_BUDGET 8U +#define LTTK_FAULT_PENDING 1U +#define LTTK_FAULT_DEFER 2U typedef void (*lttk_work)(void *context); @@ -47,15 +57,17 @@ typedef enum { restored, vector left alone */ } lttk_status; -/* Install with work stopped (ticks are counted from open). */ +/* Foreground-only open/close; do not call from the callback. Install with + * work stopped (ticks are counted from open); a successful open clears faults. */ lttk_status lttk_open(unsigned divisor, lttk_work work, void *context); lttk_status lttk_close(void); /* Start (1) or stop (0) calling work. Starting drops ticks counted while - * stopped, so the first update comes at the next tick. */ + * stopped, so the first update comes at the next tick. A faulted timer cannot + * restart until closed and opened again. Stop is permitted from the callback. */ void lttk_run(int on); /* Hold (1) or release (0) work around foreground code that shares state * with the callback. Ticks keep counting; held ones run at the next tick - * after release. Nests. */ + * after release, subject to the drain budget. Nests up to 65535 levels. */ void lttk_defer(int on); unsigned lttk_divisor(void); @@ -67,5 +79,7 @@ unsigned long lttk_now(void); /* Most ticks that waited for a running callback, and how many ran late. */ unsigned lttk_backlog_max(void); unsigned long lttk_late(void); +/* Sticky OR of LTTK_FAULT_*; overflow is observable, never silent wrap. */ +unsigned lttk_faults(void); #endif diff --git a/src/lt_ay.c b/src/lt_ay.c new file mode 100644 index 0000000..2041f39 --- /dev/null +++ b/src/lt_ay.c @@ -0,0 +1,160 @@ +/* SPDX-License-Identifier: MIT */ +#include "lt_ay.h" +#include + +/* Rounded SN 2 dB steps from AY DAC reference levels. Very quiet nonzero + * values saturate at the SN floor, not silence. This is AY, not YM's DAC. */ +static const lt_u8 ay_attenuation[16] = { + 15,14,14,14,14,13,12,10,9,7,5,4,3,2,1,0 +}; +void lt_ay_config_init(lt_ay_config *c) +{ + if (!c) return; + c->scale_num = c->scale_den = 1; + c->flags = LTAY_COUPLED_NOISE; + c->range_policy = LTAY_RANGE_FOLD; +} +static lta_u32 scale(const lt_ay_config *c, unsigned period) +{ + lta_u32 p; + if (!period) period = 1; + if (c->scale_num == c->scale_den) return (lta_u32)period; + p = ((lta_u32)period * c->scale_num + c->scale_den / 2U) / c->scale_den; + return p ? p : 1UL; +} +static lt_u16 fit(const lt_ay_config *c, lta_u32 p, lt_u8 *level, + lt_u16 *losses) +{ + if (p > 1023UL) { + *losses |= LTAY_LOSS_RANGE; + if (c->range_policy == LTAY_RANGE_MUTE) { *level = 15; return 1; } + if (c->range_policy == LTAY_RANGE_CLAMP) return 1023; + /* Maximum scaled AY period is 4095*65535: at most 19 halvings. */ + while (p > 1023UL) p = (p >> 1) + (p & 1UL); + } + return (lt_u16)p; +} +lta_status lt_ay_map(const lt_ay_config *c, const lt_ay_frame *f, + lt_ay_plan *out) +{ + lt_ay_plan p; + lta_u32 period[3], low, noise_period; + lt_u8 level[3], audible[3], noise_level, bass, coupled; + unsigned i, count, noise_count, slot; + if (!c || !f || !out || !c->scale_num || !c->scale_den || + c->flags > (LTAY_COUPLED_NOISE | LTAY_PERIODIC_BASS) || + c->range_policy > LTAY_RANGE_MUTE || f->envelope_valid > 1 || + (f->envelope_valid && f->envelope_level > 15)) return LTA_INVALID; + if (((f->reg[8] | f->reg[9] | f->reg[10]) & 16) && !f->envelope_valid) + return LTA_UNSUPPORTED; + memset(&p, 0, sizeof p); + for (i = 0; i < 3; ++i) p.period[i] = 1; + for (i = 0; i < 4; ++i) p.attenuation[i] = 15; + p.noise_mode = 4; + count = noise_count = 0; low = 0; bass = 255; noise_level = 15; + for (i = 0; i < 3; ++i) { + unsigned tone = !(f->reg[7] & (1U << i)); + unsigned noise = !(f->reg[7] & (8U << i)); + unsigned env = f->reg[8+i] & 16; + unsigned value = env ? f->envelope_level : f->reg[8+i] & 15; + period[i] = scale(c, f->reg[2*i] | ((unsigned)(f->reg[2*i+1] & 15) << 8)); + level[i] = ay_attenuation[value]; + if (value && value != 15) p.losses |= LTAY_LOSS_VOLUME; + if (env) { + p.losses |= LTAY_LOSS_ENVELOPE; + if (!tone && !noise) p.losses |= LTAY_LOSS_ENVELOPE_ONLY; + } + if (tone && noise && level[i] < 15) { + p.losses |= LTAY_LOSS_MIXER; + level[i] = (lt_u8)(level[i] >= 12 ? 15 : level[i] + 3); + } + audible[i] = (lt_u8)(tone && level[i] < 15); + if (noise && level[i] < 15) { + ++noise_count; + if (level[i] < noise_level) noise_level = level[i]; + } + if (audible[i]) { + ++count; + if (period[i] > 1023UL && period[i] > low) { + low = period[i]; bass = (lt_u8)i; + } + } + } + noise_period = scale(c, f->reg[6] & 31); + if (noise_count > 1) p.losses |= LTAY_LOSS_MIXER; + coupled = 0; + if (noise_level < 15) { + p.losses |= LTAY_LOSS_NOISE; + bass = 255; p.attenuation[3] = noise_level; + if ((c->flags & LTAY_COUPLED_NOISE) && count <= 2 && noise_period <= 1023UL) { + coupled = 1; p.noise_mode = 7; p.period[2] = (lt_u16)noise_period; + } else { + p.losses |= LTAY_LOSS_FIXED_NOISE; + p.noise_mode = (lt_u8)(noise_period < 23UL ? 4 : noise_period < 46UL ? 5 : 6); + } + } else if ((c->flags & LTAY_PERIODIC_BASS) && bass != 255) { + /* Tandy NCR periodic noise has a 15-step ring. This is an optional + * pulse waveform, not an AY square or a portable SN-chip assumption. */ + p.losses |= LTAY_LOSS_PULSE_BASS; + coupled = 1; p.noise_mode = 3; + p.attenuation[3] = level[bass] >= 3 ? (lt_u8)(level[bass]-3) : 0; + p.period[2] = fit(c, (low + 7UL) / 15UL, &p.attenuation[3], &p.losses); + } else bass = 255; + slot = 0; + for (i = 0; i < 3; ++i) if (audible[i] && i != bass) { + unsigned dest = coupled ? slot++ : i; + p.attenuation[dest] = level[i]; + p.period[dest] = fit(c, period[i], &p.attenuation[dest], &p.losses); + } + *out = p; + return LTA_OK; +} +static lta_status set_level(lta_handle *h, unsigned channel, lt_u8 level) +{ + lta_request r; + lta_request_init(&r); r.op = LTA_OP_LEVEL; + r.channel = (lt_u8)channel; r.level = level; + return lta_command(h, &r); +} +lta_status lt_ay_apply(lta_handle *h, const lt_ay_plan *p) +{ + unsigned i; + lta_request r; + lta_status s; + if (!h || !p || !h->chip || !h->chip->emit || !h->token || + p->noise_mode > 7) return LTA_INVALID; + for (i = 0; i < 3; ++i) + if (!p->period[i] || p->period[i] > 1023) return LTA_INVALID; + for (i = 0; i < 4; ++i) if (p->attenuation[i] > 15) return LTA_INVALID; + if ((p->noise_mode & 3) == 3 && p->attenuation[2] != 15) return LTA_INVALID; + if (h->active != 1) return LTA_STATE; + if (h->mask != 15) return LTA_OWNER; + for (i = 0; i < 4; ++i) if (h->chip->owner[i] != h->token) return LTA_OWNER; + if (h->deadline_mask) return LTA_BUSY; + if ((p->noise_mode & 3) == 3 && (h->playing & 4) && h->level[2] != 15) { + s = set_level(h, 2, 15); if (s != LTA_OK) return s; + } + if ((h->playing & 8) && h->noise_mode != p->noise_mode && h->level[3] != 15) { + s = set_level(h, 3, 15); if (s != LTA_OK) return s; + } + for (i = 0; i < 3; ++i) { + unsigned start = !(h->playing & (1U << i)) || h->kind[i] != LTA_KIND_TONE; + if (!start && h->period[i] == p->period[i]) { + if (h->level[i] != p->attenuation[i]) { + s = set_level(h, i, p->attenuation[i]); if (s != LTA_OK) return s; + } + continue; + } + lta_request_init(&r); r.op = start ? LTA_OP_TONE_START : LTA_OP_TONE_UPDATE; + r.channel = (lt_u8)i; r.period = p->period[i]; r.level = p->attenuation[i]; + s = lta_command(h, &r); if (s != LTA_OK) return s; + } + if (!(h->playing & 8) || h->kind[3] != LTA_KIND_NOISE || h->noise_mode != p->noise_mode) { + lta_request_init(&r); + r.op = !(h->playing & 8) || h->kind[3] != LTA_KIND_NOISE ? LTA_OP_NOISE_START : LTA_OP_NOISE_UPDATE; + r.mode = p->noise_mode; r.level = p->attenuation[3]; + return lta_command(h, &r); + } + if (h->level[3] != p->attenuation[3]) return set_level(h, 3, p->attenuation[3]); + return LTA_OK; +} diff --git a/src/lt_gfx.c b/src/lt_gfx.c index 2e47a7c..d1f3960 100644 --- a/src/lt_gfx.c +++ b/src/lt_gfx.c @@ -1,5 +1,9 @@ /* SPDX-License-Identifier: MIT */ #include "lt_gfx.h" +#if defined(__WATCOMC__) && defined(__I86__) && !defined(LT_GFX_C_FILL) +#include +#define LT_GFX_FAST_FILL +#endif static int lt_gfx_valid(const lt_gfx_surface *s) { @@ -34,11 +38,17 @@ void lt_gfx_init(lt_gfx_surface *s, lt_gfx_u8 LT_GFX_FAR *pixels) void lt_gfx_clear(lt_gfx_surface *s, lt_gfx_u8 color) { +#ifndef LT_GFX_FAST_FILL unsigned int i; +#endif lt_gfx_u8 value; if (!lt_gfx_valid(s)) return; value = (lt_gfx_u8)((color & 15U) * 17U); +#ifdef LT_GFX_FAST_FILL + _fmemset(s->pixels, value, LT_GFX_BYTES); +#else for (i = 0; i < LT_GFX_BYTES; ++i) s->pixels[i] = value; +#endif } int lt_gfx_get(const lt_gfx_surface *s, lt_gfx_i16 x, lt_gfx_i16 y) @@ -94,7 +104,14 @@ static void lt_gfx_span(lt_gfx_surface *s, long x0, long y, long x1, } offset = row + (unsigned int)(x0 >> 1); stop = row + (unsigned int)(x1 >> 1); - while (offset < stop) s->pixels[offset++] = packed; +#ifdef LT_GFX_FAST_FILL + /* Amortize the far runtime call on spans, but retain cheap tiny fills + * for glyphs/one-pixel edges. Nibble preservation above is unchanged. */ + if (stop - offset >= 8U) + _fmemset(s->pixels + offset, packed, stop - offset); + else +#endif + while (offset < stop) s->pixels[offset++] = packed; } void lt_gfx_rect(lt_gfx_surface *s, lt_gfx_i16 x0, lt_gfx_i16 y0, diff --git a/src/lt_gfx_tile8.c b/src/lt_gfx_tile8.c new file mode 100644 index 0000000..ed515c3 --- /dev/null +++ b/src/lt_gfx_tile8.c @@ -0,0 +1,65 @@ +/* SPDX-License-Identifier: MIT */ +/* Independently authored general packed-tile API; no application assets + * or device emulation. */ +#include "lt_gfx_tile8.h" + +#if defined(__WATCOMC__) && defined(__I86__) && !defined(LT_GFX_TILE8_C_ONLY) +/* Actual Watcom allocation: source segment DX / offset SI; destination + * offset AX / segment DI. Do not assume uniform ordering of register pairs. + * DS/ES are preserved, DF follows the C ABI (clear). No source alignment + * requirement on 8088. Destination starts in bank zero. */ +static void lt_tile8_words(const lt_gfx_u8 LT_GFX_FAR *, + lt_gfx_u8 LT_GFX_FAR *); +#pragma aux lt_tile8_words = \ + "push ds" "push es" "mov ds,dx" "mov es,di" "mov di,ax" "cld" \ + "movsw" "movsw" "add di,8188" \ + "movsw" "movsw" "add di,8188" \ + "movsw" "movsw" "add di,8188" \ + "movsw" "movsw" "sub di,24420" \ + "movsw" "movsw" "add di,8188" \ + "movsw" "movsw" "add di,8188" \ + "movsw" "movsw" "add di,8188" \ + "movsw" "movsw" "pop es" "pop ds" \ + parm [dx si] [ax di] modify exact [si di]; +#endif + +int lt_gfx_tile8(lt_gfx_surface *s, lt_gfx_i16 x, lt_gfx_i16 y, + const lt_gfx_u8 LT_GFX_FAR *src) +{ + unsigned row, col, px, py, off; + lt_gfx_u8 value, old; + lt_gfx_u8 LT_GFX_FAR *dst; + if (s == 0 || s->pixels == 0 || src == 0) return 0; + if (x <= -8 || x >= 320 || y <= -8 || y >= 200) return 1; + if (x >= 0 && x <= 312 && y >= 0 && y <= 192 && + !(x & 1) && !(y & 3)) { + dst = s->pixels + ((unsigned)y >> 2) * 160U + ((unsigned)x >> 1); +#if defined(__WATCOMC__) && defined(__I86__) && !defined(LT_GFX_TILE8_C_ONLY) + lt_tile8_words(src, dst); +#else + for (row = 0; row < 8U; ++row) { + off = (row & 3U) * 8192U + (row >> 2) * 160U; + for (col = 0; col < 4U; ++col) dst[off + col] = src[row*4U + col]; + } +#endif + return 1; + } + /* Rejection above bounds additions to -7..326 / -7..206: no signed + * 16-bit overflow, even when callers passed extreme coordinates. */ + for (row = 0; row < 8U; ++row) { + py = (unsigned)(y + (int)row); + if (py >= 200U) continue; + off = (py & 3U) * 8192U + (py >> 2) * 160U; + for (col = 0; col < 8U; ++col) { + px = (unsigned)(x + (int)col); + if (px >= 320U) continue; + value = src[row*4U + (col >> 1)]; + value = (lt_gfx_u8)((col & 1U) ? (value & 15U) : (value >> 4)); + dst = s->pixels + off + (px >> 1); + old = *dst; + *dst = (lt_gfx_u8)((px & 1U) ? ((old & 240U) | value) : + ((old & 15U) | (value << 4))); + } + } + return 1; +} diff --git a/tests/ay_core.c b/tests/ay_core.c new file mode 100644 index 0000000..2d0fab6 --- /dev/null +++ b/tests/ay_core.c @@ -0,0 +1,224 @@ +/* SPDX-License-Identifier: MIT */ +#include "lt_ay.h" +#include "lt_sfx.h" +#include +#include + +static lt_chip chip; +static lta_handle audio; +static lt_ay_config config; +static lt_ay_frame frame; +static lt_ay_plan plan, saved; +static lt_sfx_player effect; +static lta_u32 checks, hash = 2166136261UL, writes; +static lta_u32 failures; +static unsigned frame_writes, maximum; +static lt_u16 decoded_period[3]; +static lt_u8 decoded_attenuation[4], decoded_noise, latch; +#define CHECK(x) do { ++checks; if (!(x)) { \ + if (failures < 8) printf("FAIL ay_core.c:%u: %s\n", (unsigned)__LINE__, #x); \ + ++failures; } } while (0) + +static void emit(void *context, lt_u8 value) +{ + unsigned channel; + (void)context; + ++writes; ++frame_writes; + hash = (hash ^ value) * 16777619UL; + if (value & 128) latch = (lt_u8)((value >> 4) & 7); + channel = latch >> 1; + if (latch & 1) decoded_attenuation[channel] = (lt_u8)(value & 15); + else if (latch == 6) decoded_noise = (lt_u8)(value & 7); + else if (value & 128) + decoded_period[channel] = (lt_u16)((decoded_period[channel] & 1008) | (value & 15)); + else decoded_period[channel] = (lt_u16)((decoded_period[channel] & 15) | ((unsigned)(value & 63) << 4)); +} +static lta_status command(lt_u8 op, lt_u8 channel, lt_u8 enabled, lta_u32 value) +{ + lta_request r; + lta_request_init(&r); r.op = op; r.channel = channel; r.mask = 15; + r.enabled = enabled; r.value = value; + return lta_command(&audio, &r); +} +static void tones(unsigned a, unsigned b, unsigned c) +{ + memset(&frame, 0, sizeof frame); + frame.reg[0] = (lt_u8)a; frame.reg[1] = (lt_u8)(a >> 8); + frame.reg[2] = (lt_u8)b; frame.reg[3] = (lt_u8)(b >> 8); + frame.reg[4] = (lt_u8)c; frame.reg[5] = (lt_u8)(c >> 8); + frame.reg[7] = 56; frame.reg[8] = frame.reg[9] = frame.reg[10] = 15; +} +static void apply(void) +{ + unsigned i; + frame_writes = 0; + CHECK(lt_ay_apply(&audio, &plan) == LTA_OK); + if (frame_writes > maximum) maximum = frame_writes; + CHECK(frame_writes <= 13); + for (i = 0; i < 3; ++i) { + CHECK(plan.period[i] >= 1 && plan.period[i] <= 1023); + CHECK(decoded_period[i] == chip.period[i]); + CHECK(audio.period[i] == plan.period[i]); + } + for (i = 0; i < 4; ++i) { + CHECK(plan.attenuation[i] <= 15); + CHECK(decoded_attenuation[i] == chip.attenuation[i]); + CHECK(audio.level[i] == plan.attenuation[i]); + } + CHECK(decoded_noise == chip.noise && audio.noise_mode == plan.noise_mode); + if ((plan.noise_mode & 3) == 3) CHECK(plan.attenuation[2] == 15); +} +static void map(void) +{ + CHECK(lt_ay_map(&config, &frame, &plan) == LTA_OK); + apply(); +} +static void basics(void) +{ + lta_u32 before; + unsigned i; + lt_ay_config_init(&config); + tones(100,200,300); map(); + CHECK(plan.period[0] == 100 && plan.period[2] == 300 && !plan.losses); + apply(); CHECK(frame_writes == 0); + config.scale_num = 3; config.scale_den = 2; + tones(101,0,300); map(); CHECK(plan.period[0] == 152 && plan.period[1] == 2); + lt_ay_config_init(&config); tones(4095,200,300); + map(); CHECK(plan.period[0] == 512 && (plan.losses & LTAY_LOSS_RANGE)); + config.range_policy = LTAY_RANGE_CLAMP; map(); CHECK(plan.period[0] == 1023); + config.range_policy = LTAY_RANGE_MUTE; map(); CHECK(plan.attenuation[0] == 15); + lt_ay_config_init(&config); config.flags |= LTAY_PERIODIC_BASS; + tones(100,200,3000); map(); + CHECK(plan.period[2] == 200 && plan.noise_mode == 3 && plan.attenuation[2] == 15); + CHECK(plan.losses & LTAY_LOSS_PULSE_BASS); + tones(100,200,300); frame.reg[7] = 52; frame.reg[6] = 9; map(); + CHECK(plan.noise_mode == 7 && plan.period[2] == 9 && plan.attenuation[2] == 15); + tones(100,200,300); frame.reg[7] = 48; frame.reg[6] = 22; map(); CHECK(plan.noise_mode == 4); + frame.reg[6] = 23; map(); CHECK(plan.noise_mode == 5); + config.scale_num = 2; frame.reg[6] = 31; map(); CHECK(plan.noise_mode == 6); + lt_ay_config_init(&config); tones(100,200,300); frame.reg[8] = 16; + saved = plan; before = writes; + CHECK(lt_ay_map(&config, &frame, &plan) == LTA_UNSUPPORTED); + CHECK(memcmp(&saved, &plan, sizeof plan) == 0 && writes == before); + frame.envelope_valid = 1; frame.envelope_level = 15; + for (i = 0; i < 16; ++i) { + frame.reg[13] = (lt_u8)i; map(); + CHECK(plan.attenuation[0] == 0 && (plan.losses & LTAY_LOSS_ENVELOPE)); + } + frame.reg[7] |= 9; map(); + CHECK(plan.attenuation[0] == 15 && (plan.losses & LTAY_LOSS_ENVELOPE_ONLY)); + saved = plan; config.scale_den = 0; + CHECK(lt_ay_map(&config, &frame, &plan) == LTA_INVALID); + CHECK(memcmp(&saved, &plan, sizeof plan) == 0); + lt_ay_config_init(&config); frame.envelope_valid = 2; + CHECK(lt_ay_map(&config, &frame, &plan) == LTA_INVALID); + frame.envelope_valid = 1; frame.envelope_level = 16; + CHECK(lt_ay_map(&config, &frame, &plan) == LTA_INVALID); + saved = plan; plan.period[0] = 1024; before = writes; + CHECK(lt_ay_apply(&audio, &plan) == LTA_INVALID && writes == before); plan = saved; + CHECK(command(LTA_OP_SERVICE,0,0,1) == LTA_OK); + CHECK(command(LTA_OP_ARM_STOP,0,0,100) == LTA_OK); before = writes; + CHECK(lt_ay_apply(&audio, &plan) == LTA_BUSY && writes == before); + CHECK(command(LTA_OP_STOP,0,0,0) == LTA_OK); + CHECK(lt_ay_apply(&audio, &plan) == LTA_OK); + chip.owner[0] = 2; before = writes; + CHECK(lt_ay_apply(&audio, &plan) == LTA_OWNER && writes == before); + chip.owner[0] = 1; +} +static void exhaustive(void) +{ + unsigned period, policy, mixer, level, flags; + lt_ay_config_init(&config); + for (policy = 0; policy < 3; ++policy) { + config.range_policy = (lt_u8)policy; + for (period = 0; period < 4096; ++period) { + tones(period,4095-period,1023); map(); + } + } + for (flags = 0; flags < 4; ++flags) { + config.flags = (lt_u8)flags; config.range_policy = LTAY_RANGE_FOLD; + for (mixer = 0; mixer < 64; ++mixer) for (level = 0; level < 32; ++level) { + tones(100,2000,4095); frame.reg[7] = (lt_u8)mixer; + frame.reg[6] = (lt_u8)level; frame.reg[8] = (lt_u8)level; + frame.reg[9] = (lt_u8)(31-level); frame.reg[10] = (lt_u8)level; + frame.envelope_valid = 1; frame.envelope_level = (lt_u8)(level & 15); + map(); + } + } + config.scale_num = 65535; config.scale_den = 1; + for (period = 0; period < 4096; ++period) { tones(period,4095-period,0); map(); } + config.scale_num = 1; config.scale_den = 65535; + tones(4095,1,0); map(); CHECK(plan.period[0] == 1 && plan.period[1] == 1); +} +static void rejected_inputs(void) +{ + lt_ay_config c; + lt_ay_plan p; + lta_handle h; + lta_u32 before = writes; + lt_ay_config_init(0); + lt_ay_config_init(&c); tones(100,200,300); + CHECK(lt_ay_map(&c,&frame,&p) == LTA_OK); saved = p; + CHECK(lt_ay_map(0,&frame,&p) == LTA_INVALID); + CHECK(lt_ay_map(&c,0,&p) == LTA_INVALID); + CHECK(lt_ay_map(&c,&frame,0) == LTA_INVALID); + c.scale_num = 0; CHECK(lt_ay_map(&c,&frame,&p) == LTA_INVALID); + c.scale_num = 1; c.flags = 4; CHECK(lt_ay_map(&c,&frame,&p) == LTA_INVALID); + c.flags = 0; c.range_policy = 3; CHECK(lt_ay_map(&c,&frame,&p) == LTA_INVALID); + CHECK(memcmp(&p,&saved,sizeof p) == 0); + CHECK(lt_ay_apply(0,&p) == LTA_INVALID); + CHECK(lt_ay_apply(&audio,0) == LTA_INVALID); + p.period[0] = 0; CHECK(lt_ay_apply(&audio,&p) == LTA_INVALID); p = saved; + p.attenuation[3] = 16; CHECK(lt_ay_apply(&audio,&p) == LTA_INVALID); p = saved; + p.noise_mode = 8; CHECK(lt_ay_apply(&audio,&p) == LTA_INVALID); p = saved; + p.noise_mode = 3; CHECK(lt_ay_apply(&audio,&p) == LTA_INVALID); p = saved; + h = audio; h.active = 0; CHECK(lt_ay_apply(&h,&p) == LTA_STATE); + h = audio; h.mask = 7; CHECK(lt_ay_apply(&h,&p) == LTA_OWNER); + h = audio; h.chip = 0; CHECK(lt_ay_apply(&h,&p) == LTA_INVALID); + CHECK(writes == before); +} +static void layers(void) +{ + static const lt_sfx_event events[] = {{0,LTSFX_OP_TONE,2,188,1,2}}; + static const lt_sfx_clip clip = {LTSFX_CLIP_VERSION,4,1,1,4,0,6,60UL,1UL,events}; + lta_u32 before; + unsigned i; + lt_ay_config_init(&config); config.flags |= LTAY_PERIODIC_BASS; + tones(100,200,3000); map(); + CHECK(lt_sfx_init(&effect, &audio) == LTA_OK); + CHECK(lt_sfx_trigger(&effect, &clip) == LTA_OK); + CHECK(lt_sfx_tick(&effect) == LTA_OK); + CHECK(audio.effect_mask == 12 && chip.period[2] == 444); + tones(110,210,3450); CHECK(lt_ay_map(&config, &frame, &plan) == LTA_OK); + CHECK(lt_ay_apply(&audio, &plan) == LTA_OK); + CHECK(chip.period[2] == 444 && audio.period[2] == 230); + CHECK(lt_sfx_stop(&effect) == LTA_OK); + CHECK(chip.period[2] == 230 && chip.attenuation[2] == 15 && chip.noise == 3); + before = writes; CHECK(lt_ay_apply(&audio, &plan) == LTA_OK); CHECK(writes == before); + CHECK(command(LTA_OP_MUTE,0,1,0) == LTA_OK); + tones(150,250,400); CHECK(lt_ay_map(&config, &frame, &plan) == LTA_OK); + CHECK(lt_ay_apply(&audio, &plan) == LTA_OK); + for (i = 0; i < 4; ++i) CHECK(chip.attenuation[i] == 15); + CHECK(command(LTA_OP_MUTE,0,0,0) == LTA_OK); + CHECK(chip.period[0] == 150 && chip.attenuation[0] == 0); + CHECK(lta_music_attenuation(&audio,0,4) == LTA_OK); + tones(151,250,400); map(); CHECK(chip.attenuation[0] == 4); + CHECK(lta_layer_pause(&audio,0,1) == LTA_OK); + tones(152,251,401); map(); + for (i = 0; i < 4; ++i) CHECK(chip.attenuation[i] == 15); + CHECK(lta_layer_pause(&audio,0,0) == LTA_OK); + CHECK(chip.period[0] == 152 && chip.attenuation[0] == 4); +} +int main(void) +{ + lt_init(&chip, emit, 0); CHECK(lta_init(&audio, &chip, 1) == LTA_OK); + CHECK(command(LTA_OP_ACQUIRE,0,0,0) == LTA_OK); + basics(); exhaustive(); layers(); rejected_inputs(); + CHECK(command(LTA_OP_RELEASE,0,0,0) == LTA_OK); + CHECK(hash == 0x85731ebbUL && writes == 48309UL && maximum == 12U); + printf("AY %s checks=%lu writes=%lu hash=%08lx max=%u config=%u frame=%u plan=%u int=%u ptr=%u\n", + failures ? "FAIL" : "PASS", (unsigned long)checks, (unsigned long)writes, + (unsigned long)hash, maximum, (unsigned)sizeof config, (unsigned)sizeof frame, + (unsigned)sizeof plan, (unsigned)sizeof(int), (unsigned)sizeof(void *)); + return failures != 0; +} diff --git a/tests/event_core.c b/tests/event_core.c index e6e91b2..100adf7 100644 --- a/tests/event_core.c +++ b/tests/event_core.c @@ -15,7 +15,7 @@ typedef char lte_event_size_must_be_14[(sizeof(lte_event) == 14U) ? 1 : -1]; typedef char lte_event_far_pointer_must_be_4[ (sizeof(lte_event LTE_FAR *) == 4U) ? 1 : -1]; typedef char lte_event_near_pointer_must_be_2[ - (sizeof(lte_event *) == 2U) ? 1 : -1]; + (sizeof(lte_event __near *) == 2U) ? 1 : -1]; typedef char lte_queue_size_must_be_10[(sizeof(lte_event_queue) == 10U) ? 1 : -1]; #endif diff --git a/tests/gfx_far_dos.c b/tests/gfx_far_dos.c new file mode 100644 index 0000000..eb137fc --- /dev/null +++ b/tests/gfx_far_dos.c @@ -0,0 +1,100 @@ +/* SPDX-License-Identifier: MIT */ +/* Synthetic DOS buffers: no video hardware or display writes. */ +#include +#include +#include "lt_gfx_tile8.h" +static unsigned long cases, failures; +static unsigned char __far *source; +static unsigned char __far *actual; +static unsigned char __far *expected; +static lt_gfx_surface surface; +static unsigned base, source_offset; +static unsigned char packed[32]; +static void reset(void) +{ + unsigned i = 0; + do { actual[i]=expected[i]=0xa5U; ++i; } while(i); +} +static void compare(void) +{ + unsigned i = 0; + ++cases; + do { + if(actual[i]!=expected[i]) { + printf("FAIL case=%lu base=%04x source=%04x at=%04x got=%u want=%u\n", + cases,base,source_offset,i,(unsigned)actual[i],(unsigned)expected[i]); + ++failures;return; + } + ++i; + } while(i); +} +static void pixel(unsigned x,unsigned y,unsigned char v) +{ + unsigned at=(unsigned)(base+(y&3U)*8192U+(y>>2)*160U+(x>>1)); + unsigned char old=expected[at]; + expected[at]=(unsigned char)((x&1U)?((old&240U)|(v&15U)):((old&15U)|(v<<4))); +} +static void tile(short x,short y) +{ + unsigned r,c;long xx,yy;unsigned char v; + reset(); + if(!lt_gfx_tile8(&surface,x,y,source+source_offset))++failures; + for(r=0;r<8;++r)for(c=0;c<8;++c) { + xx=(long)x+c;yy=(long)y+r; + if(xx<0||xx>=320||yy<0||yy>=200)continue; + v=packed[r*4U+(c>>1)]; + v=(unsigned char)((c&1U)?(v&15U):(v>>4)); + pixel((unsigned)xx,(unsigned)yy,v); + } + compare(); +} +static void rect(short x0,short y0,short x1,short y1,unsigned char color) +{ + unsigned x,y; + reset();lt_gfx_rect(&surface,x0,y0,x1,y1,color); + for(y=0;y<200;++y)for(x=0;x<320;++x) + if((long)x>=x0&&(long)x=y0&&(long)y=source_offset && (unsigned)(i-source_offset)<32U)want=packed[i-source_offset]; + if(source[i]!=want){++failures;break;}++i; + }while(i); + } + reset();lt_gfx_clear(&surface,0x3eU); + for(i=0;i<32768U;++i)expected[base+i]=0xeeU; + compare(); + rect(0,0,320,200,3);rect(1,1,319,199,9); + rect(319,199,320,200,7);rect(301,196,318,200,12); + rect(-32768,-32768,32767,32767,0); + } + _dos_freemem(es);_dos_freemem(as);_dos_freemem(ss); + printf("%s far graphics cases=%lu failures=%lu data_ptr=%u; FFE0/FFDF sources, 8000/7FFF surfaces, full64KiB guards\n", + failures?"FAIL":"PASS",cases,failures,(unsigned)sizeof(void*)); + return failures?1:0; +} diff --git a/tests/gfx_far_matrix.py b/tests/gfx_far_matrix.py new file mode 100644 index 0000000..fbd2762 --- /dev/null +++ b/tests/gfx_far_matrix.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Opt-in DOS far-pointer boundary checks; installed Linux Watcom/DOSBox-X only.""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import subprocess +p=argparse.ArgumentParser(description=__doc__) +p.add_argument('--watcom',type=Path,required=True) +p.add_argument('--dosbox',type=Path,required=True,help='DOSBox-X executable or environment wrapper') +p.add_argument('--timeout',type=int,default=90) +a=p.parse_args(); root=Path(__file__).resolve().parents[1] +out=root/'build/gfx-far-matrix';out.mkdir(parents=True,exist_ok=True) +w=a.watcom.resolve();bindir=w/('binl64' if (w/'binl64/wcl').exists() else 'binl') +env=dict(os.environ,WATCOM=str(w),INCLUDE=str(w/'h'),LIB=str(w/'lib286')+':'+str(w/'lib286/dos')) +env['PATH']=str(bindir)+os.pathsep+env.get('PATH','') +sources=[root/'src/lt_gfx.c',root/'src/lt_gfx_tile8.c',root/'tests/gfx_far_dos.c'] +records=[];names=[] +for model in 'scml': + for kind in ('A','C'): + name='FAR'+model.upper()+kind;names.append(name) + flags=['-q','-0','-m'+model,'-ecc','-bt=dos','-ox','-s','-zt=0','-w4','-we','-k8192','-i='+str(root/'include'),'-fe='+name+'.EXE'] + if kind=='C':flags+=['-dLT_GFX_TILE8_C_ONLY','-dLT_GFX_C_FILL'] + cmd=[str(bindir/'wcl')]+flags+list(map(str,sources)) + r=subprocess.run(cmd,cwd=out,env=env,text=True,capture_output=True,timeout=30) + records.append(dict(argv=cmd,exit=r.returncode,stdout=r.stdout,stderr=r.stderr)) + (out/'compile.json').write_text(json.dumps(records,indent=2)+'\n') + r.check_returncode() +config='''[sdl] +fullscreen=false +output=surface +[dosbox] +machine=tandy +memsize=0 +memsizekb=640 +splash logo=false +show basic information=false +[cpu] +core=normal +cputype=8086_prefetch +cycles=fixed 20000 +fpu=false +[dos] +xms=false +ems=false +umb=false +[dosbox-x] +automount=false +automountall=false +startcmd=false +[mixer] +nosound=true +[midi] +mididevice=none +[joystick] +joysticktype=none +[autoexec] +@echo off +''' +config+='mount c "'+str(out)+'"\nc:\n' +for name in names: + (out/(name+'.LOG')).unlink(missing_ok=True) + config+=name+' > '+name+'.LOG\n' +config+='exit\n';(out/'far.conf').write_text(config) +(out/'home').mkdir(exist_ok=True) +env.update(HOME=str(out/'home'),XDG_CONFIG_HOME=str(out/'home/.config'),SDL_VIDEODRIVER='dummy',SDL_AUDIODRIVER='dummy') +with (out/'emulator.log').open('w') as log: + subprocess.run([str(a.dosbox.resolve()),'-defaultconf','-conf',str(out/'far.conf')],cwd=out,env=env,stdout=log,stderr=subprocess.STDOUT,timeout=a.timeout,check=True) +for name in names: + log=(out/(name+'.LOG')).read_text() + if 'PASS far graphics cases=64 failures=0' not in log or 'FAIL' in log:raise SystemExit(name+' failed: '+log) + print(name+': '+log.strip()) +files=sources+[root/'include/lt_gfx.h',root/'include/lt_gfx_tile8.h']+[out/(n+'.EXE') for n in names] +(out/'manifest.json').write_text(json.dumps({str(f.relative_to(root)):hashlib.sha256(f.read_bytes()).hexdigest() for f in files},indent=2)+'\n') diff --git a/tests/gfx_fill.c b/tests/gfx_fill.c new file mode 100644 index 0000000..63a2e25 --- /dev/null +++ b/tests/gfx_fill.c @@ -0,0 +1,49 @@ +/* SPDX-License-Identifier: MIT */ +/* Independent per-pixel rectangles and clear guards, including odd edges. */ +#include +#include "lt_gfx.h" +#define G 16U +static lt_gfx_u8 LT_GFX_FAR actual[LT_GFX_BYTES+2U*G]; +static lt_gfx_u8 LT_GFX_FAR expect[LT_GFX_BYTES+2U*G]; +static unsigned long failures,cases; +static lt_gfx_surface surface; +static void compare(void) +{ + unsigned i;for(i=0;i=x0&&(long)x=y0&&(long)y +#include +#include "lt_gfx.h" +#ifndef LT_FILL_BENCH_FRAMES +#define LT_FILL_BENCH_FRAMES 100U +#endif +static lt_gfx_u8 LT_GFX_FAR pixels[LT_GFX_BYTES]; +static lt_gfx_surface surface; +int main(void) +{ + unsigned i,j;clock_t start,end;unsigned long h; + surface.pixels=pixels; + for(j=0;j<2U;++j) { + start=clock(); + for(i=0;i +#include "lt_gfx_tile8.h" +#define GUARD 16U +static lt_gfx_u8 LT_GFX_FAR actual[LT_GFX_BYTES + GUARD*2U]; +static lt_gfx_u8 LT_GFX_FAR wanted[LT_GFX_BYTES + GUARD*2U]; +static lt_gfx_u8 LT_GFX_FAR source[64]; +static unsigned long cases, failures; +static lt_gfx_surface surface; + +static void reset(void) +{ + unsigned i; + for (i=0; i=320L||yy<0L||yy>=200L)continue; + pixel=source[16U+j*4U+i/2U]; + pixel=(unsigned char)((i&1U)?(pixel&15U):(pixel>>4)); + at=GUARD+((unsigned)yy%4U)*8192U+((unsigned)yy/4U)*160U+(unsigned)xx/2U; + old=wanted[at];wanted[at]=(unsigned char)((xx&1L)?((old&240U)|pixel):((old&15U)|(pixel<<4))); + } +} +int main(void) +{ + static const short edge_x[]={-32768,-32760,-9,-8,-7,-4,-1,0,1,2,7,8,310,311,312,313,316,319,320,32760,32767}; + static const short edge_y[]={-32768,-32760,-9,-8,-7,-4,-1,0,1,2,3,4,7,8,190,191,192,193,196,199,200,32760,32767}; + unsigned x,y,bias,i;lt_gfx_surface empty; + surface.pixels=actual+GUARD;empty.pixels=0;reset(); + if(lt_gfx_tile8(0,0,0,source)||lt_gfx_tile8(&empty,0,0,source)||lt_gfx_tile8(&surface,0,0,0))++failures; + compare(); + for(x=0;x +#include +#include +#include +#include "lt_guard_dos.h" +typedef void (__interrupt __far *Vector)(void); +static unsigned checks,failures; +static volatile unsigned crt_calls; +static void __interrupt __far foreign(void) { } +static int __far crt_policy(unsigned deverr,unsigned errcode,unsigned __far *devhdr) +{ (void)deverr;(void)errcode;(void)devhdr;++crt_calls;_hardresume(_HARDERR_IGNORE);return 0; } +static unsigned critical_on_stack(unsigned segment,unsigned offset); +#pragma aux critical_on_stack = \ + "pushf" "cli" "mov bx,ss" "mov cx,sp" \ + "mov ss,ax" "mov sp,dx" "push bx" "push cx" "sti" \ + "mov ax,1234h" "int 24h" "cli" "pop cx" "pop bx" \ + "mov ss,bx" "mov sp,cx" "popf" \ + parm [ax] [dx] value [ax] modify exact [ax bx cx]; +#define CHECK(x) do {++checks;if(!(x)){++failures;printf("FAIL line=%u\n",(unsigned)__LINE__);}}while(0) +static void call_int(unsigned vector,unsigned wantax) +{ + union REGS in,out;struct SREGS segments; + memset(&in,0,sizeof(in));segread(&segments); + in.x.ax=0x1234;in.x.bx=0x5678;in.x.cx=0x9abc;in.x.dx=0xdef0; + in.x.si=0xa55a;in.x.di=0x5aa5; + int86x(vector,&in,&out,&segments); + if(out.x.ax!=wantax)printf("vector=%u AX=%04x expected=%04x\n",vector,out.x.ax,wantax); + CHECK(out.x.ax==wantax);CHECK(out.x.bx==in.x.bx); + CHECK(out.x.cx==in.x.cx);CHECK(out.x.dx==in.x.dx); + CHECK(out.x.si==in.x.si);CHECK(out.x.di==in.x.di); +} +int main(void) +{ + Vector old23=_dos_getvect(0x23),old24=_dos_getvect(0x24),crt24; + unsigned stack=0,mask;unsigned char version=_osmajor; + setvbuf(stdout,0,_IONBF,0); + CHECK(ltguard_close()==0); + _osmajor=2;CHECK(ltguard_open()==LTGUARD_DOS_TOO_OLD);_osmajor=version; + CHECK(_dos_getvect(0x23)==old23&&_dos_getvect(0x24)==old24); + _harderr(crt_policy);crt24=_dos_getvect(0x24); + CHECK(ltguard_open()==LTGUARD_OK);CHECK(ltguard_open()==LTGUARD_BUSY); + CHECK(!ltguard_quit()&&!ltguard_critical()); + call_int(0x23,0x1234);CHECK(ltguard_quit()&&!ltguard_critical()); + CHECK(ltguard_close()==0);CHECK(ltguard_quit()&&!ltguard_critical()); + CHECK(_dos_getvect(0x23)==old23&&_dos_getvect(0x24)==crt24); + CHECK(ltguard_close()==0);CHECK(ltguard_open()==LTGUARD_OK); + CHECK(!ltguard_quit()&&!ltguard_critical()); + call_int(0x24,0x1203);CHECK(ltguard_quit()&<guard_critical()); + CHECK(crt_calls==0);CHECK(ltguard_close()==0); + /* Closing must restore not just the vector but the untouched CRT policy. */ + /* Watcom CRT _hardresume clears AH, unlike our AL-only handler. */ + call_int(0x24,0x0000);CHECK(crt_calls==1); + CHECK(ltguard_open()==LTGUARD_OK); + CHECK(_dos_allocmem(128,&stack)==0); + if(stack) { + CHECK(critical_on_stack(stack,2046U)==0x1203U); + CHECK(ltguard_quit()&<guard_critical()); + _dos_freemem(stack); + } + CHECK(ltguard_close()==0); + for(mask=1;mask<=3;++mask) { + CHECK(ltguard_open()==LTGUARD_OK); + if(mask&1)_dos_setvect(0x23,foreign); + if(mask&2)_dos_setvect(0x24,foreign); + CHECK(ltguard_close()==mask);CHECK(ltguard_close()==0); + CHECK(_dos_getvect(0x23)==((mask&1)?foreign:old23)); + CHECK(_dos_getvect(0x24)==((mask&2)?foreign:crt24)); + _dos_setvect(0x23,old23);_dos_setvect(0x24,crt24); + } + _dos_setvect(0x23,old23);_dos_setvect(0x24,old24); + printf("%s guard checks=%u failures=%u int=%u ptr=%u; registers/foreignSS/CRT-policy/ownership restored; no disk fault injected\n",failures?"FAIL":"PASS",checks,failures,(unsigned)sizeof(int),(unsigned)sizeof(void*)); + return failures?1:0; +} diff --git a/tests/tick_dos.c b/tests/tick_dos.c new file mode 100644 index 0000000..639ea01 --- /dev/null +++ b/tests/tick_dos.c @@ -0,0 +1,117 @@ +/* SPDX-License-Identifier: MIT; native timer ABI/ownership regression. */ +#include +#include +#include "lt_tick_dos.h" +/* Keep the address-taken callback-local pointer test a real ABI call. */ +#pragma inline_depth(0) +typedef void (__interrupt __far *Vector)(void); +extern void lttk_test_seed(unsigned pending, unsigned depth); +extern unsigned lttk_test_pending(void); +extern unsigned lttk_test_stack_used(void); +extern unsigned lttk_test_stack_guard(void); +static volatile unsigned callbacks, bad, slow_once, stop_at; +static unsigned cookie = 0x5aa5U; +static unsigned checks, failures; +#define CHECK(x) do { ++checks; if (!(x)) { ++failures; printf("FAIL line %u\n", (unsigned)__LINE__); } } while (0) +static void idle(void); +#pragma aux idle = "hlt" modify exact []; +static void alternate(unsigned segment, unsigned offset); +#pragma aux alternate = \ + "pushf" "cli" "mov bx,ss" "mov si,sp" \ + "mov ss,ax" "mov sp,dx" "push bx" "push si" "mov cx,8" \ + "sti" "again: hlt" "loop again" "cli" \ + "pop si" "pop bx" "mov ss,bx" "mov sp,si" "popf" \ + parm [ax] [dx] modify exact [bx cx si]; +static unsigned local_check(volatile unsigned *p, unsigned value) +{ return *p == value; } +static void update(void *context) +{ + volatile unsigned local = 0x3c69U; + unsigned long begin; + if (*(unsigned *)context != 0x5aa5U || !local_check(&local,0x3c69U)) bad=1; + ++callbacks; + if (slow_once) { + slow_once=0; begin=lttk_now(); + while (lttk_now()-begin < 3UL*23864UL) { } + } + if (stop_at && callbacks >= stop_at) lttk_run(0); +} +static void __interrupt __far foreign(void) { } +static void wait_ticks(unsigned count) +{ + unsigned long start=lttk_ticks(); + while (lttk_ticks()-start < count) idle(); +} +int main(void) +{ + Vector before=_dos_getvect(8); + unsigned stack_segment=0, observed, previous_calls; + unsigned long previous_time, now, late; + void *context=&cookie; +#if defined(__COMPACT__) || defined(__LARGE__) + unsigned context_segment=0; + setvbuf(stdout,0,_IONBF,0); + puts("timer phase: allocation"); + if (_dos_allocmem(1,&context_segment)) { puts("allocation failed"); return 2; } + puts("timer phase: allocated"); + context=(void *)MK_FP(context_segment,0); + *(unsigned *)context=0x5aa5U; +#endif + setvbuf(stdout,0,_IONBF,0); + puts("timer phase: init"); + CHECK(lttk_close()==LTTK_NOT_OPEN); + CHECK(lttk_open(999U,update,context)==LTTK_INVALID); + CHECK(lttk_open(23864U,0,context)==LTTK_INVALID); + CHECK(_dos_getvect(8)==before); + CHECK(lttk_open(23864U,update,context)==LTTK_OK); + CHECK(lttk_open(23864U,update,context)==LTTK_BUSY); + CHECK(lttk_divisor()==23864U && !lttk_faults()); + wait_ticks(2); CHECK(callbacks==0); + lttk_run(1); wait_ticks(4); CHECK(callbacks>=4 && !bad); + lttk_defer(1);lttk_defer(1);previous_calls=callbacks; + wait_ticks(3);CHECK(callbacks==previous_calls); + lttk_defer(0);wait_ticks(2);CHECK(callbacks==previous_calls); + lttk_defer(0);wait_ticks(2);CHECK(callbacks>=previous_calls+7); + lttk_run(0);previous_calls=callbacks;wait_ticks(2);CHECK(callbacks==previous_calls); + previous_time=lttk_now(); + { unsigned i; for(i=0;i<2000U;++i){now=lttk_now();CHECK(now>=previous_time);previous_time=now;} } + puts("timer phase: foreign stack"); + CHECK(_dos_allocmem(256,&stack_segment)==0); + if(stack_segment){ + lttk_run(1);previous_calls=callbacks; + alternate(stack_segment,4094U); + lttk_run(0);CHECK(callbacks>previous_calls && !bad); + _dos_freemem(stack_segment); + } + puts("timer phase: nested IRQ"); + slow_once=1;lttk_run(1);wait_ticks(8);lttk_run(0); + late=lttk_late();CHECK(late>=2 && lttk_backlog_max()>=2); + observed=lttk_test_stack_used(); + CHECK(observed>0 && observed=13U);stop_at=0;lttk_run(0); + lttk_defer(1);lttk_test_seed(65535U,1U);wait_ticks(1); + CHECK(lttk_faults()==LTTK_FAULT_PENDING); + previous_calls=callbacks;lttk_run(1);lttk_defer(0);wait_ticks(2);CHECK(callbacks==previous_calls); + CHECK(lttk_close()==LTTK_OK && _dos_getvect(8)==before); + CHECK(lttk_divisor()==0 && lttk_now()==0); + CHECK(lttk_open(23864U,update,context)==LTTK_OK && !lttk_faults()); + lttk_test_seed(0,65535U);lttk_defer(1); + CHECK(lttk_faults()==LTTK_FAULT_DEFER); + CHECK(lttk_close()==LTTK_OK && _dos_getvect(8)==before); + CHECK(lttk_open(23864U,update,context)==LTTK_OK); + _disable();_dos_setvect(8,foreign); + CHECK(lttk_close()==LTTK_NOT_OURS && _dos_getvect(8)==foreign); + _dos_setvect(8,before);_enable(); +#if defined(__COMPACT__) || defined(__LARGE__) + _dos_freemem(context_segment); +#endif + printf("%s timer checks=%u failures=%u data_ptr=%u callback_ptr=%u observed_stack=%u/%u late=%lu faults/restoration/budget=checked\n",failures?"FAIL":"PASS",checks,failures,(unsigned)sizeof(void*),(unsigned)sizeof(lttk_work),observed,LTTK_STACK_BYTES,late); + return failures?1:0; +} diff --git a/tests/tick_matrix.py b/tests/tick_matrix.py new file mode 100644 index 0000000..6d8fe21 --- /dev/null +++ b/tests/tick_matrix.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""Build/run the DOS IRQ ABI matrix using installed tools (no downloads).""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import subprocess + +p = argparse.ArgumentParser(description=__doc__) +p.add_argument('--watcom', required=True, type=Path) +p.add_argument('--dosbox', required=True, type=Path, help='DOSBox-X executable or environment-setting wrapper') +p.add_argument('--build', type=Path, default=Path('build/tick-matrix')) +p.add_argument('--cycles', type=int, default=20000) +p.add_argument('--timeout', type=int, default=90) +a = p.parse_args() +root = Path(__file__).resolve().parents[1] +build = a.build.resolve(); build.mkdir(parents=True, exist_ok=True) +watcom = a.watcom.resolve() +wcl = next((watcom / d / 'wcl' for d in ('binl64', 'binl') if (watcom / d / 'wcl').exists()), None) +if wcl is None: + raise SystemExit('No installed Linux Watcom wcl found') +env = dict(os.environ, WATCOM=str(watcom), INCLUDE=str(watcom/'h'), + LIB=str(watcom/'lib286')+':'+str(watcom/'lib286/dos')) +env['PATH'] = str(wcl.parent)+os.pathsep+env.get('PATH','') +flags = ['-q','-0','-ecc','-bt=dos','-ox','-s','-zt=0','-w4','-we','-dLTTK_TEST','-k8192','-i='+str(root/'include')] +sources = [root/'adapters/lt_tick_dos.c', root/'tests/tick_dos.c'] +for model in 'scml': + cmd = [str(wcl), '-m'+model]+flags+['-fe=TICK'+model+'.EXE']+list(map(str,sources)) + result = subprocess.run(cmd, cwd=build, env=env, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + (build/('compile-'+model+'.log')).write_text(result.stdout) + result.check_returncode() +# The adapter must reject stack probes rather than silently compiling an unsafe ISR. +cmd = [str(wcl), '-ms']+[x for x in flags if x not in ('-s','-ox')]+['-od']+['-c',str(sources[0])] +r = subprocess.run(cmd,cwd=build,env=env,text=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT) +(build/'reject-stack-probes.log').write_text(r.stdout) +if r.returncode == 0 or 'Compile lt_tick_dos.c' not in r.stdout: + raise SystemExit('Missing -s was not rejected as expected') +conf = '''[sdl] +fullscreen=false +output=surface +[dosbox] +machine=tandy +memsize=0 +memsizekb=640 +splash logo=false +show basic information=false +[cpu] +core=normal +cputype=8086_prefetch +cycles=fixed {cycles} +fpu=false +[dos] +xms=false +ems=false +umb=false +[dosbox-x] +automount=false +automountall=false +startcmd=false +[mixer] +nosound=true +[midi] +mididevice=none +[joystick] +joysticktype=none +[autoexec] +@echo off +mount c "{directory}" +c: +'''.format(cycles=a.cycles,directory=build) +for model in 'scml': + log = build/('TICK'+model.upper()+'.LOG') + if log.exists(): log.unlink() # only this runner's disposable logs + conf += 'TICK'+model+' > TICK'+model+'.LOG\n' +conf += 'exit\n' +(build/'matrix.conf').write_text(conf) +(build/'home').mkdir(exist_ok=True) +env.update(HOME=str(build/'home'),SDL_VIDEODRIVER='dummy',SDL_AUDIODRIVER='dummy') +with (build/'emulator.log').open('w') as log: + subprocess.run([str(a.dosbox.resolve()),'-defaultconf','-conf',str(build/'matrix.conf')],cwd=build,env=env,stdout=log,stderr=subprocess.STDOUT,timeout=a.timeout,check=True) +for model in 'scml': + log=(build/('TICK'+model.upper()+'.LOG')).read_text() + if 'PASS timer checks=' not in log or 'FAIL' in log: + raise SystemExit('Timer model '+model+' failed: '+log) + print(model+': '+log.strip().splitlines()[-1]) +files = sources+[root/'include/lt_tick_dos.h']+list(build.glob('*.EXE')) +manifest={str(f.relative_to(root) if f.is_relative_to(root) else f):hashlib.sha256(f.read_bytes()).hexdigest() for f in files} +(build/'manifest.json').write_text(json.dumps({'flags':flags,'cycles':a.cycles,'sha256':manifest},indent=2)+'\n') diff --git a/tests/tile8_bench.c b/tests/tile8_bench.c new file mode 100644 index 0000000..b2620f1 --- /dev/null +++ b/tests/tile8_bench.c @@ -0,0 +1,38 @@ +/* SPDX-License-Identifier: MIT */ +/* Relative tile-copy benchmark, not a calibrated physical EX speed claim. */ +#include +#include +#include "lt_gfx_tile8.h" +#ifndef LT_TILE8_BENCH_FRAMES +#define LT_TILE8_BENCH_FRAMES 200U +#endif +static lt_gfx_u8 LT_GFX_FAR pixels[LT_GFX_BYTES]; +static lt_gfx_u8 LT_GFX_FAR tile[32]; +static lt_gfx_surface surface; +static unsigned long hash(void) +{ + unsigned i;unsigned long h=2166136261UL; + for(i=0;i sources. +# DOS-only demos/tests of platform services: name -> sources. DOS_DEMOS = { + 'GUARD': ['tests/guard_dos.c', 'adapters/lt_guard_dos.c'], + 'TILEBEN': ['tests/tile8_bench.c', 'src/lt_gfx_tile8.c', + 'src/lt_gfx_rows.c'], 'ENGDOS': ['tests/engine_dos.c', 'src/lt_engine_view.c', 'adapters/lt_engine_dos.c'], 'ENGDEMO': ['examples/engine_demo.c', 'src/lt_engine.c', @@ -65,6 +71,8 @@ def main(): parser.add_argument('--gcc-launcher', type=Path) parser.add_argument('--runtime-bin', type=Path) parser.add_argument('--watcom', type=Path, default=Path('C:/WATCOM')) + parser.add_argument('--model', choices=('small', 'compact', 'medium', 'large'), + default='small', help='DOS memory model (default: small)') args = parser.parse_args() if args.target == 'dos' and (args.gcc_launcher or args.runtime_bin): parser.error('--gcc-launcher and --runtime-bin apply to host builds') @@ -72,7 +80,8 @@ def main(): parser.error('--runtime-bin requires --gcc-launcher') if set(TESTS) & {name.lower() for name in DOS_DEMOS}: raise RuntimeError('DOS test/demo executable names collide') - out = ROOT / 'build' / args.target + out = ROOT / 'build' / (args.target if args.target != 'dos' or + args.model == 'small' else 'dos-' + args.model) out.mkdir(parents=True, exist_ok=True) env = dict(os.environ) records = [] @@ -119,17 +128,22 @@ def run(argv, label): cc = watcom / 'BINNT/wcl.exe' else: # Open Watcom's Linux host layout (lower-case directories) + bindir = watcom / ('binl64' if (watcom / 'binl64/wcl').exists() + else 'binl') env.update(WATCOM=str(watcom), INCLUDE=str(watcom / 'h'), LIB=str(watcom / 'lib286') + os.pathsep + str(watcom / 'lib286/dos'), - PATH=str(watcom / 'binl') + os.pathsep + + PATH=str(bindir) + os.pathsep + env.get('PATH', '')) - cc = watcom / 'binl/wcl' - flags = [cc, '-q', '-bt=dos', '-0', '-ms', '-ecc', '-os', '-s', + cc = bindir / 'wcl' + model = {'small': '-ms', 'compact': '-mc', 'medium': '-mm', + 'large': '-ml'}[args.model] + flags = [cc, '-q', '-bt=dos', '-0', model, '-ecc', '-os', '-s', '-w4', '-wx', '-i=' + str(ROOT / 'include')] for source in sorted((ROOT / 'src').glob('*.c')) + sorted( (ROOT / 'adapters').glob('*.c')): - run([*flags, '-c', source], source.stem) + extra = ['-zu', '-fo=lt_guard_dos.obj'] if source.name == 'lt_guard_dos.c' else [] + run([*flags, *extra, '-c', source], source.stem) run([*flags, '-k4096', '-fe=DEMO.EXE', ROOT / 'examples/dos_demo.c', ROOT / 'src/lt_chip.c', ROOT / 'src/lt_gfx.c', ROOT / 'src/lt_event.c', @@ -150,7 +164,8 @@ def run(argv, label): ROOT / 'adapters/lt_tick_dos.c'], 'Tandy logo') for exe, sources in DOS_DEMOS.items(): run([*flags, '-k4096', '-fe=' + exe + '.EXE', - *[ROOT / src for src in sources]], exe.lower()) + *[(out / 'lt_guard_dos.obj') if src == 'adapters/lt_guard_dos.c' + else ROOT / src for src in sources]], exe.lower()) for name, (test, modules) in TESTS.items(): sources = [ROOT / 'src' / (module + '.c') for module in modules] sources.append(ROOT / 'tests' / (test + '.c')) diff --git a/tools/check_sms_package.py b/tools/check_sms_package.py index 57323c3..0ec0cdc 100644 --- a/tools/check_sms_package.py +++ b/tools/check_sms_package.py @@ -2,6 +2,8 @@ from pathlib import Path import hashlib import json +import re +import subprocess import zipfile ROOT = Path(__file__).resolve().parents[1] @@ -9,8 +11,25 @@ manifest = json.loads((DIRECTORY / 'DOS-BUILD.json').read_text()) native = json.loads((DIRECTORY / 'NATIVE.json').read_text())['stroke_logo_run'] sha = lambda data: hashlib.sha256(data).hexdigest() +# This download is an immutable historical qualification, not a promise that +# its binaries were rebuilt whenever an unrelated/current library changed. +# Check the recorded commit's exact source; never rewrite its hashes to match +# an untested current tree. CI fetches history so this check stays offline. +revision = manifest['qualified_source_commit'] +assert re.fullmatch(r'[0-9a-f]{40}', revision), 'qualified source commit' +current_differences = [] for name, digest in manifest['source_lf_sha256'].items(): - assert sha((ROOT / name).read_bytes().replace(b'\r\n', b'\n')) == digest, name + assert not Path(name).is_absolute() and '..' not in Path(name).parts, name + try: + qualified = subprocess.check_output( + ['git', 'show', revision + ':' + name], cwd=ROOT, + stderr=subprocess.PIPE) + except subprocess.CalledProcessError as error: + raise RuntimeError('Historical package check needs git history for ' + + revision + '; fetch that commit or use a full clone') from error + assert sha(qualified.replace(b'\r\n', b'\n')) == digest, name + if sha((ROOT / name).read_bytes().replace(b'\r\n', b'\n')) != digest: + current_differences.append(name) archive = DIRECTORY / 'SMSBOOT.ZIP' assert sha(archive.read_bytes()) == manifest['zip_sha256'], 'ZIP hash' with zipfile.ZipFile(archive) as package: @@ -24,4 +43,6 @@ assert data[:2] == b'MZ', name assert sha(data) == native[name]['sha256'], name + ': native receipt' assert len(data) == native[name]['bytes'], name + ': size' -print('PASS qualified DOS package: source, ZIP, EXE/native hashes and members') +print('PASS qualified DOS package: pinned source ' + revision + ', ZIP, EXE/native hashes and members') +if current_differences: + print('Current sources differ from that historical binary: ' + ', '.join(current_differences)) diff --git a/tools/qualify_dos.py b/tools/qualify_dos.py new file mode 100644 index 0000000..a5d9410 --- /dev/null +++ b/tools/qualify_dos.py @@ -0,0 +1,111 @@ +"""Run built noninteractive checks in an isolated DOSBox-X Tandy guest. + +Opt-in developer tool. Requires your installed emulator; downloads nothing. +Cycle settings qualify functionality, not physical 8088 clock equivalence. +""" +from pathlib import Path +import argparse +import hashlib +import json +import os +import re +import shutil +import subprocess +from build import TESTS + +ROOT = Path(__file__).resolve().parents[1] + + +def main(): + p = argparse.ArgumentParser(description=__doc__) + p.add_argument('--dosbox', default='dosbox-x', + help='DOSBox-X executable or isolated wrapper') + p.add_argument('--model', choices=('small', 'compact', 'medium', 'large'), + default='small') + p.add_argument('--cycles', type=int, default=20000) + p.add_argument('--timeout', type=int, default=600) + p.add_argument('--program', action='append', help='8-character EXE name; repeatable') + a = p.parse_args() + if a.cycles < 1 or a.timeout < 1: + p.error('cycles and timeout must be positive') + names = a.program or [*TESTS, 'DOSAPP', 'GUARD'] + if any(not re.fullmatch(r'[A-Za-z0-9_]{1,8}', n) for n in names): + p.error('program names must be plain 1..8-character DOS base names') + source = ROOT / 'build' / ('dos' if a.model == 'small' else 'dos-' + a.model) + out = ROOT / 'build' / ('qualify-' + a.model) + out.mkdir(parents=True, exist_ok=True) + for n in names: + src = source / (n + '.EXE') + if not src.is_file(): + raise SystemExit('Missing build: ' + str(src)) + shutil.copyfile(src, out / (n.upper() + '.EXE')) + (out / (n.upper() + '.LOG')).unlink(missing_ok=True) + (out / 'RESULTS.TXT').unlink(missing_ok=True) + commands = ['@echo off', 'mount c "' + str(out) + '"', 'c:'] + for n in names: + commands += [n + ' > ' + n + '.LOG', 'if errorlevel 1 goto failed', + 'echo PASS ' + n + ' >> RESULTS.TXT'] + commands += ['echo COMPLETE >> RESULTS.TXT', 'goto done', ':failed', + 'echo FAILED >> RESULTS.TXT', ':done', 'exit'] + cfg = out / 'checks.conf' + cfg.write_text('''[sdl] +fullscreen=false +output=surface +[dosbox] +machine=tandy +memsize=0 +memsizekb=640 +splash logo=false +show basic information=false +[cpu] +core=normal +cputype=8086_prefetch +cycles=fixed %d +fpu=false +[dos] +xms=false +ems=false +umb=false +[dosbox-x] +automount=false +automountall=false +startcmd=false +[mixer] +nosound=true +[midi] +mididevice=none +[joystick] +joysticktype=none +[autoexec] +''' % a.cycles + '\n'.join(commands) + '\n') + manifest = {'status': 'running', 'model': a.model, 'cycles': a.cycles, + 'machine': 'tandy', 'memory_kb': 640, 'programs': names, + 'sha256': {n: hashlib.sha256((out / (n.upper() + '.EXE')).read_bytes()).hexdigest() + for n in names}} + record = out / 'manifest.json' + record.write_text(json.dumps(manifest, indent=2) + '\n') + home = out / 'home' + home.mkdir(exist_ok=True) + env = dict(os.environ, HOME=str(home), XDG_CONFIG_HOME=str(home / '.config'), + SDL_VIDEODRIVER='dummy', SDL_AUDIODRIVER='dummy') + try: + with (out / 'emulator.log').open('w') as log: + run = subprocess.run([a.dosbox, '-conf', str(cfg)], cwd=out, + stdout=log, stderr=subprocess.STDOUT, + env=env, timeout=a.timeout + 10) + result = (out / 'RESULTS.TXT').read_text() if (out / 'RESULTS.TXT').exists() else '' + passed = run.returncode == 0 and 'COMPLETE' in result and 'FAILED' not in result + manifest.update(status='passed' if passed else 'failed', + exit_code=run.returncode, results=result) + except BaseException as error: + manifest.update(status='interrupted' if isinstance(error, KeyboardInterrupt) else 'failed', error=str(error)) + record.write_text(json.dumps(manifest, indent=2) + '\n') + raise + record.write_text(json.dumps(manifest, indent=2) + '\n') + print(result, end='') + if not passed: + raise SystemExit('DOS checks failed; inspect ' + str(out)) + + +if __name__ == '__main__': + main() diff --git a/tools/qualify_guard.py b/tools/qualify_guard.py new file mode 100644 index 0000000..98f3b32 --- /dev/null +++ b/tools/qualify_guard.py @@ -0,0 +1,87 @@ +"""Build/run optional DOS guard checks in all four 8088 memory models. + +Opt-in. Uses installed Open Watcom and DOSBox-X; downloads nothing. +Software interrupts only: no real device errors are injected. +""" +from pathlib import Path +import argparse, json, os, subprocess, hashlib +ROOT=Path(__file__).resolve().parents[1] +def main(): + p=argparse.ArgumentParser(description=__doc__) + p.add_argument('--watcom',type=Path,required=True) + p.add_argument('--dosbox',default='dosbox-x') + a=p.parse_args();w=a.watcom.resolve() + out=ROOT/'build'/'guard-matrix';out.mkdir(parents=True,exist_ok=True) + env=dict(os.environ) + if os.name=='nt': + bindir=w/'BINNT';cc=bindir/'wcl.exe';env.update(WATCOM=str(w),INCLUDE=str(w/'H'),LIB=str(w/'LIB286')+';'+str(w/'LIB286/DOS')) + else: + bindir=w/('binl64' if (w/'binl64/wcl').exists() else 'binl');cc=bindir/'wcl' + env.update(WATCOM=str(w),INCLUDE=str(w/'h'),LIB=str(w/'lib286')+os.pathsep+str(w/'lib286/dos')) + env['PATH']=str(bindir)+os.pathsep+env.get('PATH','') + record={'status':'building','tests':[]} + def run(args,expected=0): + r=subprocess.run(list(map(str,args)),cwd=out,env=env,capture_output=True,text=True,timeout=60) + record['tests'].append({'argv':list(map(str,args)),'exit':r.returncode,'stdout':r.stdout,'stderr':r.stderr}) + if (r.returncode==0)!=(expected==0): raise RuntimeError(r.stdout+r.stderr) + try: + for model in 'scml': + flags=[cc,'-q','-bt=dos','-0','-m'+model,'-ecc','-ox','-w4','-wx','-i='+str(ROOT/'include')] + run([*flags,'-s','-zu','-zt=0','-c','-fo=guard'+model+'.obj',ROOT/'adapters/lt_guard_dos.c']) + run([*flags,'-s','-k4096','-fe=GUARD'+model.upper()+'.EXE',ROOT/'tests/guard_dos.c','guard'+model+'.obj']) + for absent in ('-s','-zu'): + flags=[cc,'-q','-bt=dos','-0','-ml','-ecc','-s','-zu','-i='+str(ROOT/'include'),'-c','-fo=invalid.obj',ROOT/'adapters/lt_guard_dos.c'] + run([x for x in flags if x!=absent],expected=1) + commands=['@echo off','mount c "'+str(out)+'"','c:'] + for model in 'SCML': + (out/('GUARD'+model+'.LOG')).unlink(missing_ok=True) + commands+=['GUARD'+model+' > GUARD'+model+'.LOG','if errorlevel 1 goto failed'] + commands+=['echo COMPLETE > RESULT.TXT','goto done',':failed','echo FAILED > RESULT.TXT',':done','exit'] + (out/'RESULT.TXT').unlink(missing_ok=True) + cfg=out/'guard.conf' + cfg.write_text('''[sdl] +fullscreen=false +output=surface +[dosbox] +machine=tandy +memsize=0 +memsizekb=640 +splash logo=false +show basic information=false +[cpu] +core=normal +cputype=8086_prefetch +cycles=fixed 20000 +fpu=false +[dos] +xms=false +ems=false +umb=false +[dosbox-x] +automount=false +automountall=false +startcmd=false +[mixer] +nosound=true +[midi] +mididevice=none +[joystick] +joysticktype=none +[autoexec] +'''+ '\n'.join(commands)+'\n') + home=out/'home';home.mkdir(exist_ok=True) + env.update(HOME=str(home),XDG_CONFIG_HOME=str(home/'.config'),SDL_VIDEODRIVER='dummy',SDL_AUDIODRIVER='dummy') + with (out/'emulator.log').open('w') as log: + r=subprocess.run([a.dosbox,'-conf',str(cfg)],cwd=out,env=env,stdout=log,stderr=subprocess.STDOUT,timeout=60) + if r.returncode or not (out/'RESULT.TXT').exists() or 'COMPLETE' not in (out/'RESULT.TXT').read_text(): + raise RuntimeError('DOS guard matrix failed; inspect '+str(out)) + record['status']='passed' + record['logs']={m:(out/('GUARD'+m+'.LOG')).read_text() for m in 'SCML'} + record['sha256']={m:hashlib.sha256((out/('GUARD'+m+'.EXE')).read_bytes()).hexdigest() for m in 'SCML'} + for value in record['logs'].values():print(value.strip()) + except Exception as error: + record.update(status='failed',error=str(error)) + raise + finally: + (out/'manifest.json').write_text(json.dumps(record,indent=2)+'\n') +if __name__=='__main__':main()