Files
firestorm/WINDOWED-MODE.md
T
22495d7245 Docs: research notes on making -window work for all display modes
Research only. Nothing implemented, nothing scheduled. Captured now so the
investigation does not have to be repeated when we come back to it.

New file: WINDOWED-MODE.md (repo root), indexed from README.md and the
CLAUDE.md next-steps list.

Goal being assessed
-------------------
Make -window work for every pod configuration, so a full pod (main + radar
+ MFDs, or a cameraship pair) can be tested on a single monitor as a set
of moveable windows. Target order: console (already works), cameraship,
-tmfds 1, -tmfds 4. -tmfds 3 deliberately out of scope.

Headline finding
----------------
-window does NOT fail for the MFD and cameraship modes - it silently
DISABLES them, and the reason is a single line:

  MW4Application.cpp:1373
    use_shgui = Environment.fullScreen ? 1 : 0;

use_shgui is the master switch for the entire secondary-panel subsystem
and is derived from fullscreen, so with -window it is 0 and
DXRasterizer.cpp:1131 never calls HSH_EnterFullScreen2(). No panel is ever
created. Console mode only appears to work windowed because it has no
panels to lose.

What the document covers
------------------------
* Why it behaves the way it does today, with the exact gating code.
* What already exists in our favour: the main display is a working
  windowed reference implementation (primary + clipper + offscreen
  backbuffer, presented with Blt); the clipper wrappers are already in
  use; and none of the panel DRAWING code would change, because panels
  render to an offscreen target and composite - they are indifferent to
  whether the present is a Flip or a Blt.
* The six things that must change, with quoted code: decouple use_shgui;
  windowed variants of CHSH_Device::InitFirst (DDSCL_NORMAL, no
  SetDisplayMode, per-panel HWND) and InitSecond (plain primary + clipper
  + offscreen backbuffer, D3D device on the offscreen); one window per
  panel; 8 Flip->Blt sites in WinMain.cpp; and explicit frame pacing.
* Frame pacing is called out as the item that will actually bite: sh_step
  advances once per frame and drives MFD channel cycling, so an uncapped
  windowed rig would not faithfully represent pod behaviour. Same root
  cause as the known mechlab fast-spin bug.
* Reference tables: every panel class with its InitFirst location, device
  slot and resolution; the SwapRightState member-swap mechanism that any
  windowed work must keep intact; and the full lifecycle/gating map.
* Suggested phasing, with radar-only as the decisive Phase 1 experiment.

The risk that decides it
------------------------
Windowed D3D7 device creation is per-GPU, and this would need four
windowed devices in one process. Precedent runs both ways: the mission
editor hit DDERR_INVALIDOBJECT creating a windowed D3D device on Win11 and
needed DDrawCompat (STEP 8), while the game's own windowed main display
succeeds on the W4100. Nothing has proven four. If it fails, the fallback
means rewriting the panel RENDER path, not just its present - a
substantially bigger job.

Why this may be worth more than a test convenience
--------------------------------------------------
Windowed mode removes exclusive-mode contention entirely, which is the
whole reason dgVoodoo2 is currently mandatory for every multi-display
configuration on Windows 10/11 (STEP 10). If windowed panels work
natively, that is a route to dropping the dgVoodoo2 prerequisite WITHOUT
breaking the XP pods, since it needs no external DLL on either OS. Same
groundwork as the borderless-windowed migration already parked in STEP 10.

Note on citations
-----------------
All 22 file:line references were derived directly and verified to resolve
to the expected symbol. An exploratory pass had produced line numbers that
were substantially wrong (CHSH_Device::InitFirst reported at 717, actually
627; IsMultimonitorAvaliable at 2178, actually 2961), and a claim that the
radar renders at 480x640 rotated - the code passes 640,480 like the
others. Worth knowing before trusting generated citations in a document
intended to outlive the session.

Co-authored-by: Claude Opus 5 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
2026-08-05 17:31:09 -05:00

12 KiB

Making -window work for all display modes ? research notes

Status: RESEARCH ONLY. Nothing implemented. Parked 2026-08-05.

Goal: make -window work for every pod configuration, so a full pod (main + radar + MFDs, or a cameraship pair) can be tested on a single monitor as a set of moveable windows.

Target configurations, in the order they'd be tackled:

Configuration Windows needed Status today
Console (-tmfds 0, CTCL console) 1 (main) Already works
Cameraship 2 (main + Map/Armor) Panels silently disabled
-tmfds 1 (spanned MFD) 3 (main + radar + one 1280x480 MFD) Panels silently disabled
-tmfds 4 (split MFD) 4 (main + radar + 2x 640x480 MFD) Panels silently disabled

-tmfds 3 deliberately out of scope for now.

