diff --git a/CLAUDE.md b/CLAUDE.md index 6c0485cb..7835246f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1253,6 +1253,15 @@ consistency check explicitly printed *"No duplicate device assignments"*. The do summary. **Instrument, don't infer.** ## Next steps (proposed) +- [ ] (Researched, NOT started) Make `-window` work for **all** modes — full findings in + **`WINDOWED-MODE.md`** (repo root, 2026-08-05). Headline: `-window` does not fail for MFD/ + cameraship modes, it **silently disables** them, because `use_shgui` is derived from + `Environment.fullScreen` (`MW4Application.cpp:1373`). Beyond that one line the work is a windowed + variant of `CHSH_Device::InitFirst`/`InitSecond` (DDSCL_NORMAL + clipper + offscreen backbuffer, + no SetDisplayMode), one HWND per panel, and 8 `Flip`→`Blt` sites in `WinMain.cpp`. The deciding + risk is whether **four** windowed D3D7 devices can coexist in one process — the editor failed + this on Win11 and needed DDrawCompat (STEP 8), while the game's own windowed main display + succeeds on the W4100. Overlaps heavily with the borderless-windowed assessment below. - [ ] (Decision pending) Borderless-windowed migration — see the staged assessment in STEP 10. Step 2 (one panel) is the cheap, decisive experiment. Note this is the only route that would remove the dgVoodoo2 prerequisite **without** breaking the XP pods, since it needs no external diff --git a/README.md b/README.md index 06b40d82..358c8ae2 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ C:\VWE\firestorm\ ├─ RECOVERY.md Disaster-recovery notes ├─ ADDING-A-MECH.md Workflow: add a new 'Mech chassis └─ ADDING-A-MAP.md Workflow: add a new map/mission +└─ WINDOWED-MODE.md Research: making -window work for MFD/cameraship modes ``` ## The folders that matter (build inputs) diff --git a/WINDOWED-MODE.md b/WINDOWED-MODE.md new file mode 100644 index 00000000..f70a4dc2 --- /dev/null +++ b/WINDOWED-MODE.md @@ -0,0 +1,254 @@ +# 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](Gameleap/code/mw4/Code/MW4Application/MW4Application.cpp#L1373): + +```cpp +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](Gameleap/code/CoreTech/Libraries/GameOS/DXRasterizer.cpp#L1131) never calls +`HSH_EnterFullScreen2()`: + +```cpp +//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](Gameleap/code/CoreTech/Libraries/GameOS/DXRasterizer.cpp#L1407). + +--- + +## 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](Gameleap/code/CoreTech/Libraries/GameOS/DXRasterizer.cpp#L1623), windowed + branch from the `if( FullScreen )` at + [:1718](Gameleap/code/CoreTech/Libraries/GameOS/DXRasterizer.cpp#L1718)). +- **Clipper wrappers exist and are already used**: + [DXRasterizer.cpp:1868-1870](Gameleap/code/CoreTech/Libraries/GameOS/DXRasterizer.cpp#L1868) + ```cpp + 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](Gameleap/code/CoreTech/Libraries/GameOS/DXRasterizer.cpp#L1287) blits the + backbuffer to a screen rect, versus the fullscreen `wFlip` at + [:1357](Gameleap/code/CoreTech/Libraries/GameOS/DXRasterizer.cpp#L1357). `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](Gameleap/code/mw4/Code/MW4Application/MW4Application.cpp#L1373). +`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](Gameleap/code/CoreTech/Libraries/GameOS/render.cpp#L627). Default path (`-tcoop 0`): + +```cpp +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](Gameleap/code/CoreTech/Libraries/GameOS/render.cpp#L832): + +```cpp +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](Gameleap/code/CoreTech/Libraries/GameOS/Windows.cpp#L261). 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](Gameleap/code/CoreTech/Libraries/GameOS/Windows.cpp#L271) +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](Gameleap/code/CoreTech/Libraries/GameOS/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: +```cpp +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](Gameleap/code/CoreTech/Libraries/GameOS/DXRasterizer.cpp#L1252) +`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](Gameleap/code/CoreTech/Libraries/GameOS/render.cpp#L1213) | `g_nNonDualHead`, else `g_nDualHead` | 640x480 | +| `CMFDRight_Device` | [render.cpp:1385](Gameleap/code/CoreTech/Libraries/GameOS/render.cpp#L1385) | `g_nMFD2` | 640x480 | +| `CRadar_Device` | [render.cpp:1456](Gameleap/code/CoreTech/Libraries/GameOS/render.cpp#L1456) | `g_nNonDualHead` | 640x480 | +| `CMFD_Device` | [render.cpp:1719](Gameleap/code/CoreTech/Libraries/GameOS/render.cpp#L1719) | `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](Gameleap/code/CoreTech/Libraries/GameOS/render.cpp#L1774)) +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](Gameleap/code/CoreTech/Libraries/GameOS/render.cpp#L2999). + Chooses between the multi-monitor path, the single-secondary (`mr_device`) path, and none. +- `HSH_DirectDrawRelease2()` ? [render.cpp:3097](Gameleap/code/CoreTech/Libraries/GameOS/render.cpp#L3097). +- `IsMultimonitorAvaliable()` ? [render.cpp:2961](Gameleap/code/CoreTech/Libraries/GameOS/render.cpp#L2961). + Returns **false for cameraship**, so cameraship falls through to `IsSecondaryMonitorAvaliable()` + ([render.cpp:2989](Gameleap/code/CoreTech/Libraries/GameOS/render.cpp#L2989)) 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.