First real run of -fps (222 seconds, 4-monitor bench, in-mission) showed a
rock-solid 60.0 fps average but a worst frame of ~24ms in 82% of seconds, with a
median of 24.4ms and a minimum of 23.2ms. That regularity was the tell: 60Hz vsync
intervals are 16.7 / 33.3 / 50.0 ms, so 24ms is not a multiple of anything and had
no business recurring once per second with that consistency.
Cause: the logger was measuring itself. The per-second line was written straight to
the file with WriteFile on the render thread, and the frame duration is recorded
BEFORE the line is emitted -- so the write's cost landed in the *next* frame's
measurement, which belongs to the next second's bucket. Result: exactly one
inflated frame per second, indefinitely. Holding the handle open (as the first
version did) was not enough; a single WriteFile is still several milliseconds when
something like antivirus is in the path.
Three fixes:
1. Buffered output. Lines accumulate in a 128 KB static buffer (~2000 rows, about
33 minutes at one row per second) and are flushed only when the buffer fills or
at exit via atexit(). A normal measurement session now performs no file writes
at all while running. Trade-off, deliberately accepted: a hard crash loses the
un-flushed tail. gos-displays.txt remains the crash-survivable log; gos-fps.txt
is a measurement instrument and must not perturb what it measures.
2. Meaningful percentiles. A "1% low" over 60 samples per second computes
nFrames/100 = 0, which was clamped to 1 -- so the column was literally
1000/worst_frame, i.e. the worst-frame column restated in different units.
Verified against the log: worst 24.1ms -> 41.5 fps, exactly the "1% low" printed.
Per-second is now a 5% low (worst 3 of 60), which is a number that actually
differs from the worst frame. True whole-session 1% and 0.1% lows are computed
at exit from a 0.5ms-bucket frame time histogram (2000 buckets, 0-1000ms) and
printed in a new session summary along with total frames, elapsed time, average,
and total hitches. The histogram gives exact-enough percentiles over an entire
session without retaining every frame time.
3. Carry-over. A single frame longer than one second (a level load: the log shows
4338ms and 4799ms frames) left the accumulator above the 1000ms threshold, so
the next few frames each emitted their own bogus one-frame row -- visible in the
log as rows reporting "1 frame, 1339 fps". The excess is now discarded.
Also adds #include <stdlib.h> to WinMain.cpp for atexit(); it was not reachable
through pch.hpp. GOS_FpsAtExit is declared __cdecl: GameOS builds with /Gz, which
makes __stdcall the default calling convention, but atexit() takes a __cdecl
callback -- without it VC6 rejects the call with C2664.
What the run did establish, and still stands: 60 frames per second sustained for
~150 seconds with no rhythmic multi-hitch pattern, so the mode 4 split-MFD stutter
fix (0a657b59) is holding. Genuine dropped frames (>40ms) occurred in 18% of
seconds, including clusters of 80-90ms; those are real and unaffected by this fix.
Re-measure after this change to see the true baseline.
CLAUDE.md STEP 10 updated: the previous claim that holding the handle open kept the
logger from perturbing the measurement was wrong, and is replaced with what was
actually observed.
Co-authored-by: Claude Opus 5 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
Four-monitor MFD bring-up on the new bench (MR_new: AMD FirePro W4100, 4 outputs,
Win10). Adds permanent display diagnostics, a Release-capable frame pacing logger,
a self-locating AppCompat shim installer, and settles -- empirically -- whether
dgVoodoo2 can be dropped for the multi-monitor MFD modes. It cannot.
WHY THIS WAS HARD
-----------------
CHSH_Device::InitFirst/InitSecond discarded EVERY HRESULT (SetCooperativeLevel,
SetDisplayMode, CreateSurface, GetAttachedSurface, QueryInterface, CreateDevice)
and returned true unconditionally. A panel that failed to open produced no error,
no crash and no log entry -- the monitor just stayed on the desktop. SPEW is
compiled out of shipping builds, so none of it was visible. Restoring that
visibility is what unblocked everything else.
DIAGNOSTICS ADDED (keep these)
------------------------------
* VideoCard.cpp -- LogDisplayDevices() writes gos-displays.txt next to the exe:
NumDevices/NumHWDevices/NumMonitors, every DirectDraw device with its
hw_rasterization flag, the role assignment (FullScreenDevice / g_nNonDualHead /
g_nDualHead / g_nDualHead2 / g_nMFD1 / g_nMFD2), per-slot -tmon APPLIED/REJECTED
(previously silent), and whether mode 4's "BOTH mfd1 and mfd2" requirement is met.
* render.cpp -- HSH_LogInit()/HSH_CheckHR()/HSH_HRName() log every DirectDraw call
in the panel init path with its HRESULT decoded by name (27 DDERR_* codes, all
verified present and collision-free against build-env/dx7asdk/include/ddraw.h).
Each line is opened/appended/closed individually so the log survives a crash.
* WinMain.cpp -- GOS_LogFrameRate() writes gos-fps.txt: per second, frame count,
average fps, 1% low (mean of the worst 1% of frames), worst frame in ms, and a
count of frames exceeding 2x average ("hitches"). Average fps alone cannot
distinguish 60fps from 60fps-with-a-dropped-frame-every-second; the 1% low can.
The engine's own FrameRate readout is #ifdef LAB_ONLY (MWMission.cpp) so it only
exists in MW4pro.exe; this works in Release, which is what runs on the pods.
NEW SWITCHES (both documented in -help)
---------------------------------------
* -fps Enable the frame pacing report. Off by default: the gate is the first
statement in GOS_LogFrameRate, so an unflagged run does no arithmetic
and does not even create the file. The file handle is held open for the
process lifetime -- opening/closing it every second would put a syscall
of unpredictable latency on the render thread, i.e. the measurement tool
perturbing what it measures.
* -tcoop <0-5> Selects the SetCooperativeLevel form used by the radar/MFD panels.
0 = legacy (unchanged shipped behaviour, remains the default).
Exists so every candidate fix could be compared on real hardware without
a rebuild between attempts.
CRASH-SAFETY FIX
----------------
hsh_initialized was set unconditionally after panel init, so a failed panel left
null surfaces and a null IDirect3DDevice7 behind and the per-frame path called
straight through them. Now:
- all four InitSecond overrides (CMR/CRadar/CMFD/CMFDRight) bail on base failure,
- CMFD_Device::InitFirst reports its real result instead of always returning true,
- hsh_initialized is only set when the panels genuinely came up.
A display failure now leaves the game running without MFDs instead of bombing to
desktop. Crash signature for the record: `call [ecx+0x44]` with ECX=0 is
IDirectDrawSurface7::GetDC on a never-created surface (vtable offset confirmed
against the DX7 header), reported as "Attempt to read from address 0x00000044".
APPCOMPAT SHIM INSTALLER (new)
------------------------------
build-env/set-appcompat.ps1 + set-appcompat.bat. Self-locating via $PSScriptRoot:
applies DWM8And16BitMitigation to the MW4 executables sitting next to it, wherever
that install lives. HKCU always, HKLM too when elevated (the HKLM value format
differs -- it carries a leading "$" marker -- so the two must not be interchanged).
Verifies by reading back; detects the HIGHDPIAWARE-only entry that SUPPRESSES the
automatic shim; supports -Remove and -WhatIfOnly. deploy-mw4.ps1 now ships both
files into every deployment.
This matters because the layer is keyed on the executable's FULL PATH -- any copy
of an install to another folder or machine silently loses it, and the resulting
error is actively misleading (see below).
WHAT WE LEARNED
---------------
* The AppCompat shim SYNTHESISES 16-bit display modes. Proved directly: the crash
dump shows "16 bit modes :" EMPTY without it and fully populated with it. MW4
renders at bitdepth=16 and modern GPUs expose no 16-bit modes at all.
* Without the shim, GameOS reports "Another application is preventing use of full
screen mode" (GOS_DXRASTERIZER_NOFULLSCREEN, DXRasterizer.cpp ~1125). That is a
catch-all fired after every SetDisplayMode attempt fails -- it even scans for
NetMeeting -- and it sends you looking for a conflicting program that does not
exist. The real cause is the missing shim.
* Exclusive fullscreen DOES work on Win10 with the system ddraw.dll and dgVoodoo2
physically removed, once the shim is applied to that exe path.
* Windowed mode works natively with no shim at all: the windowed path sets
Environment.bitDepth = DesktopBpp (32), so there is no mode switch. Verified for
the console/shell; a full mission windowed is still untested.
* Native DirectDraw enumerates all four W4100 outputs, so dgVoodoo2 was never
needed for device enumeration.
* The engine is 4:3 ONLY. ImageHlp.cpp ~464 asserts the complete supported set:
640x480, 512x384, 800x600, 960x720, 1024x768, 1280x1024 (5:4), 1600x1200. No
16:9 mode and no aspect correction anywhere in the codebase. On a 16:9 monitor
the scaler must adapt: plain stretch distorts geometry, keep-aspect pillarboxes.
* -2dt is not a recognised switch anywhere in the codebase, despite appearing in
production ctcl.ini launch lines. Completely inert.
* NumHWDevices (5) can exceed NumDevices (4): it counts D3D device-enumeration
callbacks, and an adapter exposing both a HAL and a T&L HAL yields two. Benign.
WHAT WE TRIED AND WHY IT FAILED
-------------------------------
The panel cooperative-level call was genuinely wrong -- a latent 2002 bug. Every
panel asked to be BOTH the process focus window AND its own device window, on the
one shared hWindow, after the main device had already taken exclusive mode on it.
The main device (DXRasterizer.cpp ~1027) already uses the correct two-call idiom
(SETFOCUSWINDOW alone, then EXCLUSIVE|FULLSCREEN) -- tagged //sanghoon, the same
author. The panels never were.
Results on real hardware, no dgVoodoo2, shim applied, -tmfds 4:
-tcoop 0 SETFOCUSWINDOW|CREATEDEVICEWINDOW|ALLOWREBOOT|EXCLUSIVE|FULLSCREEN
-> DDERR_EXCLUSIVEMODEALREADYSET
-tcoop 1 CREATEDEVICEWINDOW|EXCLUSIVE|FULLSCREEN (no focus claim)
-> DDERR_INVALIDPARAMS (CREATEDEVICEWINDOW needs a focus window)
-tcoop 2 SETFOCUSWINDOW, then CREATEDEVICEWINDOW|EXCLUSIVE|FULLSCREEN
-> DDERR_INVALIDPARAMS
-tcoop 3 SETFOCUSWINDOW, then EXCLUSIVE|FULLSCREEN
-> first panel collides, but that collision STEALS exclusive mode from
the main display, after which panels 2 and 3 fully initialise (the
radar reached CreateDevice(HAL) = DD_OK -- a secondary panel running
entirely on native DirectDraw). Side effect: the desktop was left at
1920x1080 16bpp. Not viable.
-tcoop 4 EXCLUSIVE|FULLSCREEN only -> EXCLUSIVEMODEALREADYSET, all panels
-tcoop 5 ALLOWREBOOT|EXCLUSIVE|FULLSCREEN -> EXCLUSIVEMODEALREADYSET, all panels
CONCLUSION: on modern Windows only ONE DirectDraw object per process may hold
exclusive fullscreen. The main display takes it; every secondary panel is refused.
XP allowed multiple. dgVoodoo2 allows it because it is a full reimplementation of
ddraw and is not bound by that rule -- it is not papering over a bug we can fix.
=> dgVoodoo2 CANNOT be removed for -tmfds 1/3/4 by correcting these flags. The
default stays -tcoop 0. The switch is retained because it is how this was settled
and it will re-settle it on different hardware.
The only native path is a borderless windowed panel design (DDSCL_NORMAL + clipper
per monitor, no exclusive mode anywhere). Assessment and staged plan are recorded
in CLAUDE.md STEP 10; not started.
WORKING 4-MONITOR CONFIG (with dgVoodoo2)
-----------------------------------------
dgVoodoo2 Scaling mode MUST be "Stretched, Keep Aspect Ratio". Plain "Stretched"
fails silently: main and radar go fullscreen black, both MFD monitors keep showing
the desktop, and every DirectDraw call still returns DD_OK -- the devices are alive
but dgVoodoo2 never drives those outputs. Diagnosed with a temporary per-panel
colour-flash test (since removed), which also established that device index maps
1:1 to physical monitor on this bench, so -tmon 1,2,3,4 equals auto-detection.
Confirmed working end to end: all three secondary panels present, full mission
played.
KNOWN GAP: the working dgVoodoo.conf is still not versioned in the repo (removed
in 0ceba9c7), so a fresh deploy will reproduce the silent MFD failure.
Behaviour with no new switches supplied is unchanged from the previous build
except on failure paths, which now degrade gracefully instead of crashing.
Co-authored-by: Claude Opus 5 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
- Stabilize Load File first-click behavior across mission/map/options/slots.
- Fix map sequencing by rebuilding game-type scenario list before mission lookup.
- Resolve mission-name miss to map index 0 fallback for selected game type.
- Fix decal handling: map INI decal IDs to dropdown indices, clamp invalid indices, display actual decal IDs in UI labels, and avoid redraw-time decal overwrite.
- Fix option apply timing and UI visibility issues, including Weapon Jam refresh.
- Add NoReturn support end-to-end: parse/store in MW4Shell auto globals, expose script variable, and apply to respawn/no-return mission params and UI checkbox.
- Update autoconfig spec to reflect actual parser/default/fallback behavior and add NoReturn examples.
Battlemaster stock loadouts (Content\Mechs\Battlemaster\battlemaster.subsystems):
- Replace lone MediumPulseLaser with full IS stock: PPC (Special2, group 1),
6×ML (3 RT + 3 LT, group 1), 2×MG (LA, 200 rds, group 1), SRM6 (Special1,
15 rds, group 2).
Battlemaster Clan 2C (battlemaster2c.subsystems):
- Replace ClanMediumPulseLaser with: ER PPC (RA, group 1), 6×ER ML (3 RT + 3
LT, group 1), 2×Clan Gauss (LA, 16 rds each, group 1), Clan SSRM6 (Special1,
15 rds, group 2).
Behemoth / Behemoth2 (.subsystems):
- Move Gauss rifles from weapon group 3 → group 1 (3 occurrences each).
Resource builder (build-resources.ps1):
- Always run with -window (windowed + DDrawCompat). Fullscreen native DDraw
fails on VMs with the generic Microsoft display adapter.
- Remove dgVoodoo2 D3D interceptors (D3D8/D3D9/D3DImm.dll) from Gameleap\mw4:
they silently break the builder (process exits 0 without packing anything).
Script also defensively moves any such files aside via $dgvMoved block.
- Remove dgVoodoo.conf and dgVoodooCpl.exe (abandoned experiment, no longer used).
- Expected: 'Hardware Error: not compatible with MechWarrior 4' dialog at end
of build on VMs -- click OK, packages are built correctly regardless.
CLAUDE.md: updated with mfdsplit branch notes covering all 2026-07-18 work.
Co-authored-by: Claude Sonnet 4.6 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
-tbaud <rate> (9600-921600) overrides the COM1 baud rate while keeping
old-RIO (type 0) protocol behavior unchanged; independent of -trio.
SetupComm buffers 2/2 -> 1024/1024 only when the override is active.
Rebuilt Release + Profile (0 errors), deployed rel.bin\MW4.exe to MW4\.
run-firestorm driver: game-start now takes optional extra args.
CLAUDE.md: branch notes incl. the CP949 comment-encoding hazard
(mw4 sources must be edited byte-safely).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
FS507D_20161015 release analysis wrap-up (drop itself is gitignored; art-review
folder kept local for review):
- Recovered the only assets our tree lacked: 13 hsh HUD/radar/mech bmps and the
two 5.07D lobby decals (decal_46/47.tga, extracted from release props.mw4 via
a ported gos_LZDecompress).
- ResourceImagePool.cpp: missing-texture placeholder is now LAB_ONLY (editor
keeps degraded mode); Release restores the original fatal STOP. Release +
Profile rebuilt, 0 errors.
Console script reconciled:
- ConLobby.script.new ("BattleTech Console V5.0.7Df", newest revision anywhere)
promoted to Content\ShellScripts\ConLobby.script; .new removed; stale loose
deploy copy removed. Verified in-game (console title shows V5.0.7Df).
- Corrected CLAUDE.md: the release never "renamed" the console to
ComputerPlayer.script -- the resource packer stores script contents under
alphabetically skewed entry names (runtime resolves the same pairing).
Packer quirks documented (stale entry carry-forward on incremental builds,
name/content skew in directory sweeps).
Mechlab bug fix (first FireStorm bug hunt):
- New-Mech dialog showed blank rows for Wolfhound/Zeus and crashed (KERNELBASE
read AV) when creating a Zeus. Root cause: newmech in chassis.script created
its Type droplist without setting $$m_listBoxSize$$, so capacity defaulted to
60 while 65 chassis were written in -- OOB script-array writes. Fixed by
sizing the list from $$m_chassisCount$$. Latent stock-MW4 bug armed when the
FireStorm roster passed 60 chassis. Verified: Zeus variant creates cleanly.
props.mw4 fully repacked (decals + promoted console + mechlab fix, junk
.new/.org entries gone); game deploy refreshed at MW4\. Gitignore: FS507D drop
and player-created MW4\Resource\Variants.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ADDING-A-MECH.md: full workflow for adding a 'Mech chassis (MSL ADD MECH
touch points, positional Mech IDs, scriptaddmech.xls generator, hardpoints
in .damage, rebuild/repack/deploy steps)
- ADDING-A-MAP.md: full workflow for maps (MapCreator terrain) vs missions
(MW4Ed2-authored), .nfo MP registration, user vs stock routes, pitfalls
- CLAUDE.md: reference pointers to both new docs
- README.md: annotated folder map; warning to read RECOVERY.md before cloning
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Complete disaster-recovery snapshot: engine/game source, game data assets,
VC6 toolchain + DX SDKs, build outputs, deployed game, and _UNUSED archive.
Large binaries in Git LFS; text preserved byte-for-byte (core.autocrlf=false,
no eol attributes). See RECOVERY.md for the one-clone rebuild procedure.