From d97d173183b7f809eec2f767b04a744f857d5618 Mon Sep 17 00:00:00 2001 From: Chad Date: Tue, 27 Oct 2026 09:40:41 -0500 Subject: [PATCH 1/3] Add optional foreground game engine and native text/Tandy demos --- README.md | 2 + adapters/lt_engine_dos.c | 45 ++++++++ docs/ENGINE-VALIDATION.md | 116 ++++++++++++++++++++ docs/ENGINE.md | 184 ++++++++++++++++++++++++++++++++ examples/engine_demo.c | 132 +++++++++++++++++++++++ examples/engine_text_demo.c | 3 + include/lt_engine.h | 81 ++++++++++++++ include/lt_engine_dos.h | 16 +++ include/lt_engine_view.h | 25 +++++ src/lt_engine.c | 204 ++++++++++++++++++++++++++++++++++++ src/lt_engine_view.c | 70 +++++++++++++ tests/engine_core.c | 167 +++++++++++++++++++++++++++++ tests/engine_dos.c | 44 ++++++++ tests/engine_view.c | 42 ++++++++ tools/build.py | 10 ++ 15 files changed, 1141 insertions(+) create mode 100644 adapters/lt_engine_dos.c create mode 100644 docs/ENGINE-VALIDATION.md create mode 100644 docs/ENGINE.md create mode 100644 examples/engine_demo.c create mode 100644 examples/engine_text_demo.c create mode 100644 include/lt_engine.h create mode 100644 include/lt_engine_dos.h create mode 100644 include/lt_engine_view.h create mode 100644 src/lt_engine.c create mode 100644 src/lt_engine_view.c create mode 100644 tests/engine_core.c create mode 100644 tests/engine_dos.c create mode 100644 tests/engine_view.c diff --git a/README.md b/README.md index fac03a4..49566c6 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ Windows 3.0 USER/GDI project named libwintandy. | Modules | Contract | | --- | --- | +| `lt_engine.h` (optional) | Caller-owned objects, Awake/Start/FixedUpdate/Update/LateUpdate, bounded foreground catch-up, AABB/tile collision and camera. Rendering-independent simulation; [engine guide and DOS demos](docs/ENGINE.md). | +| `lt_engine_view.h`, `lt_engine_dos.h` (optional) | Explicit native Tandy 160x200/320x200 packed layouts and 40/80-column text; optional mode/page/cursor setup/restore. No timer or sound ownership. | | `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. | diff --git a/adapters/lt_engine_dos.c b/adapters/lt_engine_dos.c new file mode 100644 index 0000000..56158b2 --- /dev/null +++ b/adapters/lt_engine_dos.c @@ -0,0 +1,45 @@ +/* SPDX-License-Identifier: MIT */ +#if !defined(__WATCOMC__) || !defined(__I86__) +#error lt_engine_dos requires 16-bit Open Watcom +#endif +#include +#include +#include "lt_engine_dos.h" +static void bios(unsigned ax, unsigned bx, unsigned cx, unsigned dx) +{ + union REGS r; + memset(&r, 0, sizeof(r)); + r.x.ax = ax; r.x.bx = bx; r.x.cx = cx; r.x.dx = dx; + int86(0x10, &r, &r); +} +void lte_dos_video_close(lte_dos_video *v) +{ + if (!v || !v->active) return; + bios(v->mode, 0, 0, 0); + bios(0x0500U | v->page, 0, 0, 0); + bios(0x0100U, 0, ((unsigned)v->cursor_start << 8) | v->cursor_end, 0); + bios(0x0200U, (unsigned)v->page << 8, 0, + ((unsigned)v->row << 8) | v->col); + v->active = 0; v->view.pixels = 0; +} +int lte_dos_video_open(lte_dos_video *v, unsigned mode) +{ + union REGS r; + lte_view view; + if (!v || v->active || !lte_view_bind(&view, mode, + (unsigned char LTE_FAR *)MK_FP(0xb800, 0), 32768U)) return 0; + memset(&r, 0, sizeof(r)); r.h.ah = 15; + int86(0x10, &r, &r); v->mode = r.h.al; v->page = r.h.bh; + memset(&r, 0, sizeof(r)); r.h.ah = 3; r.h.bh = v->page; + int86(0x10, &r, &r); + v->cursor_start = r.h.ch; v->cursor_end = r.h.cl; + v->row = r.h.dh; v->col = r.h.dl; v->active = 1; + bios(mode, 0, 0, 0); + memset(&r, 0, sizeof(r)); r.h.ah = 15; + int86(0x10, &r, &r); + if (r.h.al != mode) { lte_dos_video_close(v); return 0; } + bios(0x0500, 0, 0, 0); /* page zero matches the borrowed B800 aperture */ + bios(0x0100, 0, 0x2000, 0); + v->view = view; + return 1; +} diff --git a/docs/ENGINE-VALIDATION.md b/docs/ENGINE-VALIDATION.md new file mode 100644 index 0000000..1f62bb4 --- /dev/null +++ b/docs/ENGINE-VALIDATION.md @@ -0,0 +1,116 @@ +# Engine validation — 2026-10-27 + +Candidate starts from public main `a034067` (merged compact-audio changes). +The engine is optional and no existing sound or graphics API was changed. +This record covers the C89 foreground engine, its explicit native presenters, +and the original graphics/text demonstration route; it is not a hardware +performance certification. + +## Checks + +- Managed UCRT64 GCC: all 16 host test executables passed, including the new + lifecycle/physics and presentation tests and the existing audio/graphics + suite. Strict C89, pedantic errors, warnings as errors, `-O2`. +- Open Watcom 1.9: normal complete DOS build passed with `-0 -ms -ecc -os -s + -w4 -wx`, 4096-byte demo stacks. New modules/tests produced no diagnostics. + The build still reports the preexisting always-false comparison in + `lt_time.c` and unreachable code in `gfx_host.c`. +- Focused native execution: fixed 86Box build, Tandy 1000 HX, 8088 at + 4,772,728 Hz, 640 KiB, copied DOS disk. ENGINE and ENGVIEW passed with + actual 16-bit types/far storage. ENGDOS passed mode selection and original + mode/page/cursor restoration for all four modes, invalid-mode rejection, + double-open/idempotent-close checks, and unchanged IRQ0/IRQ1 vectors. +- All five finite demonstration routes returned success to DOS. The BDA + reported the expected BIOS modes during execution; early/late headless + framebuffer captures were retained for each route. +- Independent reviewer replayed host engine/presentation binaries and reviewed + code and docs before local commit. Findings led to fixed-tick-only lifecycle + activation (including nested spawn chains/zero-step frames), correct far + fixture initialization, native warning guards, leading-edge collision + checks, and packed span/bank-address optimization. Final review found no + remaining blocker. + +| Run | Mode | Fixed ticks | Frames | Final debt | X/Y hits | Furthest camera X | Final body X,Y | +| --- | --- | --- | --- | --- | --- | --- | --- | +| ENGDEMO /auto | 09h, 320x200x16 | 300 | 76 | 0 | 1 / 141 | 316 | 330,176 | +| ENGDEMO /auto /low | 08h, 160x200x16 | 300 | 76 | 0 | 1 / 141 | 396 | 330,176 | +| TXTDEMO /auto | 03h, 80x25 | 300 | 76 | 0 | 1 / 141 | 128 | 330,176 | +| TXTDEMO /auto /40 | 01h, 40x25 | 300 | 76 | 0 | 1 / 141 | 312 | 330,176 | +| ENGDEMO /auto /slow | 09h, three-tick foreground wait | 300 | 76 | 0 | 1 / 141 | 314 | 330,176 | + +Awake/Start counts were 1/1 in every route; Update/LateUpdate counts matched. +The slow wait did not further reduce frame count on this emulator because +the viewport rendering already exceeded that wait. Determinism across +different frame partitions is directly tested in the portable/native core +test: 30 single-tick frames versus budgeted catch-up to the same 30 ticks; +also FixedUpdate -> Awake -> grandchild spawn chains with extra zero-step +frames versus one three-tick frame. Camera maxima may vary with LateUpdate +sampling; simulation results do not. + +An initial 240-second exploratory run timed out during the unoptimized +graphics route. A second exploratory run was deliberately stopped after the +renderer bottleneck was identified. Both owned emulator children stopped +and released the shared byte-range lease. The final optimized suite reached +DONE, stopped its child and released that same lease. No unrelated emulator, +disk image or panning source was modified. The raw headless graphics captures +can include stale pixels outside the active 200-row scene; those pixels are +not evidence about the display adapter. Bank/padding/nibble correctness is +also checked directly by the native far-buffer presentation test. + +## Native cost + +Measured from Open Watcom OMF SEGDEF records, not object-file length: + +| Optional module | 8088 CODE bytes | Module global data bytes | +| --- | --- | --- | +| `lt_engine.c` | 2089 | 0 | +| `lt_engine_view.c` | 622 | 0 | +| `lt_engine_dos.c` | 447 | 0 | +| Total | 3158 | 0 | + +These figures exclude shared C runtime helpers (integer arithmetic, +`memset`, far fill and BIOS wrappers) and linker alignment. Application cost +depends on which helpers it already uses. Omitting the modules adds no +engine code or state to existing applications. + +Actual native `sizeof`: world 18 bytes, slot 6, behavior table 10, body 8, +camera 8, tilemap descriptor 14, view 14, DOS video object 22. Eight slots and +one world therefore require 66 bytes, plus caller data/behavior tables. +The demo tile cells require 2400 bytes; no engine heap or interrupt stack. + +ENGDEMO.EXE is 15,912 bytes and TXTDEMO.EXE 15,910 bytes. MZ loader minimum +image/allocation plus PSP is respectively 22,976 and 22,960 bytes (rounding +the load image up to paragraphs before adding minimum allocation), including +the linked 4096-byte stack and demo/static/CRT data, excluding the environment +and DOS bookkeeping. Both EXEs retain the compiler's default maximum-memory +request; these header minima are not a tested low-memory startup threshold. +The display apertures are 32 KiB for mode 9, 16 KiB for mode 8, and 4000/2000 +bytes for text. The engine allocates no offscreen framebuffer. + +## Reproduction and evidence + +```powershell +python tools/build.py --target host --gcc-launcher C:/Users/chad/Documents/Codex/toolchains/gcc-ucrt64.py --runtime-bin C:/msys64/ucrt64/bin +python tools/build.py --target dos --watcom C:/WATCOM +``` + +Run ENGINE, ENGVIEW and ENGDOS in DOS on compatible Tandy hardware, then the +five commands above. Consumers do not need Python or converted assets. + +Developer evidence on the validation host: +`C:/Users/chad/Documents/Codex/2026-10-27/task/native-engine-03/RESULT.json` +and adjacent CORE/VIEW/VIDEO/G320/G160/T80/T40/SLOW logs and PNGs. +Host/DOS compiler invocation manifests are under `build/host/BUILD.json` and +`build/dos/BUILD.json`; OMF segment totals are `build/dos/ENGINE-COST.json`. +The machine-local developer harness and measurement script are outside the +repository and are not consumer dependencies. + +Final native binary SHA-256: + +```text +ENGINE.EXE e88b1480bcfa405499b20138e0fd59400a8714ea7a03e4b026e654a143efb703 +ENGVIEW.EXE 48e67d3b46992d6e0c7ec9216094a6dcd3bd1fef6922cb70f777b6d2d75cd622 +ENGDOS.EXE dcfbfdc2c9f0f03c3209202165647846af6606795017c1277eb5dfcefac8d25b +ENGDEMO.EXE 5f3f80eaa62734c6949d61c798a917c6ae790b2ff544d388320a108f103660de +TXTDEMO.EXE 2c5be6da695efe692bbcebf2bce43082572a8971fc8c27915aec57b45c763c37 +``` diff --git a/docs/ENGINE.md b/docs/ENGINE.md new file mode 100644 index 0000000..f728c6c --- /dev/null +++ b/docs/ENGINE.md @@ -0,0 +1,184 @@ +# Optional foreground game engine + +`lt_engine.h` adds small C89 game-loop conveniences without changing any +existing API. Compile `src/lt_engine.c` only when wanted. It owns no heap, +clock, interrupt, sound channel or display. Caller-owned object slots hold a +behavior table and user pointer; the user pointer can refer to any game data, +including a physics body. Simulation units need not be pixels. + +## Lifecycle and time + +```c +static lte_object slots[8]; +static lte_world game; +static const lte_behavior actor = { awake, start, fixed, update, late }; +/* Each callback: void callback(lte_world *w, lte_object *o). */ +lte_init(&game, slots, 8, 10, 4); +lte_add(&game, &actor, &my_actor_data); +/* Foreground, once per drawn frame; time units are chosen by the caller. */ +lte_frame(&game, elapsed_since_previous_frame); +``` + +The period here is 10 caller time units. Each frame adds elapsed time to +`game.debt`, then runs at most four complete fixed passes. For each pass: + +1. Reclaim removed slots and snapshot pending objects. +2. Call all pending **Awake**, in slot order. +3. Call all surviving pending **Start**, in slot order. +4. Call all active **FixedUpdate**, in slot order, then increment `game.ticks`. + +After the fixed passes, call all active **Update**, then all active +**LateUpdate**, once each in slot order. Awake/Start run exactly once per +surviving object, before its first FixedUpdate. Removing an object during +Awake suppresses its Start. Zero-step frames run Update/LateUpdate +on existing active objects but do not activate or reclaim objects. Keeping +lifecycle transitions at fixed-tick boundaries makes simulation spawn chains +independent of rendering cadence, including extra zero-elapsed frames. + +Creation inside any callback becomes eligible at the next fixed-tick boundary, +never halfway through the current pass. This includes creation inside +Awake/Start. Removal immediately suppresses all later callbacks; the dead +slot cannot be reused until the next fixed-tick boundary. `lte_add` returns +NULL when storage is full. Never retain an object pointer after removal: +slots are reusable and pointers are not generation handles. Behavior tables, +user data and the slot array must outlive their objects. Treat world/object +internals as read-only except your own `user` data. Do not initialize a world +again inside a callback. Reentrant `lte_frame` returns -1. + +Fixed callbacks run in the foreground, never from the engine's own ISR. +Slow drawing retains debt and catches up within the budget on later frames; +it never silently changes the fixed period. A 32-bit time accumulator +saturates on overflow and sets sticky `game.saturated`, making lost time +observable. Ticks wrap modulo 2^32. For a pause, stop advancing caller time +or deliberately rebase the caller's clock. The engine does not infer pauses. +Deterministic simulation belongs in FixedUpdate with tick-indexed inputs; +Update/LateUpdate run per rendered frame and are not a deterministic clock. + +The callback budget limits the number of passes, each visiting at most the +slot capacity. It cannot bound arbitrary user callback code. A long stall can +leave a visible backlog: choose a period/budget appropriate to the 8088 and +monitor debt. There are no threads, global state or unbounded catch-up loops. +Do not call the engine from an interrupt or concurrently. The existing +`lttk_work` API is unchanged; do not install game callbacks there to drive +this engine. A foreground caller may sample `lttk_ticks()` if it separately +owns that optional clock, or simply use the unchanged BIOS clock as the demos do. + +## Collision and camera + +`lte_box` is an integer half-open AABB. `lte_overlap` tests object/object +overlap: a shared edge is not a collision. It does not automatically resolve +object pairs or provide rigid-body dynamics, mass, rotation, impulses or a +broad phase. A game chooses its pair list and response. + +`lte_move_tiles` resolves X then Y, one world unit at a time, against tile +cells whose masked bits are nonzero. It validates the original body, then +tests each new leading edge, preventing tunneling through a one-cell wall. +Outside the map is solid. It returns `LTE_HIT_X`/`LTE_HIT_Y`; the caller can +stop or reverse velocity, apply gravity, or jump on a downward Y hit. Bodies +already inside solids are rejected unchanged rather than guessed out of them. +Invalid map/body/displacement returns `LTE_MOVE_INVALID` without mutation. + +Coordinates are -16384..16383, body sizes 1..1024, displacement -256..256 per +axis per call. Maps have at most 65535 cells and at most 16384 world units +per axis. Cells and video buffers may be far pointers on Watcom, but each +array must remain within one 64 KiB segment; no huge-pointer traversal. +Object storage and behavior/user near pointers must fit the chosen memory +model/DGROUP. No per-object body or sprite is imposed. + +Collision work is bounded by the initial overlapped tile rectangle plus +`abs(dx)` leading columns and `abs(dy)` leading rows. A conservative probe +bound is `C*R + abs(dx)*R + abs(dy)*C`, where +`C=ceil((body.w-1)/tile_w)+1`, `R=ceil((body.h-1)/tile_h)+1`. +Typical demo bodies/tiles are 8x8 with displacement 2 horizontally and at +most 6 vertically. Huge bodies on 1-unit tiles remain expensive on an 8088. + +`lte_camera_set` clamps a viewport to world bounds, pinning oversized +viewports to zero. `lte_visible_tiles` visits only the visible map cells, +row-major, with camera-relative rectangles. Partially visible edge tiles may +have negative coordinates; the presenter clips them. The map is bounded, +not wrapping. Drawing the visible viewport each frame is software tilemap +scrolling; there is no new hardware panning/page-flip or smooth-frame-rate claim. + +## Actual display coverage + +`lt_engine_view.h` / `src/lt_engine_view.c` are separate optional presenters. +They bind caller-owned memory and clip colored rectangles; they never set a +mode themselves. Glyphs/attributes are supplied for text rectangles. + +| BIOS mode | Native display | Memory layout | Bytes to bind | +| --- | --- | --- | --- | +| 09h | Tandy 320x200, 16 colors | four 8 KiB banks, 160 bytes/row | 32768 | +| 08h | Tandy 160x200, 16 colors | two 8 KiB banks, 80 bytes/row | 16384 | +| 03h | color text 80x25 | character/attribute pairs, 160 bytes/row | 4000 | +| 01h | color text 40x25 | character/attribute pairs, 80 bytes/row | 2000 | + +In graphics, even pixels use the high nibble; row bank is `y % banks`, +row within bank is `y / banks`. Padding is untouched. Text presentation +uses character cells, so the demo scales the same 8-unit world tile to one +cell and aligns its camera to 8 units. Physics and gravity remain identical. +Text's coarser display naturally hides fractional-cell movement. + +`lt_engine_dos.h` / `adapters/lt_engine_dos.c` optionally save the BIOS +mode/page/cursor, enter one of these modes at page zero, bind B800:0000 and +restore the saved state on close or failed mode verification. Initialize +with `LTE_DOS_VIDEO_INITIALIZER`, keep the object at its original address, +and pair each successful open with close. Repeated close is safe. The caller +asserts compatible Tandy hardware for 08h/09h; the BIOS mode query verifies +selection, not adapter identity. This resets the BIOS palette; it does not +save prior pixels, custom palette or arbitrary registers. Exclusive video +ownership is required. No audio or timer resources are acquired. + +The preexisting `lt_gfx` raster remains specific to 320x200 mode 9. The new +160x200 support is an explicit two-bank packed presenter, not a claim that +INDEX8 RAM surfaces select a native mode. CGA 04h/06h, 160x100 text tricks, +Tandy 0Ah, monochrome text and other resolutions have no new adapter here. +The simulation and camera can still be reused with a caller's renderer. + +## Ready-to-run DOS demonstrations + +Build with the normal Open Watcom helper: + +```powershell +python tools/build.py --target dos --watcom C:/WATCOM +``` + +Python is only a developer build helper. A consumer runs the EXEs in DOS +on compatible hardware without Python, an asset converter or PC preparation. +The sources can also be compiled directly with matching 8088 small-model +flags; see the source list in `tools/build.py`. + +```text +wcl -q -bt=dos -0 -ms -ecc -os -s -i=include -k4096 -fe=ENGDEMO.EXE examples/engine_demo.c src/lt_engine.c src/lt_engine_view.c adapters/lt_engine_dos.c src/lt_time.c adapters/lt_time_dos.c +``` + +```text +ENGDEMO Tandy mode 9, scrolling/collision/gravity +ENGDEMO /low Tandy native mode 8, 160x200 +TXTDEMO 80x25 text, same simulation +TXTDEMO /40 40x25 text, same simulation +``` + +A/D or arrows set horizontal velocity; Space jumps while grounded; Escape +restores video and quits. This small example uses BIOS press events, not +held keys. No sound is played. The map contains repeating colored landmarks, +a floor and a tall blocking wall. The camera follows the moving body through +the 768-unit world. There is no sprite asset or loading step. + +Add `/auto` for an automatic 300-tick route that jumps, hits the wall, +reverses, and prints its collision counts, furthest camera position and +lifecycle counts after returning to DOS. Add `/slow` to wait three BIOS +ticks between foreground frames. Physics still uses one BIOS tick per fixed +step (about 18.2 Hz), with up to four steps per frame. The demo caps its +finite route at exactly 300 ticks even if the caller samples a larger stall. +Direct viewport redraw can tear and will be slow on an 8088; this is a small +correctness/convenience example, not a high-frame-rate platformer benchmark. + +Host tests cover ordering, callback removal/addition, nested-frame rejection, +slow-frame debt, deterministic spawn chains with zero-step frames, half-open +collision edges, tunneling, invalid movement, camera bounds and partial +tiles. Presentation tests check native bank addresses, nibble preservation, +clipping and text attributes in both column counts. DOS builds compile those +same tests with native 16-bit types and far pointers. + +See [the validation and cost record](ENGINE-VALIDATION.md) for the measured +8088 sizes, native mode runs and their practical limits. diff --git a/examples/engine_demo.c b/examples/engine_demo.c new file mode 100644 index 0000000..c505808 --- /dev/null +++ b/examples/engine_demo.c @@ -0,0 +1,132 @@ +/* SPDX-License-Identifier: MIT */ +/* ENGDEMO [/low] [/auto] [/slow], TXTDEMO [/40] [/auto] [/slow]. + * Same world and physics in both executables; BIOS clock/keyboard only. + * Arrows/A/D set horizontal velocity, Space jumps, Escape quits. + * Automatic route stops after 300 fixed ticks and prints evidence in DOS. */ +#include +#include +#include +#include "lt_engine_dos.h" +#include "lt_time_dos.h" +#define MAP_W 96 +#define MAP_H 25 +static unsigned char cells[MAP_W * MAP_H]; +static lte_tilemap map = {cells, MAP_W, MAP_H, 8, 8, 128}; +static lte_dos_video video = LTE_DOS_VIDEO_INITIALIZER; +static lte_world world; +static lte_object objects[2]; +static lte_box player = {16,160,8,8}; +static lte_camera camera; +static short vx = 2, vy; +static unsigned scale = 1, automatic, grounded, hit_x, hit_y, max_camera; +static unsigned awake_count, start_count, updates, late_count; +static void awake(lte_world *w, lte_object *o) +{ (void)w; (void)o; ++awake_count; } +static void start(lte_world *w, lte_object *o) +{ (void)w; (void)o; ++start_count; } +static void fixed(lte_world *w, lte_object *o) +{ + int hit; + (void)o; + if (automatic && grounded && w->ticks % 20UL == 0) vy = -6; + if (vy < 4) ++vy; + hit = lte_move_tiles(&map, &player, vx, vy); + grounded = (hit & LTE_HIT_Y) && vy > 0; + if (hit & LTE_HIT_X) { ++hit_x; if (automatic) vx = (short)-vx; } + if (hit & LTE_HIT_Y) { ++hit_y; vy = 0; } +} +static void update(lte_world *w, lte_object *o) +{ (void)w; (void)o; ++updates; } +static void late(lte_world *w, lte_object *o) +{ + short x, width = (short)(video.view.width * scale); + (void)w; (void)o; ++late_count; + x = (short)(player.x + player.w / 2 - width / 2); + /* Text camera is cell-aligned. Simulation coordinates remain unchanged. */ + if (x > 0) x = (short)(x / (short)scale * (short)scale); + lte_camera_set(&camera, x, 0, width, 200, MAP_W * 8, MAP_H * 8); + if ((unsigned)camera.x > max_camera) max_camera = (unsigned)camera.x; +} +static const lte_behavior behavior = {awake, start, fixed, update, late}; +static void tile(void *user, unsigned char value, short x, short y, + short w, short h) +{ + lte_view *v = (lte_view *)user; + unsigned char ink = value & 15; + if (value == 1) return; /* common sky is one large span fill per frame */ + if (scale == 1) lte_view_rect(v, x, y, w, h, ink, 0); + else lte_view_rect(v, (short)(x / 8), (short)(y / 8), + (short)(w / 8), (short)(h / 8), + (unsigned char)(ink << 4), ' '); +} +int main(int argc, char **argv) +{ + unsigned mode, slow = 0, x, y; + int i, quit = 0, key, ok; + unsigned long previous, now, elapsed; +#ifdef ENGINE_TEXT + mode = LTE_VIEW_TEXT80; scale = 8; +#else + mode = LTE_VIEW_TANDY320; +#endif + for (i = 1; i < argc; ++i) { + if (!strcmp(argv[i], "/auto")) automatic = 1; + else if (!strcmp(argv[i], "/slow")) slow = 1; +#ifdef ENGINE_TEXT + else if (!strcmp(argv[i], "/40")) mode = LTE_VIEW_TEXT40; +#else + else if (!strcmp(argv[i], "/low")) mode = LTE_VIEW_TANDY160; +#endif + else { puts("Use /auto /slow and /low (graphics) or /40 (text)"); return 1; } + } + for (y = 0; y < MAP_H; ++y) + for (x = 0; x < MAP_W; ++x) + cells[y * MAP_W + x] = (unsigned char)( + y >= 23 ? 128U | (2U + (x % 3U)) : + (x == 60 && y >= 16 ? 128U | 6U : 1U)); + /* Decorative stripes expose camera movement without external assets. */ + for (x = 0; x < MAP_W; x += 8) cells[10 * MAP_W + x] = 3; + if (!lte_init(&world, objects, 2, 1, 4) || + !lte_add(&world, &behavior, 0)) return 1; + if (!lte_dos_video_open(&video, mode)) { + puts("Requested video mode unavailable (8/9 require Tandy hardware)"); + return 1; + } + previous = ltd_raw_ticks(); + while (!quit && (!automatic || world.ticks < 300UL)) { + now = ltd_raw_ticks(); + elapsed = now >= previous ? now - previous : LTT_DAY_TICKS - previous + now; + if (elapsed < (slow ? 3UL : 1UL)) continue; + previous = now; + if (automatic) { + unsigned long remaining = 300UL - world.ticks; + if (world.debt >= remaining) elapsed = 0; + else if (elapsed > remaining - world.debt) elapsed = remaining - world.debt; + } + while (kbhit()) { + key = getch(); + if (!key) key = 256 + getch(); + if (key == 27) quit = 1; + if (key == 'a' || key == 256 + 75) vx = -2; + if (key == 'd' || key == 256 + 77) vx = 2; + if (key == ' ' && grounded) vy = -6; + } + lte_frame(&world, elapsed); + lte_view_rect(&video.view, 0, 0, (short)video.view.width, + (short)video.view.height, (unsigned char)(scale == 1 ? 1 : 0x10), ' '); + lte_visible_tiles(&map, &camera, tile, &video.view); + lte_view_rect(&video.view, + (short)((player.x - camera.x) / (short)scale), + (short)(player.y / (short)scale), + (short)(player.w / (short)scale), (short)(player.h / (short)scale), + (unsigned char)(scale == 1 ? 15 : 0x1f), '@'); + } + lte_dos_video_close(&video); + ok = !automatic || (awake_count == 1 && start_count == 1 && + updates == late_count && hit_x && hit_y && max_camera && !world.saturated); + printf("ENGINE mode=%u ticks=%lu frames=%u debt=%lu awake/start=%u/%u " + "hits=%u/%u camera=%u xy=%d,%d %s\n", mode, world.ticks, updates, + world.debt, awake_count, start_count, hit_x, hit_y, max_camera, + player.x, player.y, ok ? "PASS" : "FAIL"); + return ok ? 0 : 1; +} diff --git a/examples/engine_text_demo.c b/examples/engine_text_demo.c new file mode 100644 index 0000000..62d5f1f --- /dev/null +++ b/examples/engine_text_demo.c @@ -0,0 +1,3 @@ +/* SPDX-License-Identifier: MIT */ +#define ENGINE_TEXT +#include "engine_demo.c" diff --git a/include/lt_engine.h b/include/lt_engine.h new file mode 100644 index 0000000..753d323 --- /dev/null +++ b/include/lt_engine.h @@ -0,0 +1,81 @@ +/* SPDX-License-Identifier: MIT */ +#ifndef LT_ENGINE_H +#define LT_ENGINE_H +#include +#if UCHAR_MAX != 255 || USHRT_MAX != 65535U || LONG_MAX < 2147483647L +#error lt_engine requires 8-bit bytes, 16-bit shorts and 32-bit longs +#endif +#if defined(__WATCOMC__) +#define LTE_FAR __far +#else +#define LTE_FAR +#endif + +typedef struct lte_world lte_world; +typedef struct lte_object lte_object; +typedef void (*lte_callback)(lte_world *, lte_object *); +typedef struct { + lte_callback awake, start, fixed_update, update, late_update; +} lte_behavior; +struct lte_object { + const lte_behavior *behavior; + void *user; + unsigned char state; /* private: use add/remove, never change directly */ +}; +struct lte_world { + lte_object *objects; + unsigned capacity, period, budget; + unsigned long debt, ticks; + unsigned char busy, saturated; +}; +/* Storage remains at this address; near data must fit the caller's memory + * model. Init erases storage. period is in caller-defined time units, 1..65535; + * budget is 1..255 fixed passes per frame. No allocation or timer/ISR. */ +int lte_init(lte_world *, lte_object *, unsigned capacity, + unsigned period, unsigned budget); +/* Pending objects activate at the NEXT fixed-tick boundary, including during + * catch-up. Zero-step frames never activate objects. Slots are stable until + * removal; do not retain a pointer after removal. Dead slots are reclaimed + * at the next fixed-tick boundary. Behavior/user must outlive the object. */ +lte_object *lte_add(lte_world *, const lte_behavior *, void *user); +int lte_remove(lte_world *, lte_object *); +/* Boundary order: reclaim, all Awake, all Start. Frame order: 0..budget + * complete [boundary, FixedUpdate] passes, all Update, all LateUpdate. + * Awake/Start run once, before the object's first FixedUpdate; no Update or + * LateUpdate for pending objects. Removal suppresses remaining callbacks + * immediately. Recursion is rejected. Debt is retained under slow rendering; + * 32-bit saturation is sticky/observable, never an unbounded catch-up loop. + * elapsed <= 0xffffffff; returns steps run, or -1 for invalid/reentrant call. */ +int lte_frame(lte_world *, unsigned long elapsed); + +/* World units are integers, independent of pixels/cells. Valid boxes have + * x/y in -16384..16383 and w/h in 1..1024. Half-open: touching is not overlap. */ +typedef struct { short x, y, w, h; } lte_box; +typedef struct { + const unsigned char LTE_FAR *cells; + unsigned width, height, tile_w, tile_h; + unsigned char solid_mask; /* nonzero masked bits mean solid */ +} lte_tilemap; +typedef struct { short x, y, w, h; } lte_camera; +#define LTE_HIT_X 1 +#define LTE_HIT_Y 2 +#define LTE_MOVE_INVALID 4 +int lte_overlap(const lte_box *, const lte_box *); +/* Maps: nonempty, <=65535 cells, each axis <=16384 world units. Outside is + * solid. Movement resolves X then Y in unit steps (no tunneling), <=256 units + * per axis per call. Caller integrates velocity/gravity in FixedUpdate. + * Invalid input leaves box unchanged, returns LTE_MOVE_INVALID. */ +int lte_map_valid(const lte_tilemap *); +int lte_blocked(const lte_tilemap *, const lte_box *); +int lte_move_tiles(const lte_tilemap *, lte_box *, short dx, short dy); +/* Clamp requested top-left to world; a viewport larger than world pins to 0. + * All sizes positive <=16384, world sizes <=16384. Invalid: unchanged/0. */ +int lte_camera_set(lte_camera *, short x, short y, short w, short h, + short world_w, short world_h); +typedef void (*lte_tile_visit)(void *, unsigned char, short, short, + short, short); +/* Visits visible cells only, row-major, with camera-relative rectangle. + * Partial tiles may extend past viewport: renderer clips. No allocation. */ +int lte_visible_tiles(const lte_tilemap *, const lte_camera *, + lte_tile_visit, void *); +#endif diff --git a/include/lt_engine_dos.h b/include/lt_engine_dos.h new file mode 100644 index 0000000..b30e4b2 --- /dev/null +++ b/include/lt_engine_dos.h @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: MIT */ +#ifndef LT_ENGINE_DOS_H +#define LT_ENGINE_DOS_H +#include "lt_engine_view.h" +typedef struct { + lte_view view; + unsigned char active, mode, page, cursor_start, cursor_end, row, col; +} lte_dos_video; +#define LTE_DOS_VIDEO_INITIALIZER { {0,0,0,0,0,0}, 0,0,0,0,0,0,0 } +/* Foreground Open Watcom DOS only. Caller asserts compatible Tandy hardware + * for modes 8/9 (BIOS mode query is not hardware detection). Saves/restores + * mode/page/cursor, not old screen contents or custom palettes/registers. + * No sound or timer ownership. Exclusive video owner; object must not move. */ +int lte_dos_video_open(lte_dos_video *, unsigned mode); +void lte_dos_video_close(lte_dos_video *); +#endif diff --git a/include/lt_engine_view.h b/include/lt_engine_view.h new file mode 100644 index 0000000..48ed85d --- /dev/null +++ b/include/lt_engine_view.h @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: MIT */ +#ifndef LT_ENGINE_VIEW_H +#define LT_ENGINE_VIEW_H +#include "lt_engine.h" +/* Native B800 layouts only. Binding does not set a BIOS mode. The caller + * supplies the matching aperture or a RAM buffer of at least bytes length. + * Text units are character cells. Mode 8/9 units are pixels. No mode claims + * for INDEX8 RAM surfaces, VGA, CGA 4/6, or Tandy mode 0Ah. */ +#define LTE_VIEW_TEXT40 1 +#define LTE_VIEW_TEXT80 3 +#define LTE_VIEW_TANDY160 8 +#define LTE_VIEW_TANDY320 9 +typedef struct { + unsigned char LTE_FAR *pixels; + unsigned width, height, stride, banks; + unsigned char mode; +} lte_view; +int lte_view_bind(lte_view *, unsigned mode, + unsigned char LTE_FAR *pixels, unsigned bytes); +/* Half-open, clipped rectangle. In graphics ink low nibble is color; + * in text ink is the complete attribute and glyph the character byte. + * No mode setting, port I/O, page flipping, palette ownership or heap. */ +void lte_view_rect(lte_view *, short x, short y, short w, short h, + unsigned char ink, unsigned char glyph); +#endif diff --git a/src/lt_engine.c b/src/lt_engine.c new file mode 100644 index 0000000..8199e93 --- /dev/null +++ b/src/lt_engine.c @@ -0,0 +1,204 @@ +/* SPDX-License-Identifier: MIT */ +#include "lt_engine.h" +#include +#define FREE 0 +#define PENDING 1 +#define READY 2 +#define ACTIVE 3 +#define DEAD 4 +#define MAX_TIME 0xffffffffUL + +int lte_init(lte_world *w, lte_object *objects, unsigned n, + unsigned period, unsigned budget) +{ + if (!w || !objects || !n || n > 65535U / sizeof(*objects) || + !period || !budget || budget > 255U) return 0; +#if UINT_MAX > 65535U + if (period > 65535U) return 0; +#endif + memset(w, 0, sizeof(*w)); + memset(objects, 0, n * sizeof(*objects)); + w->objects = objects; w->capacity = n; + w->period = period; w->budget = budget; + return 1; +} +lte_object *lte_add(lte_world *w, const lte_behavior *b, void *user) +{ + unsigned i; + if (!w || !w->objects || !b) return 0; + for (i = 0; i < w->capacity; ++i) { + lte_object *o = &w->objects[i]; + if (o->state == FREE) { + o->behavior = b; o->user = user; o->state = PENDING; + return o; + } + } + return 0; +} +int lte_remove(lte_world *w, lte_object *o) +{ + unsigned i; + if (!w || !w->objects || !o) return 0; + for (i = 0; i < w->capacity; ++i) { + if (&w->objects[i] == o && o->state != FREE && o->state != DEAD) { + o->state = DEAD; return 1; + } + } + return 0; +} +static void callbacks(lte_world *w, unsigned phase) +{ + unsigned i; + for (i = 0; i < w->capacity; ++i) { + lte_object *o = &w->objects[i]; + lte_callback cb = 0; + const lte_behavior *b = o->behavior; + if ((phase < 2 && o->state != READY) || + (phase >= 2 && o->state != ACTIVE)) continue; + if (phase == 0) cb = b->awake; + else if (phase == 1) { o->state = ACTIVE; cb = b->start; } + else if (phase == 2) cb = b->fixed_update; + else if (phase == 3) cb = b->update; + else cb = b->late_update; + if (cb) cb(w, o); + } +} +static void boundary(lte_world *w) +{ + unsigned i; + for (i = 0; i < w->capacity; ++i) { + if (w->objects[i].state == DEAD) { + memset(&w->objects[i], 0, sizeof(w->objects[i])); + } else if (w->objects[i].state == PENDING) + w->objects[i].state = READY; + } + callbacks(w, 0); callbacks(w, 1); +} +int lte_frame(lte_world *w, unsigned long elapsed) +{ + unsigned steps = 0; + if (!w || !w->objects || !w->period || !w->budget || w->busy) return -1; +#if ULONG_MAX > 0xffffffffUL + if (elapsed > MAX_TIME) return -1; +#endif + w->busy = 1; + if (elapsed > MAX_TIME - w->debt) { + w->debt = MAX_TIME; w->saturated = 1; + } else w->debt += elapsed; + while (w->debt >= w->period && steps < w->budget) { + boundary(w); + w->debt -= w->period; + callbacks(w, 2); + w->ticks = (w->ticks + 1UL) & MAX_TIME; + ++steps; + } + callbacks(w, 3); callbacks(w, 4); + w->busy = 0; + return (int)steps; +} +static int box_valid(const lte_box *b) +{ + return b && b->x >= -16384 && b->x <= 16383 && + b->y >= -16384 && b->y <= 16383 && + b->w > 0 && b->w <= 1024 && b->h > 0 && b->h <= 1024; +} +int lte_overlap(const lte_box *a, const lte_box *b) +{ + return box_valid(a) && box_valid(b) && + (long)a->x < (long)b->x + b->w && + (long)b->x < (long)a->x + a->w && + (long)a->y < (long)b->y + b->h && + (long)b->y < (long)a->y + a->h; +} +int lte_map_valid(const lte_tilemap *m) +{ + return m && m->cells && m->width && m->height && + m->tile_w && m->tile_h && m->width <= 16384U && + m->height <= 16384U && m->tile_w <= 16384U && + m->tile_h <= 16384U && + (unsigned long)m->width * m->height <= 65535UL && + (unsigned long)m->width * m->tile_w <= 16384UL && + (unsigned long)m->height * m->tile_h <= 16384UL; +} +static int blocked(const lte_tilemap *m, const lte_box *b) +{ + unsigned x, y, x0, y0, x1, y1; + if (b->x < 0 || b->y < 0 || + (long)b->x + b->w > (long)m->width * (long)m->tile_w || + (long)b->y + b->h > (long)m->height * (long)m->tile_h) return 1; + x0 = (unsigned)b->x / m->tile_w; + y0 = (unsigned)b->y / m->tile_h; + x1 = (unsigned)(b->x + b->w - 1) / m->tile_w; + y1 = (unsigned)(b->y + b->h - 1) / m->tile_h; + for (y = y0; y <= y1; ++y) + for (x = x0; x <= x1; ++x) + if (m->cells[y * m->width + x] & m->solid_mask) return 1; + return 0; +} +int lte_blocked(const lte_tilemap *m, const lte_box *b) +{ + if (!lte_map_valid(m) || !box_valid(b)) return 1; + return blocked(m, b); +} +int lte_move_tiles(const lte_tilemap *m, lte_box *b, short dx, short dy) +{ + lte_box next, edge; + int axis, hit = 0; + if (!lte_map_valid(m) || !box_valid(b) || dx < -256 || dx > 256 || + dy < -256 || dy > 256 || blocked(m, b)) return LTE_MOVE_INVALID; + for (axis = 0; axis < 2; ++axis) { + int delta = axis ? dy : dx; + int direction = delta < 0 ? -1 : 1; + unsigned count = (unsigned)(delta < 0 ? -delta : delta); + while (count--) { + next = *b; + if (axis) next.y = (short)(next.y + direction); + else next.x = (short)(next.x + direction); + edge = next; + if (axis) { + if (direction > 0) edge.y = (short)(edge.y + edge.h - 1); + edge.h = 1; + } else { + if (direction > 0) edge.x = (short)(edge.x + edge.w - 1); + edge.w = 1; + } + if (blocked(m, &edge)) { hit |= axis ? LTE_HIT_Y : LTE_HIT_X; break; } + *b = next; + } + } + return hit; +} +int lte_camera_set(lte_camera *c, short x, short y, short w, short h, + short world_w, short world_h) +{ + short max_x, max_y; + if (!c || w <= 0 || h <= 0 || w > 16384 || h > 16384 || + world_w <= 0 || world_h <= 0 || world_w > 16384 || world_h > 16384) + return 0; + max_x = world_w > w ? (short)(world_w - w) : 0; + max_y = world_h > h ? (short)(world_h - h) : 0; + c->x = x < 0 ? 0 : (x > max_x ? max_x : x); + c->y = y < 0 ? 0 : (y > max_y ? max_y : y); + c->w = w; c->h = h; + return 1; +} +int lte_visible_tiles(const lte_tilemap *m, const lte_camera *c, + lte_tile_visit visit, void *user) +{ + unsigned x0, y0, x1, y1, x, y; + if (!lte_map_valid(m) || !c || !visit || c->x < 0 || c->y < 0 || + c->w <= 0 || c->h <= 0 || c->w > 16384 || c->h > 16384) return 0; + x0 = (unsigned)c->x / m->tile_w; + y0 = (unsigned)c->y / m->tile_h; + x1 = (unsigned)(((long)c->x + c->w - 1) / m->tile_w); + y1 = (unsigned)(((long)c->y + c->h - 1) / m->tile_h); + if (x1 >= m->width) x1 = m->width - 1; + if (y1 >= m->height) y1 = m->height - 1; + for (y = y0; y <= y1; ++y) + for (x = x0; x <= x1; ++x) + visit(user, m->cells[y * m->width + x], + (short)((long)x * (long)m->tile_w - c->x), + (short)((long)y * (long)m->tile_h - c->y), + (short)m->tile_w, (short)m->tile_h); + return 1; +} diff --git a/src/lt_engine_view.c b/src/lt_engine_view.c new file mode 100644 index 0000000..f413bca --- /dev/null +++ b/src/lt_engine_view.c @@ -0,0 +1,70 @@ +/* SPDX-License-Identifier: MIT */ +#include "lt_engine_view.h" +#include +int lte_view_bind(lte_view *v, unsigned mode, + unsigned char LTE_FAR *pixels, unsigned bytes) +{ + lte_view next; + unsigned need; + if (!v || !pixels) return 0; + next.pixels = pixels; next.mode = (unsigned char)mode; + next.height = 200; next.banks = 0; + if (mode == LTE_VIEW_TEXT40 || mode == LTE_VIEW_TEXT80) { + next.width = mode == LTE_VIEW_TEXT40 ? 40 : 80; + next.height = 25; next.stride = next.width * 2; + need = next.stride * next.height; + } else if (mode == LTE_VIEW_TANDY160 || mode == LTE_VIEW_TANDY320) { + next.width = mode == LTE_VIEW_TANDY160 ? 160 : 320; + next.banks = mode == LTE_VIEW_TANDY160 ? 2 : 4; + next.stride = next.width / 2; + need = next.banks * 8192U; + } else return 0; + if (bytes < need) return 0; + *v = next; + return 1; +} +void lte_view_rect(lte_view *v, short x, short y, short w, short h, + unsigned char ink, unsigned char glyph) +{ + long right = (long)x + w, bottom = (long)y + h; + unsigned row, col, x0, y0, x1, y1, offset; + if (!v || !v->pixels || w <= 0 || h <= 0 || right <= 0 || bottom <= 0 || + x >= (short)v->width || y >= (short)v->height) return; + x0 = x < 0 ? 0 : (unsigned)x; + y0 = y < 0 ? 0 : (unsigned)y; + x1 = right > (long)v->width ? v->width : (unsigned)right; + y1 = bottom > (long)v->height ? v->height : (unsigned)bottom; + for (row = y0; row < y1; ++row) { + if (!v->banks) { + offset = row * v->stride + x0 * 2; + for (col = x0; col < x1; ++col) { + v->pixels[offset++] = glyph; v->pixels[offset++] = ink; + } + } else { + unsigned char LTE_FAR *p; + unsigned end; + if (v->banks == 2) + offset = (row & 1U) * 8192U + (row >> 1) * v->stride; + else + offset = (row & 3U) * 8192U + (row >> 2) * v->stride; + p = v->pixels + offset; + ink &= 15; + col = x0; + if (col & 1U) { + p[col / 2] = (unsigned char)((p[col / 2] & 240U) | ink); + ++col; + } + end = x1 & ~1U; + if (col < end) { +#if defined(__WATCOMC__) && defined(__I86__) + _fmemset(p + col / 2, ink * 17U, (end - col) / 2); +#else + memset(p + col / 2, ink * 17U, (end - col) / 2); +#endif + col = end; + } + if (col < x1) + p[col / 2] = (unsigned char)((p[col / 2] & 15U) | (ink << 4)); + } + } +} diff --git a/tests/engine_core.c b/tests/engine_core.c new file mode 100644 index 0000000..a434023 --- /dev/null +++ b/tests/engine_core.c @@ -0,0 +1,167 @@ +/* SPDX-License-Identifier: MIT */ +#include +#include +#include "lt_engine.h" +#define CHECK(v) do { if (!(v)) { printf("FAIL engine line %d\n", __LINE__); return 1; } } while (0) +static char logbuf[256]; +static unsigned used, mutations, recursion_ok; +static lte_object *victim, *child; +static const lte_behavior behavior; +static void record(lte_object *o, char c) +{ + if (used < sizeof(logbuf) - 2) { + logbuf[used++] = c; logbuf[used++] = *(char *)o->user; + } + logbuf[used] = 0; +} +static void awake(lte_world *w, lte_object *o) +{ + (void)w; record(o, 'A'); +} +static void start(lte_world *w, lte_object *o) +{ + (void)w; record(o, 'S'); +} +static void fixed(lte_world *w, lte_object *o) +{ + (void)w; record(o, 'F'); +} +static void update(lte_world *w, lte_object *o) +{ + static char name = 'c'; + record(o, 'U'); + recursion_ok = lte_frame(w, 0) == -1; + if (mutations && *(char *)o->user == 'a') { + lte_remove(w, victim); + child = lte_add(w, &behavior, &name); + mutations = 0; + } +} +static void late(lte_world *w, lte_object *o) +{ + (void)w; record(o, 'L'); +} +static const lte_behavior behavior = {awake, start, fixed, update, late}; +static unsigned fixed_count; +static void count_tick(lte_world *w, lte_object *o) +{ + (void)w; (void)o; ++fixed_count; +} +static const lte_behavior counter = {0, 0, count_tick, 0, 0}; +static unsigned child_ticks, child_awakes, child_starts, chain_ticks; +static void chain_fixed(lte_world *w, lte_object *o) +{ (void)w; (void)o; ++chain_ticks; } +static const lte_behavior chain = {0, 0, chain_fixed, 0, 0}; +static void child_awake(lte_world *w, lte_object *o) +{ (void)o; ++child_awakes; lte_add(w, &chain, 0); } +static void child_start(lte_world *w, lte_object *o) +{ (void)w; (void)o; ++child_starts; } +static void child_fixed(lte_world *w, lte_object *o) +{ (void)w; (void)o; ++child_ticks; } +static const lte_behavior spawned = {child_awake, child_start, child_fixed, 0, 0}; +static void spawn_fixed(lte_world *w, lte_object *o) +{ + if (!w->ticks) { lte_add(w, &spawned, 0); lte_remove(w, o); } +} +static const lte_behavior spawner = {0, 0, spawn_fixed, 0, 0}; +static unsigned visits; +static short first_x, first_y; +static void tile(void *u, unsigned char value, short x, short y, short w, short h) +{ + (void)u; (void)value; (void)w; (void)h; + if (!visits) { first_x = x; first_y = y; } + ++visits; +} +int main(void) +{ + lte_world world, fast, slow; + lte_object objects[3], a[1], b[1], *first; + char names[2] = {'a', 'b'}; + unsigned i, fast_count; + unsigned char cells[16] = {0,0,1,0, 0,0,1,0, 1,1,1,1, 0,0,0,0}; + lte_tilemap map; + lte_box box = {0,0,8,8}, other = {8,0,8,8}, saved; + lte_camera camera, before; + CHECK(!lte_init(&world, objects, 3, 0, 4)); + CHECK(lte_init(&world, objects, 3, 10, 2)); + first = lte_add(&world, &behavior, &names[0]); + victim = lte_add(&world, &behavior, &names[1]); + CHECK(first && victim); + CHECK(lte_frame(&world, 20) == 2); + CHECK(!strcmp(logbuf, "AaAbSaSbFaFbFaFbUaUbLaLb")); + CHECK(recursion_ok); + used = 0; logbuf[0] = 0; mutations = 1; + CHECK(lte_frame(&world, 0) == 0 && child); + CHECK(!strcmp(logbuf, "UaLa")); + CHECK(!lte_add(&world, &behavior, names)); /* removed slot not reused yet */ + CHECK(!lte_remove(&world, victim)); + used = 0; logbuf[0] = 0; + CHECK(lte_frame(&world, 10) == 1); + CHECK(!strcmp(logbuf, "AcScFaFcUaUcLaLc")); + CHECK(lte_add(&world, &behavior, names) == victim); + CHECK(lte_remove(&world, victim)); /* remove pending: never Awake/Start */ + CHECK(lte_remove(&world, first)); + used = 0; logbuf[0] = 0; + CHECK(lte_frame(&world, 0) == 0 && !strcmp(logbuf, "UcLc")); + CHECK(lte_init(&fast, a, 1, 10, 3)); + CHECK(lte_init(&slow, b, 1, 10, 3)); + CHECK(lte_add(&fast, &counter, 0) && lte_add(&slow, &counter, 0)); + fixed_count = 0; + for (i = 0; i < 30; ++i) CHECK(lte_frame(&fast, 10) == 1); + fast_count = fixed_count; fixed_count = 0; + CHECK(lte_frame(&slow, 300) == 3 && slow.debt == 270); + for (i = 0; i < 9; ++i) CHECK(lte_frame(&slow, 0) == 3); + CHECK(fixed_count == fast_count && slow.debt == 0 && slow.ticks == fast.ticks); + CHECK(lte_frame(&slow, 0xffffffffUL) == 3); + CHECK(lte_frame(&slow, 100) == 3 && slow.saturated); + CHECK(lte_init(&world, objects, 3, 10, 3)); + CHECK(lte_add(&world, &spawner, 0)); + for (i = 0; i < 3; ++i) { + CHECK(lte_frame(&world, 10) == 1); + CHECK(lte_frame(&world, 0) == 0); + CHECK(lte_frame(&world, 0) == 0); + } + CHECK(child_ticks == 2 && child_awakes == 1 && child_starts == 1); + CHECK(chain_ticks == 1); + child_ticks = child_awakes = child_starts = chain_ticks = 0; + CHECK(lte_init(&world, objects, 3, 10, 3)); + CHECK(lte_add(&world, &spawner, 0)); + CHECK(lte_frame(&world, 30) == 3); + CHECK(child_ticks == 2 && child_awakes == 1 && child_starts == 1); + CHECK(chain_ticks == 1); + map.cells = cells; map.width = map.height = 4; + map.tile_w = map.tile_h = 8; map.solid_mask = 1; + CHECK(lte_map_valid(&map)); + CHECK(!lte_overlap(&box, &other)); + other.x = 7; CHECK(lte_overlap(&box, &other)); + CHECK(lte_move_tiles(&map, &box, 32, 32) == (LTE_HIT_X | LTE_HIT_Y)); + CHECK(box.x == 8 && box.y == 8); /* cannot tunnel through 1-cell wall */ + CHECK(lte_move_tiles(&map, &box, -32, -32) == (LTE_HIT_X | LTE_HIT_Y)); + CHECK(box.x == 0 && box.y == 0); + saved = box; + CHECK(lte_move_tiles(&map, &box, 257, 0) == LTE_MOVE_INVALID); + CHECK(!memcmp(&saved, &box, sizeof(box))); + box.x = 16; saved = box; + CHECK(lte_move_tiles(&map, &box, -1, 0) == LTE_MOVE_INVALID); + CHECK(!memcmp(&saved, &box, sizeof(box))); + box.x = -1; CHECK(lte_blocked(&map, &box)); + CHECK(lte_camera_set(&camera, -10, 99, 16, 16, 32, 32)); + CHECK(camera.x == 0 && camera.y == 16); + CHECK(lte_camera_set(&camera, 99, 99, 64, 64, 32, 32)); + CHECK(camera.x == 0 && camera.y == 0); + before = camera; + CHECK(!lte_camera_set(&camera, 0, 0, 0, 10, 32, 32)); + CHECK(!memcmp(&camera, &before, sizeof(camera))); + CHECK(lte_camera_set(&camera, 3, 7, 16, 16, 32, 32)); + CHECK(lte_visible_tiles(&map, &camera, tile, 0)); + CHECK(visits == 9 && first_x == -3 && first_y == -7); + camera.x = 100; visits = 0; + CHECK(lte_visible_tiles(&map, &camera, tile, 0) && visits == 0); + map.width = 65535U; CHECK(!lte_map_valid(&map)); + printf("engine passed lifecycle, mutation, foreground debt, collision, camera; " + "world=%u object=%u box=%u camera=%u map=%u behavior=%u bytes\n", + (unsigned)sizeof(world), (unsigned)sizeof(objects[0]), + (unsigned)sizeof(box), (unsigned)sizeof(camera), + (unsigned)sizeof(map), (unsigned)sizeof(behavior)); + return 0; +} diff --git a/tests/engine_dos.c b/tests/engine_dos.c new file mode 100644 index 0000000..cb5a13a --- /dev/null +++ b/tests/engine_dos.c @@ -0,0 +1,44 @@ +/* SPDX-License-Identifier: MIT */ +#include +#include +#include +#include "lt_engine_dos.h" +static void snapshot(unsigned char state[6]) +{ + union REGS r; + memset(&r, 0, sizeof(r)); r.h.ah = 15; + int86(0x10, &r, &r); state[0] = r.h.al; state[1] = r.h.bh; + memset(&r, 0, sizeof(r)); r.h.ah = 3; r.h.bh = state[1]; + int86(0x10, &r, &r); + state[2] = r.h.ch; state[3] = r.h.cl; state[4] = r.h.dh; state[5] = r.h.dl; +} +int main(void) +{ + lte_dos_video v = LTE_DOS_VIDEO_INITIALIZER; + unsigned char before[6], after[6]; + unsigned modes[4] = {1, 3, 8, 9}, i; + unsigned short vector[4]; + const volatile unsigned short __far *ivt = + (const volatile unsigned short __far *)MK_FP(0, 0x20); + int pass = 1; + for (i = 0; i < 4; ++i) vector[i] = ivt[i]; /* IRQ0 and IRQ1 */ + snapshot(before); + if (lte_dos_video_open(&v, 4) || v.active) pass = 0; + for (i = 0; i < 4 && pass; ++i) { + if (!lte_dos_video_open(&v, modes[i])) { pass = 0; break; } + snapshot(after); + if (after[0] != modes[i] || after[1] != 0) pass = 0; + if (lte_dos_video_open(&v, modes[i])) pass = 0; + lte_view_rect(&v.view, -2, -2, 20, 20, 15, '@'); + lte_dos_video_close(&v); + lte_dos_video_close(&v); + snapshot(after); + if (memcmp(before, after, sizeof(before)) || v.active || v.view.pixels) pass = 0; + } + for (i = 0; i < 4; ++i) if (vector[i] != ivt[i]) pass = 0; + lte_dos_video_close(&v); + printf("engine DOS mode/page/cursor restore, double-open/close, invalid mode, " + "IRQ0/1 unchanged: %s; video=%u bytes\n", pass ? "PASS" : "FAIL", + (unsigned)sizeof(v)); + return pass ? 0 : 1; +} diff --git a/tests/engine_view.c b/tests/engine_view.c new file mode 100644 index 0000000..b59677d --- /dev/null +++ b/tests/engine_view.c @@ -0,0 +1,42 @@ +/* SPDX-License-Identifier: MIT */ +#include +#include +#include "lt_engine_view.h" +#define CHECK(v) do { if (!(v)) { printf("FAIL engine view line %d\n", __LINE__); return 1; } } while (0) +/* Far: the 32 KiB fixture must not consume DOS small-model DGROUP. */ +static unsigned char LTE_FAR pixels[32770U]; +int main(void) +{ + lte_view view, saved; + unsigned i; + CHECK(lte_view_bind(&view, 8, pixels + 1, 16384)); + saved = view; + CHECK(!lte_view_bind(&view, 8, pixels + 1, 16383)); + CHECK(!memcmp(&saved, &view, sizeof(view))); + CHECK(!lte_view_bind(&view, 4, pixels + 1, 32768U)); + for (i = 0; i < 32770U; ++i) pixels[i] = 0x5a; + lte_view_rect(&view, 0, 0, 160, 200, 3, 0); + CHECK(pixels[1] == 0x33 && pixels[8193] == 0x33); + CHECK(pixels[1 + 99 * 80] == 0x33); + CHECK(pixels[1 + 8192 + 99 * 80 + 79] == 0x33); + CHECK(pixels[0] == 0x5a && pixels[16385] == 0x5a); + CHECK(pixels[1 + 8000] == 0x5a); /* bank padding untouched */ + lte_view_rect(&view, 1, 0, 2, 1, 14, 0); + CHECK(pixels[1] == 0x3e && pixels[2] == 0xe3); + lte_view_rect(&view, -2, -2, 3, 3, 7, 0); + CHECK(pixels[1] == 0x7e); + CHECK(lte_view_bind(&view, 9, pixels + 1, 32768U)); + lte_view_rect(&view, 319, 199, 30000, 30000, 15, 0); + CHECK(pixels[1 + 3 * 8192 + 49 * 160 + 159] == 0x5f); + CHECK(pixels[32769U] == 0x5a); + CHECK(lte_view_bind(&view, 3, pixels + 1, 4000)); + lte_view_rect(&view, 0, 0, 80, 25, 0x1e, 219); + CHECK(pixels[1] == 219 && pixels[2] == 0x1e); + CHECK(pixels[3999] == 219 && pixels[4000] == 0x1e); + CHECK(lte_view_bind(&view, 1, pixels + 1, 2000)); + lte_view_rect(&view, 39, 24, 2, 2, 0x2f, '@'); + CHECK(pixels[1999] == '@' && pixels[2000] == 0x2f); + CHECK(pixels[2001] == 219); /* clip at 40x25 */ + printf("engine view passed native 8/9 banks, nibble edges, text 1/3, clipping; view=%u bytes\n", (unsigned)sizeof(view)); + return 0; +} diff --git a/tools/build.py b/tools/build.py index 98ae005..de0bb5a 100644 --- a/tools/build.py +++ b/tools/build.py @@ -8,6 +8,8 @@ ROOT = Path(__file__).resolve().parents[1] TESTS = { + 'engine': ('engine_core', ['lt_engine']), + 'engview': ('engine_view', ['lt_engine_view']), 'stream': ('stream_core', ['lt_chip', 'lt_audio', 'lt_sfx', 'lt_stream']), 'smscheck': ('sms_boot_core', ['lt_chip', 'lt_sms_boot']), 'chip': ('chip_core', ['lt_chip']), @@ -24,6 +26,14 @@ } # DOS-only demos of the platform services: name -> sources. DOS_DEMOS = { + 'ENGDOS': ['tests/engine_dos.c', 'src/lt_engine_view.c', + 'adapters/lt_engine_dos.c'], + 'ENGDEMO': ['examples/engine_demo.c', 'src/lt_engine.c', + 'src/lt_engine_view.c', 'adapters/lt_engine_dos.c', + 'src/lt_time.c', 'adapters/lt_time_dos.c'], + 'TXTDEMO': ['examples/engine_text_demo.c', 'src/lt_engine.c', + 'src/lt_engine_view.c', 'adapters/lt_engine_dos.c', + 'src/lt_time.c', 'adapters/lt_time_dos.c'], 'STRMDEMO': ['examples/stream_demo.c', 'src/lt_chip.c', 'src/lt_audio.c', 'src/lt_sfx.c', 'src/lt_stream.c', 'adapters/lt_dos.c'], 'STRMBEN': ['tests/stream_bench.c', 'src/lt_chip.c', 'src/lt_audio.c', From d6d5b168db93ae377bbca6033d71e4b25114101e Mon Sep 17 00:00:00 2001 From: Chad Date: Tue, 27 Oct 2026 10:13:48 -0500 Subject: [PATCH 2/3] Publish source-bound qualified DOS engine demo package --- .gitattributes | 1 + .github/workflows/portable-midi.yml | 2 + .gitignore | 1 + docs/ENGINE.md | 5 ++ examples/engine/DOS-BUILD.json | 82 +++++++++++++++++++++++++ examples/engine/ENGINE.ZIP | Bin 0 -> 59123 bytes examples/engine/NATIVE.json | 89 ++++++++++++++++++++++++++++ examples/engine/README.md | 33 +++++++++++ tools/check_engine_package.py | 40 +++++++++++++ 9 files changed, 253 insertions(+) create mode 100644 examples/engine/DOS-BUILD.json create mode 100644 examples/engine/ENGINE.ZIP create mode 100644 examples/engine/NATIVE.json create mode 100644 examples/engine/README.md create mode 100644 tools/check_engine_package.py diff --git a/.gitattributes b/.gitattributes index 945cc9e..7425389 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,3 +9,4 @@ sound/tests/audition/*.H text eol=crlf examples/doebel/DOEBEL.VGM binary examples/doebel/NOTES.csv binary examples/smsboot/SMSBOOT.ZIP binary +examples/engine/ENGINE.ZIP binary diff --git a/.github/workflows/portable-midi.yml b/.github/workflows/portable-midi.yml index 10f0aed..b286ef1 100644 --- a/.github/workflows/portable-midi.yml +++ b/.github/workflows/portable-midi.yml @@ -13,3 +13,5 @@ jobs: run: python3 sound/tests/run.py --cc gcc - name: Qualified optional DOS example package run: python3 tools/check_sms_package.py + - name: Qualified optional engine DOS package + run: python3 tools/check_engine_package.py diff --git a/.gitignore b/.gitignore index ae34f3e..e32ad4f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ __pycache__/ *.img *.zip !examples/smsboot/SMSBOOT.ZIP +!examples/engine/ENGINE.ZIP diff --git a/docs/ENGINE.md b/docs/ENGINE.md index f728c6c..0ff35d2 100644 --- a/docs/ENGINE.md +++ b/docs/ENGINE.md @@ -136,6 +136,11 @@ The simulation and camera can still be reused with a caller's renderer. ## Ready-to-run DOS demonstrations +[Download the qualified ENGINE.ZIP](https://github.com/astrobleem/libtandy/raw/refs/heads/main/examples/engine/ENGINE.ZIP) +for self-contained DOS executables and instructions, or build from source. +The [package manifest](../examples/engine/DOS-BUILD.json) and CI bind the +download to its qualified source and native execution receipt. + Build with the normal Open Watcom helper: ```powershell diff --git a/examples/engine/DOS-BUILD.json b/examples/engine/DOS-BUILD.json new file mode 100644 index 0000000..168201f --- /dev/null +++ b/examples/engine/DOS-BUILD.json @@ -0,0 +1,82 @@ +{ + "qualified_source_commit": "d97d173", + "compiler": "Open Watcom 1.9", + "flags": [ + "-bt=dos", + "-0", + "-ms", + "-ecc", + "-os", + "-s", + "-w4", + "-wx", + "-k4096" + ], + "source_lf_sha256": { + "src/lt_engine.c": "d9bd67954865ed85513652d4c97d1b999d39b8ab59076cf217c037447c817586", + "src/lt_engine_view.c": "84df1b9e00adab45afaa4893e78a22d315bb91d63f2e0574231e40a326d42ba7", + "adapters/lt_engine_dos.c": "7e6900fd0d5df7211b124eac75151472afee444ad8448cde51fd8c141d130729", + "examples/engine_demo.c": "8439e7765708282d439a2199cee5422b89196e78d2b41d3186421f1ab58c457b", + "examples/engine_text_demo.c": "b2689df94cc66238aa3a256b8209fff533c9e0de5ef0a5547c03437a86d41a7a", + "include/lt_engine.h": "1043bb78702f6a9488275fb1afd424984405450bdf24f5280feac2e7b71baeed", + "include/lt_engine_view.h": "cb9824e2140a462e23ecbeabe1d4af7bbb1b4528bb0b4cf89502519a81feb5c2", + "include/lt_engine_dos.h": "83bd6172bf2d28faf1d6fbd02c8928cd716f6dd92a709261a7a96ddf8b363994", + "tests/engine_core.c": "0365570e0278334058dce81319dc0b87ebe2f751bebb218c8d613160145de1a1", + "tests/engine_view.c": "347279286915072ae1329e3ae0abd5629ae45d338efd1f96138c57f62ebac2f5", + "tests/engine_dos.c": "53a50b7d26cc1dfcd3de5e7bc0eac61db3ddec79958ef5b249e2229ff0a0be7e", + "src/lt_time.c": "d26bea522d482b0134cdbc6fb3b642a5e34f32ee5a3b8fdebb8e29f152ab841f", + "adapters/lt_time_dos.c": "beb10baca039286fe550b428c984b17201c30ab506459ef1c153e7c6adc99817", + "include/lt_time.h": "70a750125d08b6096896ad7a98a43cd4e340d17cebfae07b615b7b949ee39d96", + "include/lt_time_dos.h": "7102506c96865900947cfbc6fdcf47b8b5c0af4b8b1c4eae8d234f8aa69a252a", + "include/lt_surface.h": "5ce7a074d1bc734fd460ff70590e4a8234f84df3f95e7beceed226854b4392a9" + }, + "native_sha256": "9928f229c5d8da5cf59ccd27b449120718fed571ff84cfb28b5cd045bda33ed9", + "zip_sha256": "1ab2c7f0441464cca06f3d28cf07947ca075900bda6fa439da71e1015aa6451d", + "members": { + "DOCS/ENGINE.MD": { + "bytes": 10486, + "sha256": "bd2ce10965513e3bc76f9c3e5f80f27729c045ab64fe74094c5afb49d78c88e2" + }, + "DOCS/VALIDATE.MD": { + "bytes": 6358, + "sha256": "de158a2bbdd628bd4b3bff17470703c6601a6a4cff83cb5b8fa711a07e5b1324" + }, + "ENGDEMO.EXE": { + "bytes": 15912, + "sha256": "5f3f80eaa62734c6949d61c798a917c6ae790b2ff544d388320a108f103660de" + }, + "ENGDOS.EXE": { + "bytes": 11498, + "sha256": "dcfbfdc2c9f0f03c3209202165647846af6606795017c1277eb5dfcefac8d25b" + }, + "ENGINE.EXE": { + "bytes": 13578, + "sha256": "e88b1480bcfa405499b20138e0fd59400a8714ea7a03e4b026e654a143efb703" + }, + "ENGVIEW.EXE": { + "bytes": 42966, + "sha256": "48e67d3b46992d6e0c7ec9216094a6dcd3bd1fef6922cb70f777b6d2d75cd622" + }, + "LICENSE.TXT": { + "bytes": 1063, + "sha256": "1b7073ca79c0f526b7017032b475e99b0443ad0bac029169a2d646a12e533985" + }, + "README.TXT": { + "bytes": 746, + "sha256": "a73df1ae350591b5c26381934848e0efa0c83e8f785f75299581050345dd3a7e" + }, + "RUNCHECK.BAT": { + "bytes": 206, + "sha256": "40cf27bf947892d1e4bfea72d8a397cefa357fcf05caebf45c4ff8ddcdab3205" + }, + "TXTDEMO.EXE": { + "bytes": 15910, + "sha256": "2c5be6da695efe692bbcebf2bce43082572a8971fc8c27915aec57b45c763c37" + } + }, + "document_members": { + "DOCS/ENGINE.MD": "docs/ENGINE.md", + "DOCS/VALIDATE.MD": "docs/ENGINE-VALIDATION.md", + "LICENSE.TXT": "LICENSE" + } +} diff --git a/examples/engine/ENGINE.ZIP b/examples/engine/ENGINE.ZIP new file mode 100644 index 0000000000000000000000000000000000000000..95ab0d96281c1145a43802cd7352265440121a5b GIT binary patch literal 59123 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 literal 0 HcmV?d00001 diff --git a/examples/engine/NATIVE.json b/examples/engine/NATIVE.json new file mode 100644 index 0000000..bbf942e --- /dev/null +++ b/examples/engine/NATIVE.json @@ -0,0 +1,89 @@ +{ + "complete": true, + "cpu_hz": 4772728, + "started": "2026-10-27T14:32:26.495909+00:00", + "captures": [ + { + "label": "G320-early", + "mode": 9, + "bytes": 1350 + }, + { + "label": "G320-late", + "mode": 9, + "bytes": 1432 + }, + { + "label": "G160-early", + "mode": 8, + "bytes": 1486 + }, + { + "label": "G160-late", + "mode": 8, + "bytes": 1436 + }, + { + "label": "T80-early", + "mode": 3, + "bytes": 1764 + }, + { + "label": "T80-late", + "mode": 3, + "bytes": 1747 + }, + { + "label": "T40-early", + "mode": 1, + "bytes": 1574 + }, + { + "label": "T40-late", + "mode": 1, + "bytes": 1509 + }, + { + "label": "SLOW-early", + "mode": 9, + "bytes": 4442 + }, + { + "label": "SLOW-late", + "mode": 9, + "bytes": 4515 + } + ], + "TXTDEMO.EXE": { + "bytes": 15910, + "sha256": "2c5be6da695efe692bbcebf2bce43082572a8971fc8c27915aec57b45c763c37" + }, + "ENGINE.EXE": { + "bytes": 13578, + "sha256": "e88b1480bcfa405499b20138e0fd59400a8714ea7a03e4b026e654a143efb703" + }, + "ENGVIEW.EXE": { + "bytes": 42966, + "sha256": "48e67d3b46992d6e0c7ec9216094a6dcd3bd1fef6922cb70f777b6d2d75cd622" + }, + "ENGDEMO.EXE": { + "bytes": 15912, + "sha256": "5f3f80eaa62734c6949d61c798a917c6ae790b2ff544d388320a108f103660de" + }, + "ENGDOS.EXE": { + "bytes": 11498, + "sha256": "dcfbfdc2c9f0f03c3209202165647846af6606795017c1277eb5dfcefac8d25b" + }, + "lease_acquired": true, + "owned_pid": 22140, + "owned_child_stopped": true, + "lease_released": true, + "CORE": "engine passed lifecycle, mutation, foreground debt, collision, camera; world=18 object=6 box=8 camera=8 map=14 behavior=10 bytes\r\n", + "VIEW": "engine view passed native 8/9 banks, nibble edges, text 1/3, clipping; view=14 bytes\r\n", + "VIDEO": "engine DOS mode/page/cursor restore, double-open/close, invalid mode, IRQ0/1 unchanged: PASS; video=22 bytes\r\n", + "G320": "ENGINE mode=9 ticks=300 frames=76 debt=0 awake/start=1/1 hits=1/141 camera=316 xy=330,176 PASS\r\n", + "G160": "ENGINE mode=8 ticks=300 frames=76 debt=0 awake/start=1/1 hits=1/141 camera=396 xy=330,176 PASS\r\n", + "T80": "ENGINE mode=3 ticks=300 frames=76 debt=0 awake/start=1/1 hits=1/141 camera=128 xy=330,176 PASS\r\n", + "T40": "ENGINE mode=1 ticks=300 frames=76 debt=0 awake/start=1/1 hits=1/141 camera=312 xy=330,176 PASS\r\n", + "SLOW": "ENGINE mode=9 ticks=300 frames=76 debt=0 awake/start=1/1 hits=1/141 camera=314 xy=330,176 PASS\r\n" +} diff --git a/examples/engine/README.md b/examples/engine/README.md new file mode 100644 index 0000000..e40f09d --- /dev/null +++ b/examples/engine/README.md @@ -0,0 +1,33 @@ +# Optional engine DOS demonstrations + +[Download ENGINE.ZIP](https://github.com/astrobleem/libtandy/raw/refs/heads/main/examples/engine/ENGINE.ZIP) +for the qualified, self-contained DOS executables. Extract to a directory on +compatible Tandy DOS hardware and run explicitly. No Python, assets, setup +program or conversion step is needed on the DOS machine. + +| Command | Display | +| --- | --- | +| `ENGDEMO` | Tandy 320x200x16 (mode 09h) | +| `ENGDEMO /low` | Tandy 160x200x16 (mode 08h) | +| `TXTDEMO` | Color text 80x25 (mode 03h) | +| `TXTDEMO /40` | Color text 40x25 (mode 01h) | + +A/D or arrows set horizontal velocity; Space jumps; Escape quits and restores +video. Each example uses the same world, tile collisions and gravity, with a +camera following the moving body through a scrolling map. Add `/auto` for a +finite 300-fixed-tick route, or `/auto /slow` for a three-BIOS-tick foreground +wait. The engine runs fixed callbacks in the foreground; it owns no timer, +ISR, sound channel or heap. These are small convenience examples, not a +high-frame-rate platformer benchmark. + +`RUNCHECK.BAT` runs ENGINE/ENGVIEW/ENGDOS, the native lifecycle/physics, +presentation and mode-restoration checks. The ZIP also contains the guide, +validation record and MIT license. It bundles no emulator, ROM, DOS or compiler. + +The [engine guide](../../docs/ENGINE.md) documents optional source integration, +supported display layouts and bounds. The [validation record](../../docs/ENGINE-VALIDATION.md) +records the 4.77 MHz Tandy HX emulator results and code/state costs. +[DOS-BUILD.json](DOS-BUILD.json) pins the qualified source and ZIP members; +[NATIVE.json](NATIVE.json) records exact tested binary hashes and runtime results. +CI checks the committed download without requiring an emulator or compiler +for its verification. diff --git a/tools/check_engine_package.py b/tools/check_engine_package.py new file mode 100644 index 0000000..42f6f41 --- /dev/null +++ b/tools/check_engine_package.py @@ -0,0 +1,40 @@ +"""Verify the optional engine DOS download against source and native receipts.""" +from pathlib import Path +import hashlib +import json +import zipfile + +ROOT = Path(__file__).resolve().parents[1] +DIRECTORY = ROOT / 'examples/engine' +manifest = json.loads((DIRECTORY / 'DOS-BUILD.json').read_text()) +native = json.loads((DIRECTORY / 'NATIVE.json').read_text()) +sha = lambda data: hashlib.sha256(data).hexdigest() +for name, digest in manifest['source_lf_sha256'].items(): + assert sha((ROOT / name).read_bytes().replace(b'\r\n', b'\n')) == digest, name +assert sha((DIRECTORY / 'NATIVE.json').read_bytes()) == manifest['native_sha256'] +assert native['complete'] and native['owned_child_stopped'] and native['lease_released'] +for run in ('CORE', 'VIEW', 'VIDEO'): + assert 'FAIL' not in native[run] and ('passed' in native[run] or 'PASS' in native[run]), run +for run in ('G320', 'G160', 'T80', 'T40', 'SLOW'): + assert 'ticks=300' in native[run] and 'debt=0' in native[run], run + assert 'xy=330,176 PASS' in native[run], run +archive = DIRECTORY / 'ENGINE.ZIP' +assert sha(archive.read_bytes()) == manifest['zip_sha256'], 'ZIP hash' +with zipfile.ZipFile(archive) as package: + assert package.testzip() is None + assert set(package.namelist()) == set(manifest['members']), 'ZIP members' + for name, expected in manifest['members'].items(): + data = package.read(name) + assert len(data) == expected['bytes'] and sha(data) == expected['sha256'], name + for name in ('ENGINE.EXE', 'ENGVIEW.EXE', 'ENGDOS.EXE', 'ENGDEMO.EXE', 'TXTDEMO.EXE'): + data = package.read(name) + assert data[:2] == b'MZ', name + assert sha(data) == native[name]['sha256'], name + ': native hash' + assert len(data) == native[name]['bytes'], name + ': native size' + for name, source in manifest['document_members'].items(): + assert package.read(name).replace(b'\r\n', b'\n') == \ + (ROOT / source).read_bytes().replace(b'\r\n', b'\n'), name + for name in ('README.TXT', 'RUNCHECK.BAT'): + remainder = package.read(name).replace(b'\r\n', b'') + assert b'\r' not in remainder and b'\n' not in remainder, name + ': DOS CRLF' +print('PASS qualified engine DOS package: source, native receipt, ZIP, EXEs and docs') From ee0b3d34c1be9e94c839779105d2c7d5d81f3f5f Mon Sep 17 00:00:00 2001 From: Chad Date: Tue, 27 Oct 2026 10:14:40 -0500 Subject: [PATCH 3/3] Normalize native receipt digest across Git text checkouts --- examples/engine/DOS-BUILD.json | 4 ++-- tools/check_engine_package.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/engine/DOS-BUILD.json b/examples/engine/DOS-BUILD.json index 168201f..d19aee5 100644 --- a/examples/engine/DOS-BUILD.json +++ b/examples/engine/DOS-BUILD.json @@ -30,7 +30,6 @@ "include/lt_time_dos.h": "7102506c96865900947cfbc6fdcf47b8b5c0af4b8b1c4eae8d234f8aa69a252a", "include/lt_surface.h": "5ce7a074d1bc734fd460ff70590e4a8234f84df3f95e7beceed226854b4392a9" }, - "native_sha256": "9928f229c5d8da5cf59ccd27b449120718fed571ff84cfb28b5cd045bda33ed9", "zip_sha256": "1ab2c7f0441464cca06f3d28cf07947ca075900bda6fa439da71e1015aa6451d", "members": { "DOCS/ENGINE.MD": { @@ -78,5 +77,6 @@ "DOCS/ENGINE.MD": "docs/ENGINE.md", "DOCS/VALIDATE.MD": "docs/ENGINE-VALIDATION.md", "LICENSE.TXT": "LICENSE" - } + }, + "native_lf_sha256": "59f43dc5b506abda53b58bb4eaeb50fee860b8ae85c37e3e014bba0c09c3f9bd" } diff --git a/tools/check_engine_package.py b/tools/check_engine_package.py index 42f6f41..1dbcde8 100644 --- a/tools/check_engine_package.py +++ b/tools/check_engine_package.py @@ -11,7 +11,8 @@ sha = lambda data: hashlib.sha256(data).hexdigest() for name, digest in manifest['source_lf_sha256'].items(): assert sha((ROOT / name).read_bytes().replace(b'\r\n', b'\n')) == digest, name -assert sha((DIRECTORY / 'NATIVE.json').read_bytes()) == manifest['native_sha256'] +assert sha((DIRECTORY / 'NATIVE.json').read_bytes().replace(b'\r\n', b'\n')) == \ + manifest['native_lf_sha256'] assert native['complete'] and native['owned_child_stopped'] and native['lease_released'] for run in ('CORE', 'VIEW', 'VIDEO'): assert 'FAIL' not in native[run] and ('passed' in native[run] or 'PASS' in native[run]), run