fix #376/#388 review round: post-condition truth, idempotence, one

position memory, unified monitor, maximized restore; AD-92

Dual-lens Opus review of e56aa511 (reports committed under
docs/research/). The consolidated corrections:

- Mechanism M1 (load-bearing): on Windows, Silk's GLFW error callback
  QUEUES exceptions on a static list instead of throwing - they detonate
  later at window close, which is exactly #388's original two-stage
  crash shape. catch(GlfwException) was dead code here and a failed
  SetWindowMonitor "succeeded". Success is now judged by the NATIVE
  POST-CONDITION (GetWindowMonitor after the call) on both enter and
  exit; the catches remain only for the throwing platforms.
- M2 (both lenses): same-mode fullscreen re-apply is a no-op BEFORE any
  native work (new IDisplayModeSwitcher.CurrentFullscreenMode). Every
  Display-backed Config row applies per change - sliders per DRAG TICK -
  so without this every tick while fullscreen re-issued a real
  display-mode change.
- M3/M5 (both): the remembered windowed placement is process state (two
  target instances exist - startup and live-save); a fullscreen boot now
  exits through either instance to the real placement, not the (60,60)
  literal.
- M4 (both): the switcher resolves the WINDOW'S monitor (attached
  monitor when fullscreen, else IWindow.Monitor's index into the GLFW
  array - the same monitor DisplayModeCatalog enumerated), primary only
  as a last resort; the offered-list/switch-target mismatch is gone.
- Blast M2b: the offered-mode validator falls back to the SAME static
  ladder the dropdown falls back to - Full Screen is no longer a
  permanent silent no-op on catalog-less hosts (the switcher's own
  monitor-mode-list check remains the hard guard).
- Blast M3: a windowed pick on a MAXIMIZED window restores it first
  (Size writes are silently ignored while maximized; the deleted
  WindowState=Normal write used to do this incidentally). New
  IWindowedSizeSurface.IsMaximized/Restore.
- Mechanism M5: no silent bail-outs - the unparseable-resolution
  fullscreen path logs, and the failure line no longer claims "staying
  windowed" when the state is unchanged (#392 noted inline).
- Q1 nit: one cached Glfw wrapper (per-call GetApi allocated + took a
  native refcount); IsFullscreen/CurrentFullscreenMode guarded.
- AD-92: highest-refresh-for-WxH + refuse-and-log versus retail's
  pass-through-and-error ForceDisplayResolution.

Known-open tail, filed not hidden: #392 (persisted-flag divergence on a
refused enter - needs an apply-result seam); the mechanism report's
pacing-refresh WATCH rides the same seam.

Tests: +3 (same-mode no-op, unparseable-while-fullscreen refusal,
maximized restore-before-write). App suite 4,975/3 skips.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-13 18:10:07 +02:00
parent 229242e1fe
commit ec2a7b0cce
6 changed files with 1257 additions and 28 deletions

View file

