# Mechanism review — `e56aa511` (#376 + #388): native fullscreen mode switching + state-aware display apply **Reviewer lens:** MECHANISM (does the machine do what it claims, on the platform it runs on). **Scope:** `src/AcDream.App/Settings/DisplayModeSwitching.cs` (new), `src/AcDream.App/Settings/RuntimeSettingsTargets.cs` (`SilkRuntimeDisplayWindowTarget.Apply` rework), and their live surroundings. **Date:** 2026-08-13. **Report only — no files were edited.** Primary evidence for the Silk/GLFW claims below is IL read directly out of the pinned packages (`Silk.NET.GLFW 2.23.0`, `Silk.NET.Windowing.Glfw 2.23.0` under `%USERPROFILE%\.nuget\packages`), not recollection. Where a claim rests on IL, the decoded bytes are quoted. --- ## Verdict summary | # | Question | Verdict | |---|---|---| | 1 | GLFW API correctness (`Glfw.GetApi()`, main thread) | **PASS** on both halves — but the error-handling contract built on top of it is broken (M1) | | 2 | State-machine holes | **FAIL** — M2 (no idempotence → video-mode storm), M5 (persisted state diverges, one path silent). The `_windowedPosition` clobber the question asked about is correctly guarded. | | 3 | Silk-vs-native `WindowState` desync | **PASS** — no desync exists; the getter is native-backed and no writer remains. Events fire on both paths. One latent trap (W3). | | 4 | Exit path / remembered position | **PARTIAL FAIL** — M3: two switcher instances, so the startup-fullscreen → live-exit path never restores the remembered placement. True off-screen stranding is unlikely (W5). | | 5 | Validation seam | **PARTIAL FAIL** — M4: catalog and switcher enumerate *different monitors*. The catalog-uninstalled (fixture/headless) case refuses cleanly rather than crashing — that half **PASSES**. | | 6 | #377 regression surface (startup ordering) | **PASS on ordering** — the composition order is safe and the quiescence gate is reentrant. But the commit does not close #377's mechanism; it makes it reachable more often (W1). | --- ## MUST-FIX findings, ranked ### M1 — `catch (GlfwException)` is dead code on Windows: a failed native switch reports SUCCESS and arms a deferred crash `DisplayModeSwitching.cs:103-109` (and `:135-139`) wrap the native calls in `catch (GlfwException)`, and the interface doc at `:24-25` promises "False (with a reason) instead of throwing on any failure". The commit message repeats it: "every failure is a no-throw (bool, reason) result." **On Windows that is false.** Silk's process-global GLFW error callback is installed by `GlfwProvider::GetGlfw` (`… callvirt Glfw::Init … call Glfw::get_ErrorCallback; callvirt Glfw::SetErrorCallback`) and its body is `Glfw.<>c::<.cctor>b__143_0`. Raw IL (51 bytes): ``` 00: 72 3F160070 ldstr "{0}: {1}" 05: 03 8C 11000002 ldarg.3 ; box (error code) 0B: 02 ldarg.2 (description) 0C: 28 4F00000A call String::Format 11: 73 9E000006 newobj GlfwException::.ctor 16: 25 03 6F A2..06 dup; ldarg.3; set_ErrorCode 1D: 0A stloc.0 (ex) 1E: 7E 40010004 ldsfld Glfw::_isWindows 23: 2C 02 brfalse.s -> 0x27 25: 06 7A ldloc.0; THROW <- non-Windows only 27: 7E 41010004 ldsfld Glfw::_exceptions 2C: 06 6F 5600000A ldloc.0; callvirt List::Add <- WINDOWS: DEFERRED 32: 2A ret ``` i.e. `if (!_isWindows) throw ex; else Glfw._exceptions.Add(ex);`. The drain is `Glfw.ThrowExceptions()`, and the only callers in the entire Silk closure are seven `GlfwWindow` methods: ``` GlfwWindow::Create, CoreInitialize, CoreReset, RegisterCallbacks, GlfwWindow::SetWindowIcon, set_Monitor, GetProcAddress ``` (verified by scanning `Silk.NET.Windowing.Glfw`, `Silk.NET.Input.Glfw`, `Silk.NET.Windowing.Common`, `Silk.NET.Input.Common`, `Silk.NET.Core` — the last four contain **zero** call sites). None of them is per-frame, and none of them is on any path this commit takes. `ThrowExceptions` also never clears the list — its 66-byte body contains `get_Count`, `get_Item(0)`, `AggregateException`, `throw`, and no `Clear` — so a queued error is re-raised at *every* later drain. **Consequences, in order of severity:** 1. A failing `glfw.SetWindowMonitor` (`DisplayModeSwitching.cs:98`) — e.g. "Failed to set video mode: Graphics mode not supported", the exact #388 error — returns normally. `TryEnterFullscreen` returns `true` and prints `display: fullscreen mode switch WxH@R` (`:99-100`) for a switch that did not happen. Every downstream consumer, including the §D5/§D6 gate script and the "live-verified" evidence line in the commit message, reads that log as proof of success. **The gate's own oracle can lie.** 2. The queued `GlfwException` detonates at the next `ThrowExceptions()`, which in this client is `GlfwWindow::CoreReset` — window close. A failed mode switch mid-session therefore surfaces as an unexplained crash *at shutdown*, which is exactly the path the project needs graceful for ACE session cleanup. 3. This also retro-explains #388's "first surfaced as a caught `settings: display save failed: PlatformError…`, then a second fired as an UNHANDLED exception": with a never-cleared static list, **the same exception instance is thrown twice** from two different drains. That pattern is a signature of this mechanism, not of two independent errors. **Fix direction (no workaround):** do not rely on exceptions at all. Verify the *post-condition* natively inside `TryEnterFullscreen` / `TryLeaveFullscreen`: after `SetWindowMonitor`, re-read `glfw.GetWindowMonitor(handle)` and `glfw.GetVideoMode(monitor)` and require they match the request; return `false` with the observed state otherwise. That is platform-independent, needs no knowledge of Silk's deferral, and turns the log line into a measurement instead of an assumption. Draining `Glfw.ThrowExceptions()` is a *second-choice* option only, and must account for the never-cleared static list (a stale error from anywhere in the process would be misattributed to the switch). --- ### M2 — No idempotence guard on the fullscreen branch: every Display-backed Config row re-issues a REAL video-mode change, once per slider drag tick `RuntimeSettingsTargets.cs:116-130` — the fullscreen branch calls `TryEnterFullscreen` **unconditionally**. The windowed branch immediately below keeps its change guard (`:147`, `if (haveResolution && (_window.Size.X != width || _window.Size.Y != height))`), and the code this commit **deleted** had one too (`if (_window.WindowState != desired) _window.WindowState = desired;`). The idempotence that existed before this commit was removed on the fullscreen side only. Why that matters here specifically: - Every Display-backed Config row funnels through the same apply: `ConfigOptionsPageController.cs:587, 643, 651, 659, 671, 678, 685, 693, 738, 746, 754` → `bindings.SaveDisplay(...)` → `RuntimeSettingsController.SaveDisplay` (`:386-399`) → `ApplyDisplayWindowState` → `Apply`. - `FloatOptionRow.SetCurrentValue` applies **live, on every drag tick, not on release** — stated verbatim in `OptionPageModel.cs:176-181` and relied on by the Chat tab's fade-while-dragging behaviour. So, while fullscreen, dragging Field of View / Screen Brightness / Degrade Distance / Graphics Performance issues one `glfwSetWindowMonitor` (→ `ChangeDisplaySettingsEx`) **per mouse-move sample**, each one taking the monitor through a real mode set + re-sync, each one re-entrantly firing `FramebufferResize` + `Move` (see W1) and arming a swapchain recreate. Every non-slider Display row (texture detail, filtering, degrades, quality preset, VSync) does the same once per click. Secondary cost on the same path: `Glfw.GetApi()` is called fresh on every `IsFullscreen` read (`:60`) and every `Try*` entry (`:76`, `:124`), and `Glfw::GetApi` is `newobj GlfwLibraryNameContainer → GetLibraryNames → CreateDefaultContext → newobj Glfw` — a new `DefaultNativeContext` (and a `LoadLibrary`/`dlopen` refcount) per call, never disposed. That is ~2–3 per apply, i.e. per drag tick under this bug. **Fix:** early-return when already fullscreen at the requested mode. That needs a current-mode read on the seam (e.g. `IDisplayModeSwitcher.TryGetCurrentMode(out int w, out int h)`), which the fake can implement trivially — and a test asserting a same-state re-apply issues zero `enter:` calls (`FakeModeSwitcher` already records them, so this gap was one assertion away from being caught). --- ### M3 — Two `SilkRuntimeDisplayWindowTarget` instances: the remembered windowed placement is lost exactly on the startup-fullscreen → live-exit path There are two production construction sites, each building its **own** `GlfwDisplayModeSwitcher` with its own `_windowedPosition` field (`DisplayModeSwitching.cs:47`, initial value `(60, 60)`): - `src/AcDream.App/Rendering/GameWindow.cs:1296` — the **startup** target (`RuntimeSettingsStartupTargets`), which runs `ApplyDisplay` during composition phase 3. - `src/AcDream.App/Composition/SessionPlayerComposition.cs:334` — the **live** target (`RuntimeSettingsTargets`), which runs `ApplyDisplayWindowState` on every Config save. Sequence that breaks: `settings.json` has `fullscreen: true` → **instance A** captures the real windowed position at `:90-96` and enters fullscreen → user later unticks Full Screen in the Config tab → **instance B** runs `TryLeaveFullscreen`, and its `_windowedPosition` has never been written, so the window is placed at the `(60, 60)` literal. On a single monitor that is merely wrong-but-harmless; on multi-monitor it teleports the client to the **primary** monitor's top-left regardless of where it was. It also makes the commit message's claim ("the windowed placement is remembered for the exit path") and gate-script step §D5 ("positioned where it was before entering fullscreen", `docs/research/2026-08-13-display-block-test-script.md`) false for that ordering — and *only* for that ordering, so a tester who toggles fullscreen on and off within one session will not reproduce it. **The gate must explicitly exercise launch-fullscreen → untick.** **Fix:** one process-wide switcher (or hoist the remembered placement into a single owner both targets borrow). --- ### M4 — Catalog and switcher enumerate DIFFERENT monitors, so "an offered mode is supported by construction" does not hold The whole crash-class argument rests on the catalog and the switcher agreeing about which display's mode list is authoritative. They do not: - `DisplayModeCatalog.InstallFromWindow` (`Rendering/DisplayModeCatalog.cs:53`) enumerates **`window.Monitor`**. Silk's `GlfwWindow::get_Monitor` (IL: `GetWindowMonitor` → else walk `GlfwMonitorEnumerable` for the monitor whose bounds contain the window centre → else `GetPrimaryMonitor`) means at `OnLoad` this is *the monitor the window happens to be on*. - `GlfwDisplayModeSwitcher.TryEnterFullscreen` (`:77`) enumerates and switches on **`GetPrimaryMonitor()`**. Two failure shapes on a heterogeneous multi-monitor desktop: 1. A mode offered from the secondary's list is absent from the primary's → `TryFindRefreshRate` fails (`:84-88`) → fullscreen refused. The user ticks the box, nothing happens, `settings.json` says `fullscreen: true` (see M5). 2. Even on success, entering fullscreen moves the client to the *primary* monitor. That may be retail-faithful (`Device::ForceDisplayResolution` drove the primary display device, per the class doc), but it is a behavioural deviation from "fullscreen the window where it is" and is not in the divergence register. **Fix:** pick one monitor authority and use it at both ends (catalog + switcher). If "always primary" is kept as the retail-faithful choice, the catalog must enumerate the primary too, and a `docs/architecture/retail-divergence-register.md` row must record the window-jumps-to-primary behaviour. --- ### M5 — Every fullscreen refusal/failure leaves `settings.json` inconsistent with reality; one path is entirely silent `RuntimeSettingsController.SaveDisplay` **persists first, applies second** (`:390` then `:392`). `Apply`'s three fullscreen exits never revert: | Path | Line | Logged? | Persisted state after | |---|---|---|---| | Resolution unparseable | `:118-119` | **no log at all** | `fullscreen: true`, window windowed | | Mode not offered | `:120-125` | yes (`refused — not an offered mode`) | `fullscreen: true`, window windowed | | Switch failed | `:126-128` | yes (`failed (…) — staying windowed`) | `fullscreen: true`, window windowed | The unparseable case is the one the review brief asked about, and it is the worst of the three: it produces **no `display:` line whatsoever**, so the gate script's "any refused/failed switch logs a `display: … failed/refused` line" acceptance criterion (§D6 step 1) cannot be met and the tester has nothing to read. The Config checkbox then reads `true` from storage forever and every subsequent launch silently re-refuses. A second accuracy defect lives at `:128`: when the *re-entry* attempt fails while the window is **already fullscreen**, the message says "staying windowed", which is factually wrong. **Fix:** log the unparseable case; make the failure message report the actual resulting state; and decide deliberately whether a refusal should revert the persisted `Fullscreen` (retail has a confirmation flow — `SetConfirmChange` — already noted as unported in `ConfigOptionsPageController.cs:623`). --- ## Per-question findings ### Q1 — GLFW API correctness **`Glfw.GetApi()` is the right instance. PASS.** `Glfw::GetApi` IL is `newobj GlfwLibraryNameContainer → SearchPathContainer::GetLibraryNames → Glfw::CreateDefaultContext → newobj Glfw`: a *new managed wrapper* over the *same native library*. GLFW's state (init flag, window list, monitor list, error callback) lives in the native module, which the OS loader returns as one instance per process — so a second wrapper drives the same GLFW. The `GraphicalWindowBackendSelection.cs:135-138` comment ("A separate `Glfw.GetApi()` instance would receive the hint but would not own the window backend's process-global GLFW state") is about **init-hint ordering**, not about later calls: `GlfwProvider.UninitializedGLFW` is itself literally `new Lazy(Glfw.GetApi)` (`GlfwProvider::.cctor` → `<>O::<0>__GetApi`), so it is the same kind of object. Using `GlfwProvider.UninitializedGLFW.Value` there matters because Silk must later `Init()` *that* instance; it does not imply later `GetApi()` calls are wrong. `GlfwCursorCache.TryCreate` (`Rendering/GlfwCursorCache.cs:47`) already established this in production (#348). **Caveat (W4):** `GlfwCursorCache` caches its `Glfw` for the object's lifetime; `GlfwDisplayModeSwitcher` calls `Glfw.GetApi()` on **every** property read and method entry (`:60`, `:76`, `:124`). Each call allocates a `Glfw` + `DefaultNativeContext` and takes a native-library refcount that is never released (`NativeApiContainer.Dispose` is never called). Cache one instance in the field, as the #348 precedent does. **Thread: PASS.** `Program.cs` constructs `GameWindow` and calls `window.Run()` on the process main thread (`Program.cs:142`); `GameWindow.Run` calls `Window.Create` (`GameWindow.cs:761`) and `_window.Run()` (`:785`) on that same thread; Silk's loop and all callbacks (Load/Update/Render/FramebufferResize) run on the calling thread. Both the startup `ApplyDisplay` (composition, inside `OnLoad`) and the live `ApplyDisplayWindowState` (Config-tab click handling in the update phase) therefore execute on the GLFW main thread. `glfwSetWindowMonitor`, `glfwGetVideoModes` and `glfwGetWindowPos` are all main-thread-only, and all three are satisfied. ### Q2 — State-machine hole enumeration `haveResolution` = `TryParseResolution` succeeded (`:113-114`); `offered` = `_isOfferedMode("{w}x{h}")`; `fs` = `_modeSwitcher.IsFullscreen`. | # | target | fs | haveRes | offered | switcher result | Outcome | Assessment | |---|---|---|---|---|---|---|---| | 1 | FS | no | yes | yes | ok | enters fullscreen | correct | | 2 | FS | no | yes | yes | fail | logs, stays windowed | correct **but see M1** (on Windows "fail" is not observable) | | 3 | FS | no | yes | no | — | logs refusal, stays windowed | correct; **M5** (settings now lie) | | 4 | FS | no | **no** | — | — | **silent return** | **M5** — no log, settings lie | | 5 | FS | **yes** | yes | yes | ok | redundant real mode set | **M2** — storm | | 6 | FS | **yes** | yes | yes | fail | logs "staying windowed" **while fullscreen** | **M5** (wrong message) | | 7 | FS | **yes** | yes | no | — | logs refusal, remains fullscreen at old mode | acceptable; message is accurate | | 8 | FS | **yes** | no | — | — | silent return, remains fullscreen | benign, but silent | | 9 | win | yes | yes | — | ok | native exit at picked size + `_windowedPosition` | correct; **M3** (wrong position across instances), **W5** (size==desktop) | | 10 | win | yes | yes | — | fail | logs, remains fullscreen | correct | | 11 | win | yes | **no** | — | ok | exit at **current fullscreen size** (`:136-140` reads `_window.Size`, which under native fullscreen is the *mode* size) | window client = desktop size → frame overflows desktop (**W5**) | | 12 | win | no | yes | — | — | size write iff changed (`:147`) | correct, guarded | | 13 | win | no | no | — | — | no-op | correct | **On the specific idempotency sub-question:** the guard at `:90-96` is **correct**. `_windowedPosition` is only captured when `glfw.GetWindowMonitor(handle) is null`, so re-entering fullscreen while already fullscreen cannot clobber it with `(0,0)`. GLFW itself tolerates `glfwSetWindowMonitor` with the same monitor (it re-applies the mode); the problem is not correctness of a single repeat but the *rate* of repeats (M2). ### Q3 — Silk-vs-native desync **No desync exists. PASS.** `GlfwWindow::get_CoreWindowState` is native-backed: ``` _glfw.GetWindowAttrib(_glfwWindow, Iconified) -> Minimized _glfw.GetWindowAttrib(_glfwWindow, Maximized) -> Maximized _glfw.GetWindowMonitor(_glfwWindow) != null -> Fullscreen else -> Normal ``` So after a native `SetWindowMonitor`, `_window.WindowState` correctly reports `Fullscreen`. Silk's `_extendedState`-style caching only applies before `IsInitialized`. **No readers to misbehave.** `grep` over `src/` and `tests/` finds **zero** `WindowState =` writers left after this commit, and the only consumer is `DisplayFramePacingController.OnWindowStateChanged(WindowState _)` (`Rendering/DisplayFramePacingController.cs:132`), which discards the value and calls `RefreshActiveMonitor()`. UI/picking read `IWindow.Size` (`Composition/InteractionUiRuntimeSources.cs:559`) and the render path reads `IWindow.FramebufferSize` (`Rendering/RetailPViewPassExecutor.cs:27`, `Rendering/Gpu/Vk/VulkanGraphicsContext.cs:261, 406`) — both are live native reads (`get_FramebufferSize` = `GetFramebufferSize`), both correct under native fullscreen. There is no screenshot path that reads `WindowState`. **Events fire on both paths, and directly.** Silk raises them straight from the GLFW callback with **no queueing** — `GlfwWindow::b__94_2` is `ldsfld FramebufferResize; newobj Vector2D; callvirt Invoke`, and `b__94_0` (pos) is `UpdatePosition; ldsfld Move; Invoke`. Since `glfwSetWindowMonitor` calls `SetWindowPos`, the WM_SIZE / WM_MOVE handlers run synchronously inside it, so `FramebufferResize` and `Move` are delivered **re-entrantly**, on both enter and exit. (Confirmed safe: see W1/W8.) **`StateChanged` does NOT fire** for a monitor change — `b__94_6`/`b__94_7` are the iconify/maximize callbacks and are the only sites that call `UpdateState` + raise `StateChanged`. `pacing.OnWindowStateChanged` therefore never runs for our switch; `pacing.OnWindowMoved` covers it instead (`SilkWindowCallbackBinding.cs:152`). No action needed — just do not build anything new on `StateChanged`. **W3 (latent trap):** `GlfwWindow::set_CoreWindowState` is the only writer of Silk's `_nonFullscreenPosition` / `_nonFullscreenSize`. Bypassing it means those stay stale forever. If any future code sets `WindowState = Normal` while natively fullscreen, Silk calls `glfwRestoreWindow`, which for a fullscreen window restores the *video mode on the same monitor* — it does **not** leave fullscreen. Worth a one-line comment in `DisplayModeSwitching.cs` so the next author does not reach for the Silk setter as a "simpler" exit. ### Q4 — The exit path - **`(60,60)` default:** reachable in production, via M3 (not via the "never captured" path the question hypothesised — startup entry *does* capture, just on the other instance). - **Off-screen stranding proper:** unlikely. `glfwSetWindowMonitor(NULL, …)` restores the monitor's original video mode, so a position captured pre-fullscreen is still valid for the restored desktop. `(60,60)` is always on-screen on the primary. - **W5 (real, milder):** the exit uses the *picked resolution* as the **client** size. The catalog's Defaults value **is the desktop mode** (`DisplayModeCatalog.cs:39-43`), so the common case produces a windowed client exactly the size of the desktop — its frame and title bar then overflow the work area, and row 11 of the Q2 table (unparseable resolution) produces the same via `_window.Size`. Recommend clamping the restored placement/size to the monitor work area (`glfwGetMonitorWorkarea`) on the exit path. - **#390 interaction — confirmed, the UI clamp does NOT cover the OS window.** `RetailWindowLayoutPersistence.ClampAllToScreen` (`src/AcDream.App/UI/RetailWindowLayoutPersistence.cs:102-129`) clamps each attached retail-UI window into `ValidScreenSize()` — the client area — via `handle.MoveTo`. It never touches the native window. So #390 keeps the *panels* reachable inside whatever client rect exists; it cannot rescue a client rect that itself overflows the desktop. That is the right layering; it just means W5 needs its own fix rather than leaning on #390. ### Q5 — Validation seam - **Are valid sub-desktop fullscreen modes wrongly refused?** Not by the curation itself. `DisplayModeCatalog.Curate` (`:94-144`) keeps any monitor mode that fits the desktop, is ≥1280 wide and is 16:9/16:10/21:9/32:9 within ±2.5% — plus the desktop mode unconditionally. Those are all genuine fullscreen targets, and `TryFindRefreshRate` re-checks against the live GLFW mode list, so nothing supported-and-offered is refused **on a single-monitor machine**. On multi-monitor, **M4** breaks it. - **Catalog-uninstalled (fixture/headless):** refuses cleanly, no crash. `DisplayModeCatalog.Resolutions` is `null`, and `RuntimeSettingsTargets.cs:83` is `spec => DisplayModeCatalog.Resolutions?.Contains(spec) == true` — a null-conditional whose `null == true` is `false`. Fullscreen entry is refused with a log; nothing dereferences null. **PASS.** - **Nit (not blocking):** `Contains` is `Enumerable.Contains` over an `IReadOnlyList` (ordinal, O(n≈10)) — fine. The spec string is rebuilt from the parsed ints (`$"{width}x{height}"`), so `"01920x1080"`-style input normalises before lookup. `TryParseResolution` splits on lowercase `'x'` only (`:165`) — pre-existing. - **W7:** the catalog is captured once at `OnLoad` (`GameWindow.cs:1246`) and never refreshed; monitor hot-plug or a topology change leaves it stale, and (with M4) increasingly wrong. ### Q6 — #377 regression surface at startup **The ordering is safe. PASS.** Verified chain inside `OnLoad`: 1. `AcquirePlatform()` → `VulkanGraphicsContext.Acquire` — swapchain created at the 1280x720 startup size (`GameWindow.cs:1219-1238`, `:752-757`). 2. `DisplayModeCatalog.InstallFromWindow(_window!)` — `GameWindow.cs:1246`, **before** the pipeline, so the startup fullscreen entry is validated against a real catalog. 3. Composition **phase 1** binds the framebuffer targets: `FramebufferResize.BindViewport` (`HostInputCameraComposition.cs:204`) and `BindCamera` (`:317`). 4. Composition **phase 3** runs `Settings.ApplyStartup` (`SettingsDevToolsComposition.cs:53`) → `ApplyDisplay` → native switch. So the re-entrant `FramebufferResize` fired from inside `glfwSetWindowMonitor` finds both targets bound — `FramebufferResizeController.Resize` (`:109-129`) is null-conditional throughout, and the Vulkan viewport target only **arms** a frame-boundary swapchain recreate (`VulkanHostInputCameraCompositionFactory.cs:38, 122`) rather than doing GPU work re-entrantly. Had step 4 preceded step 3, the resize would have been silently dropped ("late binding never replays an earlier resize transition", `FramebufferResizeController.cs:123-124`) and the client would have rendered a 1280x720-aspect image stretched over the fullscreen surface — the #387 symptom, resurrected at startup. **This ordering is load-bearing and undocumented; it deserves a comment at `GameWindow.cs:1246` or a boundary test.** Re-entrancy itself cannot deadlock: `HostQuiescenceGate.Invoke` uses `lock` (`Rendering/HostQuiescenceGate.cs:39, 49`), and `Monitor` is reentrant on the same thread — the class doc already calls this out. **W1 (the residual, and the honest answer to "could this resurrect the old access violation"): yes, the mechanism is still reachable — more often than before.** The re-entrant `Move` event runs `pacing.OnWindowMoved` → `RefreshActiveMonitor()` → `SilkDisplayFramePacingSurface.TryGetActiveMonitorRefreshHz` → `_window.Monitor?.VideoMode.RefreshRate` (`DisplayFramePacingController.cs:35, 121-128`) **from inside `glfwSetWindowMonitor`, mid-mode-transition**. `GlfwWindow::get_Monitor` resolves through `GetWindowMonitor` → `GetMonitors` → `new GlfwMonitor` and `GlfwMonitor.VideoMode` → `GetVideoMode` — which is precisely #377's crash site (`0xC0000005 in Glfw.GetVideoMode`, "reading a monitor mid-mode-transition", `docs/ISSUES.md` §#377). Its `catch (GlfwException)` guard (`:42-49`) does not help: per **M1** GLFW errors do not throw on Windows, and an access violation is not a managed exception at all. This is not a *new* class — Silk's own `WindowState = Fullscreen` setter also called `SetWindowMonitor` — but before this commit the fullscreen write was guarded by `if (_window.WindowState != desired)` and therefore rare. With **M2**, it now runs on every Display-tab save and every slider drag tick. Recommendation: suppress the pacing monitor refresh for the duration of a deliberate mode switch (a re-entrancy latch around `SetWindowMonitor`, then one explicit `RefreshActiveMonitor()` after it returns) — that is a correctness fix for a re-entrancy hazard, not a symptom-hiding guard. --- ## Test-coverage gaps (all five new tests pass on their own facts) `tests/AcDream.App.Tests/Settings/RuntimeSettingsControllerTests.cs:157-288` covers: validated switch / never-size-write, unoffered refusal, failed-switch usability, native exit, plain windowed write. Missing, each one assertion away with the existing `FakeModeSwitcher`: 1. `Fullscreen=true` while `IsFullscreen=true` **at the same mode** → expect `Assert.Empty(switcher.Calls)` (**M2**). 2. `Fullscreen=true` with an unparseable `Resolution` → currently a silent `return`; assert whatever the decided behaviour is (**M5**). 3. Enter-then-exit round trip on **one** switcher instance, asserting the position is round-tripped (would not catch **M3**, which is a composition fact — that one needs a construction-site assertion or a single-owner refactor). 4. `Fullscreen=false` while `IsFullscreen=true` with an unparseable resolution → asserts the row-11 fallback reads the surface size. ## Gate-script gaps `docs/research/2026-08-13-display-block-test-script.md` §D4–§D6 is otherwise well-shaped (the black-screen-risk flagging is right). Add: - §D5 must include **launch fullscreen → untick Full Screen** as a distinct step from **tick → untick in one session**; only the former exposes **M3**. - §D6 must include **drag a slider (Field of View) while fullscreen** — the **M2** storm. - §D6's "any refused/failed switch logs a `display: … failed/refused` line" is not currently guaranteed (**M1** makes failure invisible, **M5** case 4 logs nothing). Treat that acceptance line as unmet until M1/M5 land. ## Register / bookkeeping - No `docs/architecture/retail-divergence-register.md` row accompanies this commit. At minimum the **always-switch-the-primary-monitor** behaviour (**M4**) is a deviation ("retail's `ForceDisplayResolution` drove the primary display device" is asserted in the class doc but the *window* also relocates), and the CLAUDE.md rule is that a deviation ships with its row in the same commit. - On Wayland (`GraphicalDisplayProtocol.Wayland`), a client cannot change the display mode; `glfwSetWindowMonitor` will report success without switching. Slice L is parked, so this is documentation-only today — one line in the `GlfwDisplayModeSwitcher` class doc.