diff --git a/emulator/deploy/vwetest.conf.tmpl b/emulator/deploy/vwetest.conf.tmpl new file mode 100644 index 0000000..4fa045c --- /dev/null +++ b/emulator/deploy/vwetest.conf.tmpl @@ -0,0 +1,54 @@ +# DEPLOY TEMPLATE -- rendered to vwetest.conf by deploy/configure.ps1 at install. +# Tokens (filled by postinstall): @@ROOT@@ = package dir (C:\games\), +# @@REALNIC@@ = this pod's NIC GUID fragment, @@MACADDR@@ = stable derived MAC. +# VGL_LABS factory cockpit test suite: the stock C:\VWETEST\TEST.BAT chain +# (DIAGNOSE + AWEUTIL /S on both AWE cards, SETSVGA, VGLTEST\TSTALL.EXE -- +# RIO, plasma, audio, video/Division calibration, Munga-net). Launch: +# pod-launch test. AWEUTIL's WC-timed init needs the parity-safe EMU8000 +# sample-counter readback (vweawe.cpp, 2026-07-16) -- on older dosbox-x.exe +# builds it hangs at AWEUTIL. Drops to a DOS prompt when the suite exits. +[sdl] +output=opengl +priority=highest,highest +[dosbox] +memsize=32 +machine=svga_s3 +[cpu] +core=dynamic +cputype=pentium +cycles=max +[ne2000] +ne2000=true +nicbase=340 +# IRQ 10, NOT 3: COM2 (plasma) owns IRQ 3 in the suite, same as in-game. +nicirq=10 +macaddr=@@MACADDR@@ +backend=pcap +[ethernet, pcap] +realnic=@@REALNIC@@ +[sblaster] +sbtype=sb16 +sbbase=220 +irq=5 +dma=1 +hdma=5 +[mixer] +rate=44100 +blocksize=1024 +prebuffer=60 +[serial] +serial1=directserial realport:COM1 rxpollus:100 rxburst:16 +serial2=directserial realport:COM2 +[autoexec] +mount c "@@ROOT@@\ALPHA_1" +mount d "@@ROOT@@\net-boot" +d: +echo === loading NE2000 packet-driver stack (for the Munga-net tests) === +d:\lsl +d:\ne2000 +d:\odipkt +c: +cd \vwetest +echo === VGL_LABS cockpit test suite (stock TEST.BAT) === +call test.bat +echo === test suite exited -- DOS prompt === diff --git a/emulator/pod-launch/Modes.cs b/emulator/pod-launch/Modes.cs index 2998186..407c8bf 100644 --- a/emulator/pod-launch/Modes.cs +++ b/emulator/pod-launch/Modes.cs @@ -30,6 +30,8 @@ namespace VwePod Note = "BattleTech, networked cockpit. Also serves the camera ship and LIVE mission-review -- the console sets the role per-IP via the egg hostType, so no separate mode is needed." }, new Mode { Name = "rp", ConfBase = "net_rp", Supported = true, Note = "Red Planet, networked cockpit (also camera / live mission-review via egg hostType)." }, + new Mode { Name = "test", ConfBase = "vwetest", Supported = true, + Note = "VGL_LABS factory cockpit test suite (C:\\VWETEST TEST.BAT -> TSTALL.EXE): RIO, plasma, audio (both AWE cards), video/Division calibration, Munga-net diagnostics." }, // Distinct launches, mechanics not yet wired on the emulator: new Mode { Name = "review", ConfBase = null, Supported = false, diff --git a/emulator/pod-launch/Options.cs b/emulator/pod-launch/Options.cs index 0004a49..65e16ef 100644 --- a/emulator/pod-launch/Options.cs +++ b/emulator/pod-launch/Options.cs @@ -49,6 +49,7 @@ namespace VwePod var o = new Options(); string modeName = null, confArg = null, rootArg = null, dosboxArg = null, bridgeScriptArg = null, rendererArg = null, aweArg = null; + bool dosboxXySet = false; for (int i = 0; i < args.Length; i++) { @@ -72,7 +73,7 @@ namespace VwePod case "--layout": o.Layout = Next(a).Equals("explode", StringComparison.OrdinalIgnoreCase) ? LayoutMode.Explode : LayoutMode.Cockpit; break; case "--bridge-pos": o.BridgePos = Next(a); break; case "--bridge-size": ParseWH(Next(a), out o.BridgeW, out o.BridgeH); break; - case "--dosbox-xy": ParseWH(Next(a), out o.DosBoxX, out o.DosBoxY); break; + case "--dosbox-xy": ParseWH(Next(a), out o.DosBoxX, out o.DosBoxY); dosboxXySet = true; break; case "--no-bridge": o.NoBridge = true; break; case "--no-sound": o.NoSound = true; break; case "--mipmap": o.Mipmap = true; break; @@ -92,6 +93,10 @@ namespace VwePod // reason and exits without touching the launch env. if (!o.Mode.Supported) return o; + // test mode: the DOS screen IS the suite's UI -- default it out from + // under the render window unless the caller placed it explicitly. + if (!dosboxXySet && o.Mode.Name == "test") { o.DosBoxX = 900; o.DosBoxY = 600; } + o.DosBox = dosboxArg ?? FirstExisting( Path.Combine(o.Root, "dosbox-x.exe"), Path.Combine(o.Root, "src", "src", "dosbox-x.exe")) @@ -155,11 +160,12 @@ namespace VwePod Supervising entry-point: launches DOSBox-X + the render bridge into a job object and blocks; killing this process kills them too (no orphans). - mode bt | rp (default bt). bt/rp also serve the camera ship - + live mission-review roles -- the console picks the - role via the egg hostType, so no separate mode. Also - recognized (not yet wired): review, test-plasma, - reset-rio, audio-test. + mode bt | rp | test (default bt). bt/rp also serve the + camera ship + live mission-review roles -- the console + picks the role via the egg hostType, so no separate + mode. test = the VGL_LABS factory cockpit test suite + (C:\VWETEST -> TSTALL.EXE). Also recognized (not yet + wired): review, test-plasma, reset-rio, audio-test. --mode same as the positional mode --conf explicit conf (overrides the mode's conf) --root base dir for dosbox/conf/bridge (default: exe dir) diff --git a/emulator/pod-launch/README.md b/emulator/pod-launch/README.md index e6458ff..5db499f 100644 --- a/emulator/pod-launch/README.md +++ b/emulator/pod-launch/README.md @@ -51,6 +51,7 @@ layout is fixed at `postinstall` time, so a typical call is just `pod-launch bt` |---|---|---| | `bt` | BattleTech, networked cockpit (`net_loop.conf`, looped) | wired | | `rp` | Red Planet, networked cockpit (`net_rp.conf`, looped) | wired | +| `test` | VGL_LABS factory cockpit test suite (`vwetest.conf` → `C:\VWETEST` → `TSTALL.EXE`): RIO, plasma, audio, video/Division calibration, Munga-net. DOSBox window defaults to 900,600 (the DOS screen is the suite's UI) | wired | | `review` | mission-review PLAYBACK of `last.spl` | recognized, exits 3 (DOS arg TBD) | | `test-plasma` | plasma-display diagnostic | recognized, exits 3 | | `reset-rio` | RIO reset diagnostic | recognized, exits 3 | diff --git a/emulator/vpx-device/emu8k.cpp b/emulator/vpx-device/emu8k.cpp index 4544b39..a290028 100644 --- a/emulator/vpx-device/emu8k.cpp +++ b/emulator/vpx-device/emu8k.cpp @@ -712,11 +712,11 @@ emu8k_inw(uint16_t addr, void *priv) it is used by programs to wait. Not critical, but should help reduce the amount of calls and wait time */ case 27: /*Sample Counter ( 44Khz clock) */ - /* VWE: interpolate between mixer blocks so - * driver busy-waits on the clock resolve at - * sample granularity instead of block steps */ - return (uint16_t)(emu8k->wc + - emu8k_shim_wc_extra()); + /* VWE: free-running host-clock counter, like the + * silicon. Software (AWEUTIL) busy-waits on this; + * tying it to render progress made those waits + * crawl under lock contention. */ + return emu8k_shim_wc_read(); default: break; diff --git a/emulator/vpx-device/emu8k_shim.h b/emulator/vpx-device/emu8k_shim.h index 7ea428b..61fa428 100644 --- a/emulator/vpx-device/emu8k_shim.h +++ b/emulator/vpx-device/emu8k_shim.h @@ -37,8 +37,10 @@ void io_removehandler(uint16_t base, int size, extern int wavetable_pos_global; FILE *emu8k_shim_rom_fopen(void); -/* samples elapsed since the last mixer render (WC interpolation) */ -unsigned emu8k_shim_wc_extra(void); +/* the WC sample counter readback: free-running 44100Hz off the host clock, + * exactly like the silicon (guest software busy-waits on it; deriving it + * from render progress made those waits crawl under lock contention) */ +uint16_t emu8k_shim_wc_read(void); #define pclog(...) fprintf(stderr, __VA_ARGS__) #define pclog_ex(fmt, ap) vfprintf(stderr, fmt, ap) diff --git a/emulator/vpx-device/vweawe.cpp b/emulator/vpx-device/vweawe.cpp index b4d36f3..c56afc8 100644 --- a/emulator/vpx-device/vweawe.cpp +++ b/emulator/vpx-device/vweawe.cpp @@ -46,6 +46,7 @@ #include "dosbox.h" #include "inout.h" +#include "regs.h" /* guest CS:IP in the storm trace */ #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN @@ -63,21 +64,71 @@ int wavetable_pos_global = 0; * per-chunk renders, so contention stays in the microseconds. */ static CRITICAL_SECTION awe_lock; -/* WC interpolation: real-time samples elapsed since the render thread's - * last pass, so guest busy-waits on the 44kHz sample counter see smooth - * advancement between render chunks */ -static volatile LONGLONG awe_last_render_qpc = 0; +/* WC: the EMU8000 sample counter is a FREE-RUNNING 44100Hz 16-bit counter on + * silicon -- it ticks regardless of what the host, driver, or synth is doing, + * and software (AWEUTIL /S in particular) busy-waits on it for every timed + * init step. Derive it from QPC alone. The old render-coupled version + * (rendered samples + interpolation capped at 100ms) crawled ~90x slow when + * a guest poll storm starved the render thread of the shared lock -- AWEUTIL + * "hang", root-caused 2026-07-16. See also awe_render_waiting below. */ static LONGLONG awe_qpc_freq = 1; -unsigned emu8k_shim_wc_extra(void) { +static LONGLONG awe_wc_origin_qpc = 0; +uint16_t emu8k_shim_wc_read(void) { + /* Free-running 44100Hz -- but with one more piece of silicon truth: on + * real ISA a poll loop reads the port many times per tick, so software + * NEVER observes the counter skipping a value, and AWEUTIL's timed waits + * exit on EQUALITY with a target tick. Our trapped port reads cost ~30us + * (heavier than one 22.7us tick), so a raw time-derived counter jumps + * +2..+4 per poll and equality targets get skipped -- each miss costs a + * full 1.49s wrap (the "AWEUTIL hang", 2026-07-16). So: while the guest + * is polling TIGHTLY (<200us between reads), advance at most one tick + * per read -- every value is observed, waits terminate, and the counter + * runs at worst ~25% slow during the poll itself. Any idle gap snaps it + * back to true wall-clock time. Single caller thread (emulation). */ LARGE_INTEGER now; QueryPerformanceCounter(&now); - LONGLONG d = now.QuadPart - awe_last_render_qpc; - if (d < 0) d = 0; - LONGLONG smp = (d * 44100) / awe_qpc_freq; - if (smp > 4410) smp = 4410; - return (unsigned)smp; + static uint16_t last_ret = 0; + static LONGLONG last_qpc = 0; + static unsigned reads_since_adv = 0; + LONGLONG gap_us = ((now.QuadPart - last_qpc) * 1000000) / awe_qpc_freq; + uint16_t ret; + if (gap_us < 50000) { + /* Poll storm. The hard constraint (AWEUTIL WaitUntilWC @5F42, + * decoded 2026-07-16): its wait exits on EQUALITY with a target + * tick, and each loop iteration reads WC TWICE -- so the counter + * must advance SLOWER than the guest reads, or the equality sample + * only sees every 2nd value and misses targets of the wrong parity + * forever. Real silicon polls at ~1us vs the 22.7us tick, so every + * value is observed many times; our trapped reads cost ~30us, more + * than a real tick, so we peg the storm-time counter to the READ + * rate instead: +1 per 4 reads. Every value is then observable by + * loops that re-check at least once per 4 reads (all of AWEUTIL's + * do), waits stretch ~5x (seconds, not minutes), and the dead-clock + * bailouts (8192 unchanged reads) never trip. Never consult real + * time mid-storm. */ + if (++reads_since_adv >= 4) { + last_ret = (uint16_t)(last_ret + 1); + reads_since_adv = 0; + } + ret = last_ret; + } else { + /* genuinely idle since the last read: resync to true wall time */ + ret = (uint16_t)(((unsigned long long) + (now.QuadPart - awe_wc_origin_qpc) + * 44100ull) / (unsigned long long)awe_qpc_freq); + last_ret = ret; + reads_since_adv = 0; + } + last_qpc = now.QuadPart; + return ret; } +/* Fairness gate: guest port I/O at storm rates (AWEUTIL polls WC ~550k/s) + * re-acquires awe_lock so fast that the render thread starves for seconds. + * The render thread raises this flag while it waits for the lock; the guest + * side backs off until it's through. */ +static volatile LONG awe_render_waiting = 0; + /* activity stats (reported every ~10s of rendered audio per card) */ struct AweStats { unsigned long wr, rd_wc, rd, smld_w; }; static AweStats awe_stats[2]; /* 0 = front, 1 = rear */ @@ -100,21 +151,35 @@ static AweIoRange *awe_io_find(Bitu port) { return NULL; } -/* VWE_AWE_LOG=1: trace the first accesses + growth-of-count milestones */ +/* VWE_AWE_LOG=1: trace the first accesses + growth-of-count milestones. + * VWE_AWE_LOG=2: additionally dump a 64-op CONSECUTIVE burst (with us + * timestamps) every 4M ops -- shows the guest's actual poll-loop structure + * instead of isolated samples. */ static int awe_log_on = -1; static unsigned long awe_access_count = 0; static void awe_io_trace(const char *dir, Bitu port, Bitu val, Bitu iolen) { if (awe_log_on < 0) { const char *l = getenv("VWE_AWE_LOG"); - awe_log_on = (l && l[0] && l[0] != '0') ? 1 : 0; + awe_log_on = (l && l[0] && l[0] != '0') ? (l[0] == '2' ? 2 : 1) : 0; } if (!awe_log_on) return; awe_access_count++; - if (awe_access_count <= 64 || - (awe_access_count & (awe_access_count - 1)) == 0) - fprintf(stderr, "VWE AWE32 io[%lu]: %s %03lx val=%04lx len=%lu\n", - awe_access_count, dir, (unsigned long)port, + bool burst = awe_log_on >= 2 && (awe_access_count & 0x3FFFFF) < 64; + if (awe_access_count <= 64 || burst || + (awe_access_count & (awe_access_count - 1)) == 0) { + LARGE_INTEGER t; + QueryPerformanceCounter(&t); + /* caller of the port-access primitive: real-mode [ss:bp+2] */ + unsigned ret_ip = mem_readw(((PhysPt)SegValue(ss) << 4) + + ((reg_bp + 2) & 0xFFFF)); + fprintf(stderr, "VWE AWE32 io[%lu @%lluus cs:ip=%04x:%04x ret=%04x]: %s %03lx val=%04lx len=%lu\n", + awe_access_count, + (unsigned long long)((t.QuadPart - awe_wc_origin_qpc) * 1000000 + / awe_qpc_freq), + (unsigned)SegValue(cs), (unsigned)reg_ip, ret_ip, + dir, (unsigned long)port, (unsigned long)val, (unsigned long)iolen); + } } /* PTR shadow per card: mirror of the core's cur_reg/cur_voice, so the glue @@ -141,6 +206,7 @@ static Bitu awe_io_read(Bitu port, Bitu iolen) { AweIoRange *r = awe_io_find(port); if (!r) return ~0ul; Bitu v; + while (awe_render_waiting) Sleep(1); /* let the render thread through */ EnterCriticalSection(&awe_lock); if (iolen >= 2 && r->inw) v = r->inw((uint16_t)port, r->priv); else if (r->inb) v = r->inb((uint16_t)port, r->priv); @@ -148,6 +214,22 @@ static Bitu awe_io_read(Bitu port, Bitu iolen) { LeaveCriticalSection(&awe_lock); AweStats &s = awe_stats[(port & 0x40) ? 1 : 0]; if ((port & 0xF9F) == 0xA02) s.rd_wc++; else s.rd++; /* A22/A42 */ + /* the RARE reads are the interesting ones during a WC-delay storm: log + * every read that isn't a WC poll, with the PTR shadow, to expose the + * never-satisfied outer wait condition (VWE_AWE_LOG>=1, first 200) */ + { + const int ci2 = (port & 0x40) ? 1 : 0; + if (awe_log_on > 0 && !((port & 0xF9F) == 0xA02 && + (awe_ptr[ci2] & 0xFF) == 0x3B)) { + static int rare_n = 0; + if (rare_n < 200) { rare_n++; + fprintf(stderr, + "VWE AWE32 rareR[%d]: port=%03lx ptr=%04x val=%04lx len=%lu\n", + rare_n, (unsigned long)port, awe_ptr[ci2], + (unsigned long)v, (unsigned long)iolen); + } + } + } if ((port & 0xF1C) == 0xA00) /* A20-A23/A40-A43 */ awe_sm_trace("R", (port & 0x40) ? 1 : 0, port, v); awe_io_trace("R", port, v, iolen); @@ -167,6 +249,7 @@ static void awe_io_write(Bitu port, Bitu val, Bitu iolen) { if ((port & 0xF1C) == 0xA00) awe_sm_trace("W", ci, port, val); awe_io_trace("W", port, val, iolen); + while (awe_render_waiting) Sleep(1); /* let the render thread through */ EnterCriticalSection(&awe_lock); if (iolen >= 2 && r->outw) r->outw((uint16_t)port, (uint16_t)val, r->priv); else if (r->outb) r->outb((uint16_t)port, (uint8_t)val, r->priv); @@ -323,13 +406,14 @@ static DWORD WINAPI awe_thread_proc(LPVOID) { Sleep(2); continue; } - /* render one 10ms chunk from both cards, sum into the slot */ - LARGE_INTEGER now; + /* render one 10ms chunk from both cards, sum into the slot. The + * waiting flag holds off guest port-I/O storms (see above) so this + * acquire can't be starved. */ + awe_render_waiting = 1; EnterCriticalSection(&awe_lock); + awe_render_waiting = 0; awe_render_chunk(&awe_front, 0, AWE_SLOT_FRAMES); awe_render_chunk(&awe_rear, 1, AWE_SLOT_FRAMES); - QueryPerformanceCounter(&now); - awe_last_render_qpc = now.QuadPart; LeaveCriticalSection(&awe_lock); int16_t *out = awe_slotbuf[free_slot]; for (unsigned i = 0; i < AWE_SLOT_FRAMES; i++) { @@ -496,6 +580,8 @@ void VWEAWE_Init(void) { LARGE_INTEGER f; QueryPerformanceFrequency(&f); awe_qpc_freq = f.QuadPart ? f.QuadPart : 1; + QueryPerformanceCounter(&f); + awe_wc_origin_qpc = f.QuadPart; /* WC free-runs from power-on */ emu8k_init(&awe_front, 0x620, ram_kb); emu8k_init(&awe_rear, 0x640, ram_kb);