fix #407: windowed resolution offering decoupled from the video-mode list

Campaign CC gate round 1. The Config Resolution dropdown now offers
DisplayModeCatalog.WindowedResolutions — the curated hardware modes
UNIONed with the static modern-ladder sizes that fit the desktop —
because a windowed pick is a plain Size write needing no video mode,
and remote/RDP virtual displays advertise almost none (the live RDP
display exposed exactly 1920x1080 + the 2056x1290 desktop, leaving the
dropdown with nothing below 1920). The fullscreen apply still validates
against the hardware Resolutions list plus the switcher's
monitor-mode-list hard guard, so a fullscreen pick of a windowed-only
entry refuses safely (log-and-stay, #388/#392) — IA-22's
offered-implies-supported invariant narrows to the fullscreen half and
its register row carries the amendment. Three new pure-union tests
including the exact live RDP shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-16 09:03:36 +02:00
parent 26e6f984f4
commit e601a496db
4 changed files with 119 additions and 5 deletions

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AcDream.UI.Abstractions.Panels.Settings;
using Silk.NET.Windowing;
namespace AcDream.App.Rendering;
@ -29,13 +30,28 @@ namespace AcDream.App.Rendering;
internal static class DisplayModeCatalog
{
private static IReadOnlyList<string>? _resolutions;
private static IReadOnlyList<string>? _windowedResolutions;
private static string? _desktopResolution;
/// <summary>The curated list, or null when no catalog was installed
/// (fixture/headless callers — consumers fall back to the static
/// preset ladder).</summary>
/// <summary>The curated HARDWARE mode list, or null when no catalog was
/// installed (fixture/headless callers — consumers fall back to the
/// static preset ladder). This is the fullscreen mode-switch validation
/// source (#376/#388): a fullscreen pick must be a real adapter mode.</summary>
public static IReadOnlyList<string>? Resolutions => _resolutions;
/// <summary>#407: the WINDOWED size offering — the curated hardware
/// modes UNIONed with the static modern ladder entries that fit the
/// desktop. A windowed client needs no video mode (a Size write is
/// displayable at any size ≤ desktop), so gating the windowed dropdown
/// on the adapter's mode list starved remote/virtual displays whose
/// drivers advertise almost nothing (the RDP display that exposed only
/// 1920x1080 + the desktop mode, found live at the Campaign CC gate).
/// Null when no catalog was installed. The fullscreen APPLY still
/// validates against <see cref="Resolutions"/> + the switcher's own
/// monitor-mode-list hard guard, so a fullscreen pick of a
/// windowed-only entry refuses safely (log-and-stay, #388/#392).</summary>
public static IReadOnlyList<string>? WindowedResolutions => _windowedResolutions;
/// <summary>The desktop's current mode as a "WxH" string — the Config
/// Resolution row's Defaults value in production (see the class doc for
/// why this replaces retail's authored 800x600). Null when no catalog
@ -69,6 +85,7 @@ internal static class DisplayModeCatalog
return;
_resolutions = curated;
_windowedResolutions = BuildWindowedOffering(curated, (desktop.X, desktop.Y));
_desktopResolution = $"{desktop.X}x{desktop.Y}";
}
@ -76,9 +93,59 @@ internal static class DisplayModeCatalog
internal static void ResetForTests()
{
_resolutions = null;
_windowedResolutions = null;
_desktopResolution = null;
}
/// <summary>
/// #407's pure union rule: the windowed offering is every curated
/// hardware mode plus every static-ladder entry that fits the desktop,
/// deduped, ascending by width then height — the same ordering
/// <see cref="Curate"/> emits so the dropdown reads identically on
/// physical and remote displays.
/// </summary>
internal static IReadOnlyList<string> BuildWindowedOffering(
IReadOnlyList<string> curated,
(int W, int H) desktop)
{
var keep = new SortedSet<(int W, int H)>(
Comparer<(int W, int H)>.Create(static (a, b) =>
a.W != b.W ? a.W.CompareTo(b.W) : a.H.CompareTo(b.H)));
foreach (string spec in curated)
{
if (TryParse(spec, out (int W, int H) mode))
keep.Add(mode);
}
foreach (string spec in DisplaySettings.AvailableResolutions)
{
if (TryParse(spec, out (int W, int H) mode)
&& mode.W <= desktop.W
&& mode.H <= desktop.H)
{
keep.Add(mode);
}
}
return keep.Select(static m => $"{m.W}x{m.H}").ToArray();
static bool TryParse(string spec, out (int W, int H) mode)
{
mode = default;
string[] parts = spec.Split('x', 2);
if (parts.Length == 2
&& int.TryParse(parts[0], out int w)
&& int.TryParse(parts[1], out int h)
&& w > 0
&& h > 0)
{
mode = (w, h);
return true;
}
return false;
}
}
/// <summary>
/// The pure curation rule (#391): keep a mode iff
/// - it is a modern widescreen format (16:9, 16:10, or ultrawide 21:9 /

View file

@ -2578,7 +2578,12 @@ public sealed class RetailUiRuntime : IDisposable
// default, installed at startup by the graphical host;
// fixture/headless mounts leave the catalog empty and the
// controller falls back to the static preset ladder.
availableResolutions: Rendering.DisplayModeCatalog.Resolutions,
// #407: the dropdown offers the WINDOWED union (hardware
// modes + static-ladder sizes that fit the desktop) — a
// windowed Size write needs no video mode, and remote/RDP
// displays advertise almost none. The fullscreen APPLY
// still validates against the hardware list only.
availableResolutions: Rendering.DisplayModeCatalog.WindowedResolutions,
resolutionDefault: Rendering.DisplayModeCatalog.DesktopResolution);
if (!configBound)
Console.WriteLine("[UI] options panel: Config tab rows did not bind.");