Files
TeslaRel410/emulator/pod-launch/Options.cs
T
CydandClaude Fable 5 72da9b2f14 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>
2026-07-16 23:51:52 -05:00

191 lines
9.2 KiB
C#

// Argument parsing + path resolution. Paths resolve relative to --root (default:
// the exe's own directory, i.e. the deployed game dir), with dev fallbacks so
// the same exe runs against the emulator/ source tree via `--root <emulator>`.
using System;
using System.Collections.Generic;
using System.IO;
namespace VwePod
{
internal enum LayoutMode { Cockpit, Explode }
internal sealed class Options
{
public Mode Mode;
public LayoutMode Layout = LayoutMode.Cockpit;
public string Root;
public string Work;
public string DosBox;
public string DosBoxDir;
public string Conf;
public string BridgeScript; // dev: live_bridge.py via pyw
public string RendererExe; // deploy: frozen renderer exe (else null)
public string AweRom;
public bool NoBridge;
public bool NoSound;
public bool Mipmap;
public bool NoFocus;
public bool DryRun;
public string BridgePos = "0,0"; // main out-the-window head
// 832x512 = the dPL3 board's native framebuffer res (the game's view
// flush / pick coords are 832x512). Render 1:1 until the 800x600
// presentation question is settled (see LAUNCH.md).
public int BridgeW = 832;
public int BridgeH = 512;
public int DosBoxX = 10;
public int DosBoxY = 10;
// Cockpit head rects (deployment variant of pod_deploy.ps1's RECTS).
public string RadarRect = "800,0,640,480"; // VPX_WIN0 radar (HEAD C)
public string Mfd3Rect = "1440,0,640,480"; // VPX_WIN4 3-color MFD (HEAD B)
public string Mfd2Rect = "2080,0,640,480"; // VPX_WIN3 2-color MFD (HEAD A)
public static Options Parse(string[] args)
{
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++)
{
string a = args[i];
string Next(string name)
{
if (i + 1 >= args.Length) throw new ArgumentException(name + " needs a value");
return args[++i];
}
if (!a.StartsWith("-")) { modeName = a; continue; } // positional mode
switch (a)
{
case "--mode": modeName = Next(a); break;
case "--conf": confArg = Next(a); break;
case "--root": rootArg = Next(a); break;
case "--work": o.Work = Next(a); break;
case "--dosbox": dosboxArg = Next(a); break;
case "--bridge-script": bridgeScriptArg = Next(a); break;
case "--renderer": rendererArg = Next(a); break;
case "--awe-rom": aweArg = Next(a); break;
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); dosboxXySet = true; break;
case "--no-bridge": o.NoBridge = true; break;
case "--no-sound": o.NoSound = true; break;
case "--mipmap": o.Mipmap = true; break;
case "--no-focus": o.NoFocus = true; break;
case "--dry-run": o.DryRun = true; break;
case "-h": case "--help": Usage(); Environment.Exit(0); break;
default: throw new ArgumentException("unknown arg: " + a);
}
}
o.Root = rootArg ?? AppContext.BaseDirectory;
o.Mode = Modes.Find(modeName ?? "bt");
if (o.Mode == null)
throw new ArgumentException("unknown mode '" + modeName + "'; valid: " + Modes.Names());
// Unsupported modes: return with only the Mode set; Program prints the
// 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"))
?? throw new ArgumentException("dosbox-x.exe not found under root; pass --dosbox");
o.DosBoxDir = Path.GetDirectoryName(o.DosBox);
o.Conf = confArg ?? FirstExisting(Path.Combine(o.Root, o.Mode.ConfBase + ".conf"));
if (o.Conf == null)
{
// A .tmpl sitting there means the install was extracted but never
// configured -- point at the real fix, not at --conf.
bool unrendered = File.Exists(Path.Combine(o.Root, o.Mode.ConfBase + ".conf.tmpl"));
throw new ArgumentException(unrendered
? o.Mode.ConfBase + ".conf not rendered yet: this install hasn't been configured.\n" +
"Run postinstall.bat (elevated) from the extracted zip root, or:\n" +
" powershell -File <install>\\deploy\\configure.ps1 -Root <install>"
: o.Mode.ConfBase + ".conf not found under root; pass --conf");
}
o.RendererExe = rendererArg ?? FirstExisting(
Path.Combine(o.Root, "renderer.exe"),
Path.Combine(o.Root, "render-bridge", "renderer.exe"));
o.BridgeScript = bridgeScriptArg ?? FirstExisting(
Path.Combine(o.Root, "live_bridge.py"),
Path.Combine(o.Root, "render-bridge", "live_bridge.py"));
if (!o.NoBridge && o.RendererExe == null && o.BridgeScript == null)
throw new ArgumentException("no renderer found; pass --renderer, --bridge-script, or --no-bridge");
o.AweRom = aweArg ?? FirstExisting(
Path.Combine(o.Root, "roms", "awe32.raw"),
Path.Combine(o.Root, "..", "roms", "awe32.raw"));
o.Work = o.Work ?? Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Temp", "vwe-pod");
return o;
}
private static void ParseWH(string s, out int a, out int b)
{
var parts = s.Split(',');
if (parts.Length != 2 || !int.TryParse(parts[0], out a) || !int.TryParse(parts[1], out b))
throw new ArgumentException("expected 'x,y': " + s);
}
private static string FirstExisting(params string[] paths)
{
foreach (var p in paths)
{
if (p == null) continue;
var full = Path.GetFullPath(p);
if (File.Exists(full)) return full;
}
return null;
}
public static void Usage()
{
Console.Error.WriteLine(@"pod-launch [mode] [options]
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 | 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)
--work <dir> runtime/work dir (default: %LOCALAPPDATA%\Temp\vwe-pod)
--layout cockpit|explode window layout (default cockpit)
--dosbox <exe> explicit dosbox-x.exe
--renderer <exe> frozen renderer exe (else falls back to pyw live_bridge)
--bridge-script <py> live_bridge.py (dev)
--awe-rom <raw> AWE32 SoundFont ROM
--bridge-pos x,y main render window position (default 0,0)
--bridge-size w,h main render window size (default 832,512 = dPL3-native)
--dosbox-xy x,y DOSBox window position + focus (default 10,10)
--no-bridge pod only, no GL render window
--no-sound skip the ~4-min SoundFont upload. WARNING: no AWE32 =
no SOS ticks; bt/rp FREEZE at RIO init on the default
FAST-clock confs (only safe with a SLOW-clock conf)
--mipmap VRVIEW_MIPMAP=1 (RP floor shimmer fix; non-authentic)
--no-focus skip the window layout/focus pass
--dry-run resolve paths + print the launch plan, then exit");
}
}
}