Companion reading: CLAUDE.md STEP 10 (the borderless-windowed assessment and the exclusive-mode wall), STEP 8 (the editor's windowed D3D7 failure), STEP 12 (panel lifecycle and the re-entry fix).


1. Why -window only works in console mode today

It is not a rendering limitation. It is one line, in MW4Application.cpp:1373:

Environment.fullScreen = (strstr(all_lower, "-window") == NULL);
use_shgui = Environment.fullScreen ? 1 : 0;

use_shgui is the master switch for the entire secondary-panel subsystem, and it is derived from fullscreen. With -window it becomes 0, so DXRasterizer.cpp:1131 never calls HSH_EnterFullScreen2():

//sanghoon begin
if(use_shgui){
        HSH_EnterFullScreen2();
}
//sanghoon end

No panel is ever created. Console mode appears to "work" windowed only because it has no panels to lose. -window does not fail for the other modes ? it silently disables them.

The teardown is gated the same way at DXRasterizer.cpp:1407.


2. What already exists that helps

The job is smaller than it sounds, because a working windowed reference implementation is already in the tree ? the main display does exactly what each panel would need to do.

  • Windowed main display: primary surface + clipper + offscreen backbuffer, in SetupMode() (DXRasterizer.cpp:1623, windowed branch from the if( FullScreen ) at :1718).
  • Clipper wrappers exist and are already used: DXRasterizer.cpp:1868-1870
    wCreateClipper( DDobject,0, &ClipperObject, NULL );
    wSetHWnd( ClipperObject, 0, hWindow );
    wSetClipper( FrontBufferSurface, ClipperObject );
    
    Wrapper definitions in DirectDraw.cpp (wCreateClipper / wSetHWnd / wSetClipper).
  • The windowed present already exists: DXRasterizer.cpp:1287 blits the backbuffer to a screen rect, versus the fullscreen wFlip at :1357. Blt stretches when the destination rect differs in size, so aspect-preserving output is rect maths, not new machinery.
  • No panel drawing code changes. Panels render into an offscreen pDDSTarget and composite it onto their own backbuffer with DrawPrimitive (CMFD_Device::EndChannel). DrawQuad, DrawTexture, the fonts and the mode-4 SwapRightState juggling are all indifferent to whether the present is a Flip or a Blt.
  • Panel init already fails gracefully (the [panelcoop] work in STEP 10), so a half-migrated state degrades to "no panels" rather than dereferencing null surfaces.

3. What has to change

3.1 Decouple use_shgui from fullscreen

MW4Application.cpp:1373. use_shgui should mean "this configuration has panels" ? derived from g_nTypeOfMFDs and the CTCL type ? not "we are fullscreen". One line, but nothing else can be tested until it changes.

3.2 CHSH_Device::InitFirst is hard-wired to exclusive fullscreen

render.cpp:627. Default path (-tcoop 0):

const DWORD dwFlags = DDSCL_SETFOCUSWINDOW | DDSCL_CREATEDEVICEWINDOW |
                      DDSCL_ALLOWREBOOT | DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN;
hr=pDD->SetCooperativeLevel( hWindow, dwFlags );
...
hr=pDD->SetDisplayMode( resx, resy, 16, 60, 0 );

Windowed needs DDSCL_NORMAL, no SetDisplayMode at all, and each panel's own HWND rather than the single shared global hWindow.

3.3 CHSH_Device::InitSecond creates a fullscreen flip chain

render.cpp:832:

ddsd.dwFlags = DDSD_BACKBUFFERCOUNT | DDSD_CAPS;
ddsd.dwBackBufferCount = 1;
ddsd.ddsCaps.dwCaps = DDSCAPS_COMPLEX | DDSCAPS_FLIP |
                      DDSCAPS_PRIMARYSURFACE | DDSCAPS_3DDEVICE;
hr=pDD->CreateSurface( &ddsd, &pDDSFront, NULL );
hr=pDDSFront->GetAttachedSurface( &ddsCaps, &pDDSBack );
...
hr=pD3D->CreateDevice(IID_IDirect3DHALDevice,pDDSBack,&pD3DDevice);

Windowed needs: plain DDSCAPS_PRIMARYSURFACE, a clipper bound to that panel's window, a separate DDSCAPS_OFFSCREENPLAIN|DDSCAPS_3DDEVICE backbuffer, and the D3D device created on the offscreen backbuffer. Structurally identical to the main display's windowed branch.

3.4 No panel windows exist

Only one window is created, at Windows.cpp:261/273. Each windowed panel needs its own CreateWindowEx, sized to the panel resolution, with a message loop that survives being dragged.

Watch the topmost style: Windows.cpp:271 applies WS_EX_TOPMOST for every CTCL type except console/none. Left as-is, the main window would sit on top of the panel windows.

3.5 Panel present: 8 Flip sites to convert

All in WinMain.cpp, in the if(hsh_initialized) block around line 1347:

Line Device Mode
1355 radar_device -tmfds 1
1357 mfd_device -tmfds 1
1365 (commented out) -tmfds 2
1369 mfd_device -tmfds 2
1385 radar_device -tmfds 4
1387 mfd_device (left) -tmfds 4
1390 mfd_device_right -tmfds 4
1396 mr_device cameraship

All of the form:

device.pDDSFront->Flip(0,DDFLIP_DONOTWAIT|DDFLIP_NOVSYNC);

Each becomes a Blt from that panel's backbuffer to its window's client rect in screen coordinates. Mechanical ? but the mode-4 stagger logic around them must be preserved exactly (that is the eaa5fd3 fix; see CLAUDE.md "MFD mode 4: right device stagger fix").

3.6 Frame pacing ? the one that will actually bite

Fullscreen Flip paces the panels. A windowed Blt has no vsync whatsoever. This is the same root cause as the known mechlab fast-spin bug (DXRasterizer.cpp:1252 DisplayBackBuffer, focused-windowed uses the unsynced Blt path).

It matters more than usual here because sh_step advances once per frame and drives the MFD channel cycling. At uncapped FPS the channels cycle far faster than on a real pod, so a windowed test rig would not faithfully represent pod behaviour without an explicit frame cap.

Options: scale by frame time, cap explicitly, or use gos_Set_LoseFocusBehavior mode 3 (the built-in 60 Hz cap, Windows.cpp).


4. Reference: how the panels are currently wired

Panel classes (all derive from CHSH_Device)

Class InitFirst Device slot Resolution
CMR_Device (cameraship Map/Armor) render.cpp:1213 g_nNonDualHead, else g_nDualHead 640x480
CMFDRight_Device render.cpp:1385 g_nMFD2 640x480
CRadar_Device render.cpp:1456 g_nNonDualHead 640x480
CMFD_Device render.cpp:1719 g_nMFD1 (mode 4) or g_nDualHead 640x480 (mode 4) / 1280x480 (spanned)

Shared-device case: CRadar_Device passes &mfd_device as pOtherHSHD when g_nTypeOfMFDs == 2, so the two panels share one IDirectDraw7. InitFirst/InitSecond both branch on that.

CMFD_Device::SwapRightState() (render.cpp:1774) swaps every device member (surfaces, D3D device, fonts, sizes) between the left and right devices so mode 4 can route a channel to the right panel without duplicating the drawing code. Any windowed work must keep this intact ? it swaps pDD, pDDSFront, pDDSBack, pDDSTarget, pD3DDevice and more, so a per-panel HWND would need to be swapped alongside them (or deliberately not).

Lifecycle

  • HSH_EnterFullScreen2() ? render.cpp:2999. Chooses between the multi-monitor path, the single-secondary (mr_device) path, and none.
  • HSH_DirectDrawRelease2() ? render.cpp:3097.
  • IsMultimonitorAvaliable() ? render.cpp:2961. Returns false for cameraship, so cameraship falls through to IsSecondaryMonitorAvaliable() (render.cpp:2989) and the single mr_device. Both would need windowed-aware equivalents.
  • hsh_initialized / hsh_mrdev_initialized gate all per-frame panel work (WinMain.cpp ~1174 and ~1347, hudchat.cpp 168/398/913).

5. Suggested phasing

Phase Work Risk
0 Decouple use_shgui from Environment.fullScreen; gate panels on configuration instead Trivial
1 Windowed InitFirst/InitSecond + one panel window. Radar only. Medium ? decisive
2 Convert that one panel's Flip to Blt Low
3 Explicit frame cap for windowed mode Low
4 Cameraship (mr_device) ? one extra panel, same path Low
5 -tmfds 1 (one 1280x480 window), then -tmfds 4 (two 640x480 + SwapRightState) Medium

Phase 1 with the radar alone is the make-or-break experiment. If a DDSCL_NORMAL panel with a clipper and a D3D device on an offscreen backbuffer comes up in a window, the rest is repetition.


6. The risk that decides the whole thing

Windowed D3D7 device creation is per-GPU, and this would need four of them in one process.

Precedent in both directions:

  • Against: the mission editor hit exactly this. IDirect3D7::CreateDevice returned DDERR_INVALIDOBJECT for a windowed offscreen backbuffer on Win11, for both the hardware HAL and the Blade software device, and only worked once DDrawCompat was dropped in (CLAUDE.md STEP 8, "3D viewport FIXED via DDrawCompat").
  • For: the game's main display does create a windowed D3D device successfully on the W4100 bench ? console mode windowed is confirmed working.

Nothing has yet proven four windowed D3D7 devices in one process on any hardware. If that fails, the fallback is rendering panels without D3D (GDI/Blt only), which means rewriting the panel render path rather than just its present ? a substantially bigger job.


7. Two decisions to make before starting

Is this test-only, or shippable? A bench-only tool can skip window chrome, dragging, resize handling and pacing polish, and the job shrinks a lot. If pods might ever run windowed, it grows.

Does it need to work without dgVoodoo2? This is the reason the work might be worth more than test convenience. Windowed mode removes exclusive-mode contention entirely ? which is the whole reason dgVoodoo2 is currently mandatory for every multi-display configuration on Windows 10/11 (STEP 10). If windowed panels work natively, that is a genuine route to dropping the dgVoodoo2 prerequisite without breaking the XP pods, since it needs no external DLL on either OS. That is the "borderless windowed" migration already parked in STEP 10's assessment, and this research is the same groundwork.