@ -20,6 +20,12 @@ internal interface IDisplayModeSwitcher
/// monitor attached).</summary>
bool IsFullscreen { get; }
/// <summary>The active fullscreen mode's WxH, or null when windowed.
/// The apply's idempotence guard (review M2: every Display-backed Config
/// row applies per change — sliders per DRAG TICK — so a same-mode
/// re-apply must be a no-op before any native call).</summary>
(int Width, int Height)? CurrentFullscreenMode { get; }
/// <summary>Switches the window to exclusive fullscreen at the given
/// mode. False (with a reason) instead of throwing on any failure —
/// a settings apply must never crash the client (#388).</summary>
@ -32,19 +38,38 @@ internal interface IDisplayModeSwitcher
/// <summary>
/// The native GLFW implementation. Uses the SAME handle path the #348
/// cursor cache proved (<c>IWindow.Native.Glfw</c>) and the primary
/// monitor — retail's <c>ForceDisplayResolution</c> likewise drove the
/// primary display device. The refresh rate for a mode is the highest the
/// monitor reports for that WxH (retail passed the device's mode as-is).
/// Callers validate the requested WxH against <c>DisplayModeCatalog</c>
/// BEFORE calling — an offered mode is supported by construction, which is
/// what makes the old "Graphics mode not supported" crash class
/// unreachable from the dropdown.
/// cursor cache proved (<c>IWindow.Native.Glfw</c>) and the WINDOW'S OWN
/// monitor (<c>IWindow.Monitor</c>'s index into the GLFW monitor array —
/// the same monitor <c>DisplayModeCatalog</c> enumerated, so the offered
/// list and the switch target agree; review M4 caught the earlier
/// primary-monitor mismatch). The refresh rate for a mode is the highest
/// the monitor reports for that WxH. Callers validate the requested WxH
/// against <c>DisplayModeCatalog</c> BEFORE calling.
///
/// <para><b>Error model (review M1 — load-bearing):</b> on Windows, Silk's
/// GLFW error callback does NOT throw — it QUEUES the exception on a static
/// list that detonates much later (window close), which is exactly the
/// two-stage crash #388 originally recorded. `catch (GlfwException)` is
/// therefore dead code on this platform; success is judged by the NATIVE
/// POST-CONDITION (does the window have / not have a monitor afterwards),
/// never by the absence of an exception. The catches stay only for the
/// non-Windows platforms where Silk does throw.</para>
/// </summary>
internal sealed unsafe class GlfwDisplayModeSwitcher : IDisplayModeSwitcher
{
private readonly IWindow _window;
private (int X, int Y) _windowedPosition = (60, 60);
// Q1 nit: one cached wrapper (a Glfw.GetApi() per call allocates and
// takes a native library refcount) — the same pattern GlfwCursorCache
// uses.
private static readonly Lazy<Glfw> Api = new(Glfw.GetApi);
// Review M3/M5: the remembered windowed placement is PROCESS state, not
// per-instance state — the startup target and the live-save target are
// two instances over the one window, and a fullscreen boot must exit
// through whichever instance the untick reaches. Static, single-window
// process (the same write-once shape DisplayModeCatalog uses).
private static (int X, int Y) _windowedPosition = (60, 60);
public GlfwDisplayModeSwitcher(IWindow window)
{
@ -55,9 +80,40 @@ internal sealed unsafe class GlfwDisplayModeSwitcher : IDisplayModeSwitcher
{
get
{
WindowHandle* handle = Handle();
if (handle is null) return false;
return Glfw.GetApi().GetWindowMonitor(handle) is not null;
try
{
WindowHandle* handle = Handle();
if (handle is null) return false;
return Api.Value.GetWindowMonitor(handle) is not null;
}
catch (GlfwException)
{
// Never throws on Windows (queued instead) — this guard is
// for the throwing platforms; a monitor query must never
// take the caller down (#377 family).
return false;
}
}
}
public (int Width, int Height)? CurrentFullscreenMode
{
get
{
try
{
WindowHandle* handle = Handle();
if (handle is null) return null;
Glfw glfw = Api.Value;
Silk.NET.GLFW.Monitor* monitor = glfw.GetWindowMonitor(handle);
if (monitor is null) return null;
Silk.NET.GLFW.VideoMode* mode = glfw.GetVideoMode(monitor);
return mode is null ? null : (mode->Width, mode->Height);
}
catch (GlfwException)
{
return null;
}
}
}
@ -73,11 +129,11 @@ internal sealed unsafe class GlfwDisplayModeSwitcher : IDisplayModeSwitcher
try
{
Glfw glfw = Glfw.GetApi();
Silk.NET.GLFW.Monitor* monitor = glfw.GetPrimaryMonitor();
Glfw glfw = Api.Value;
Silk.NET.GLFW.Monitor* monitor = ResolveWindowMonitor(glfw, handle);
if (monitor is null)
{
error = "no primary monitor";
error = "no monitor";
return false;
}
@ -96,14 +152,22 @@ internal sealed unsafe class GlfwDisplayModeSwitcher : IDisplayModeSwitcher
}
glfw.SetWindowMonitor(handle, monitor, 0, 0, width, height, refresh);
// Review M1: judge by the native post-condition, not by the
// absence of an exception (Windows queues GLFW errors).
if (glfw.GetWindowMonitor(handle) is null)
{
error = "the mode switch did not take (GLFW reports no monitor attached)";
return false;
}
Console.WriteLine(
$"display: fullscreen mode switch {width}x{height}@{refresh}");
return true;
}
catch (GlfwException ex)
{
// #388: a failed switch is a logged failure the caller reverts
// from, never a crash. Validation makes this path exceptional.
// Non-Windows platforms throw; Windows never reaches here.
error = ex.Message;
return false;
}
@ -121,13 +185,20 @@ internal sealed unsafe class GlfwDisplayModeSwitcher : IDisplayModeSwitcher
try
{
Glfw glfw = Glfw.GetApi();
Glfw glfw = Api.Value;
if (glfw.GetWindowMonitor(handle) is null)
return true; // already windowed
glfw.SetWindowMonitor(
handle, null,
_windowedPosition.X, _windowedPosition.Y,
width, height, 0);
if (glfw.GetWindowMonitor(handle) is not null)
{
error = "the window is still fullscreen (GLFW reports a monitor attached)";
return false;
}
Console.WriteLine(
$"display: left fullscreen to windowed {width}x{height}");
return true;
@ -139,8 +210,30 @@ internal sealed unsafe class GlfwDisplayModeSwitcher : IDisplayModeSwitcher
}
}
/// <summary>The monitor the window belongs to: the attached monitor when
/// fullscreen, else the window's own monitor per Silk's assignment
/// (index into the GLFW monitor array — the SAME monitor
/// <c>DisplayModeCatalog.InstallFromWindow</c> enumerated), falling back
/// to the primary.</summary>
private Silk.NET.GLFW.Monitor* ResolveWindowMonitor(Glfw glfw, WindowHandle* handle)
{
Silk.NET.GLFW.Monitor* attached = glfw.GetWindowMonitor(handle);
if (attached is not null) return attached;
int? index = _window.Monitor?.Index;
if (index is int i && i >= 0)
{
Silk.NET.GLFW.Monitor** monitors = glfw.GetMonitors(out int count);
if (monitors is not null && i < count)
return monitors[i];
}
return glfw.GetPrimaryMonitor();
}
/// <summary>The monitor's highest refresh rate for an exact WxH, or
/// false when the monitor does not report the mode at all.</summary>
/// false when the monitor does not report the mode at all. (Register
/// row AD-92: retail passed the device mode's own refresh as-is;
/// highest-for-WxH is a deliberate adaptation.)</summary>
private static bool TryFindRefreshRate(
Glfw glfw, Silk.NET.GLFW.Monitor* monitor, int width, int height, out int refresh)
{

View file

@ -45,13 +45,20 @@ internal interface IRuntimeChatOpacityTarget
void Apply(float defaultOpacity, float activeOpacity);
}
/// <summary>The one window property the display apply touches — a narrow
/// <summary>The window properties the display apply touches — a narrow
/// seam so the #388 state machine is testable without faking all of
/// <see cref="IWindow"/> (same idiom as <c>FakePacingSurface</c>'s
/// surface).</summary>
internal interface IWindowedSizeSurface
{
Vector2D<int> Size { get; set; }
/// <summary>#388 blast M3: a maximized window silently ignores a Size
/// write — the apply un-maximizes first (the deleted
/// <c>WindowState = Normal</c> write used to do this incidentally).</summary>
bool IsMaximized { get; }
void Restore();
}
internal sealed class SilkWindowSizeSurface(IWindow window) : IWindowedSizeSurface
@ -64,6 +71,10 @@ internal sealed class SilkWindowSizeSurface(IWindow window) : IWindowedSizeSurfa
get => _window.Size;
set => _window.Size = value;
}
public bool IsMaximized => _window.WindowState == WindowState.Maximized;
public void Restore() => _window.WindowState = WindowState.Normal;
}
internal sealed class SilkRuntimeDisplayWindowTarget : IRuntimeDisplayWindowTarget
@ -76,11 +87,14 @@ internal sealed class SilkRuntimeDisplayWindowTarget : IRuntimeDisplayWindowTarg
: this(
new SilkWindowSizeSurface(window),
new GlfwDisplayModeSwitcher(window),
// #391's catalog is the validation source: an offered mode is
// supported by construction. With no catalog installed
// (fixture/uninitialised hosts) nothing is "offered", so
// fullscreen entry is refused rather than guessed.
spec => Rendering.DisplayModeCatalog.Resolutions?.Contains(spec) == true)
// #391's catalog is the validation source. With no catalog
// installed, the dropdown falls back to the static preset
// ladder — the validator must fall back to the SAME list
// (blast M2: an asymmetric fallback made Full Screen a permanent
// silent no-op on catalog-less hosts). The switcher's own
// monitor-mode-list check remains the hard guard either way.
spec => (Rendering.DisplayModeCatalog.Resolutions
?? DisplaySettings.AvailableResolutions).Contains(spec))
{
}
@ -116,6 +130,19 @@ internal sealed class SilkRuntimeDisplayWindowTarget : IRuntimeDisplayWindowTarg
if (display.Fullscreen)
{
if (!haveResolution)
{
// Mechanism M5: never a SILENT bail-out — §D6's acceptance is
// "any refused/failed switch logs a line".
Console.WriteLine(
$"display: fullscreen refused — unparseable resolution '{display.Resolution}'");
return;
}
// Mechanism/blast M2: idempotence BEFORE any native work — every
// Display-backed Config row applies per change (sliders per drag
// tick), and only this guard keeps those from re-issuing a real
// display-mode change per mouse sample.
if (_modeSwitcher.CurrentFullscreenMode is (int curW, int curH)
&& curW == width && curH == height)
return;
if (!_isOfferedMode.Invoke($"{width}x{height}"))
{
@ -125,7 +152,7 @@ internal sealed class SilkRuntimeDisplayWindowTarget : IRuntimeDisplayWindowTarg
}
if (!_modeSwitcher.TryEnterFullscreen(width, height, out string? error))
Console.WriteLine(
$"display: fullscreen {width}x{height} failed ({error}) — staying windowed");
$"display: fullscreen {width}x{height} failed ({error}) — window state unchanged (#392 tracks the persisted-flag divergence)");
return;
}
@ -146,6 +173,11 @@ internal sealed class SilkRuntimeDisplayWindowTarget : IRuntimeDisplayWindowTarg
if (haveResolution && (_window.Size.X != width || _window.Size.Y != height))
{
// Blast M3: a maximized window ignores Size writes — restore
// first (the deleted WindowState=Normal write did this
// incidentally; now it is explicit and only-when-needed).
if (_window.IsMaximized)
_window.Restore();
// #387 evidence line (permanent): the resolution-pick write path.
Console.WriteLine(
$"display: resolution pick {width}x{height} " +