Emulator: parity-safe EMU8000 sample counter -- AWEUTIL runs; wire the
VGL_LABS cockpit test suite as pod-launch 'test' mode The WC hang, root-caused in three layers (each necessary): 1. WC was rendered-samples + 100ms-capped interpolation; AWEUTIL's poll storm (~550k port-ops/s) starved the render thread of awe_lock, so the clock crawled ~90x slow. Fixed: free-running host-clock derivation + a fairness gate so the render thread can always take the lock. 2. Free-running at true 44.1kHz still failed: trapped port reads cost ~30us -- MORE than one 22.7us tick -- so consecutive reads skipped counter values, and AWEUTIL's WaitUntilWC (decoded at COM offset 0x5F42) exits only on EQUALITY with a target tick: skipped value = missed target = 1.49s wrap penalty, or forever. 3. Advancing +1 per read still failed: WaitUntilWC reads WC TWICE per iteration, so its equality sample saw only every 2nd value -- wrong parity = infinite loop. Final semantics: during a poll storm the counter advances once per FOUR reads (every value observable by all of AWEUTIL's loop shapes; its 8192-unchanged-reads dead-clock bailout never trips), and resyncs to true wall time after any 50ms idle gap. Result: the full stock TEST.BAT (DIAGNOSE + AWEUTIL /S on both cards) completes in seconds. Also: pod-launch 'test' mode -> vwetest.conf (stock TEST.BAT -> TSTALL), DOSBox window defaults to 900,600 in test mode (the DOS screen is the suite's UI), VWE_AWE_LOG gains storm bursts with guest cs:ip + caller and rare-read tracing (the instrumentation that cracked this). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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\<folder>),
|
||||
# @@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 ===
|
||||
@@ -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,
|
||||
|
||||
@@ -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 <name> same as the positional mode
|
||||
--conf <path> explicit conf (overrides the mode's conf)
|
||||
--root <dir> base dir for dosbox/conf/bridge (default: exe dir)
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
+106
-20
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user