Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ English (US) release notes, mirroring CHANGELOG.md (Brazilian Portuguese). Befor
### What's new
- Settings → "Receive test versions (alpha and beta)": anyone can join the tests of upcoming versions from the app. Off by default; turning it on shows a warning that test versions can have bugs.

### Fixes
- Displays: a monitor turned to portrait (rotated) keeps its rotation in the layout backup, so restoring the desk puts it back in portrait. Before, the rotation was never saved or restored. (#108)

### Behind the scenes
- Publishing a stable release now also opens the update PR on winget (`winget install lippdev.ConsoleMode`). Test versions are skipped. (#106)

Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Notas em português do Brasil; a versão em inglês (Estados Unidos) fica em `CH
### Novidades
- Ajustes → "Receber versões de teste (alpha e beta)": qualquer pessoa pode entrar nos testes das próximas versões pelo próprio app. Desligado por padrão; ao ligar, o app avisa que a versão pode ter bugs.

### Correções
- Telas: um monitor girado (em retrato) mantém a rotação no backup do layout, então restaurar a mesa devolve o monitor em retrato. Antes a rotação nunca era salva nem restaurada. (#108)

### Por trás dos panos
- Publicar uma versão estável agora também abre o PR de atualização no winget (`winget install lippdev.ConsoleMode`). Versões de teste ficam de fora. (#106)

Expand Down
1 change: 1 addition & 0 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ Pré-condições: 1.6 instalada por cima da 1.5, com a configuração da 1.5 (te
- [ ] TV começando **desligada**: "Jogar agora" liga a TV, ela vira a principal e as telas da mesa desligam. O log mostra `Telas: ativar …` e `Telas: desativar …` com `=> 0`. Resultado: ______
- [ ] Resolução/Hz salvos para a TV são aplicados. Resultado: ______
- [ ] Ao sair do Big Picture/Playnite, a mesa volta **exatamente** como estava: mesmas telas, principal, posições e resoluções. O log mostra `Telas: layout restaurado => 0`. Resultado: ______
- [ ] Monitor **girado (retrato)**: depois de restaurar, ele volta girado, na mesma posição e resolução (ex.: 1080×1920 a 90°). O `backup_monitores.cfg` guarda `DisplayOrientation=1` para ele. Resultado: ______
- [ ] Repetir a sessão 3 vezes seguidas: nenhuma tela troca de nome (\\.\DISPLAYn) nem de posição entre uma sessão e outra. Resultado: ______
- [ ] Com a TV já **ligada** antes de começar: mesmo resultado. Resultado: ______

Expand Down
12 changes: 9 additions & 3 deletions src/ConsoleMode/Native/NativeDisplays.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,9 @@ public static int Disable(IReadOnlyCollection<string> gdiNames)

/// <summary>
/// Queues a mode and/or position for a monitor (applied by <see cref="ApplyPending"/>);
/// zero or null keeps the current value.
/// zero or null keeps the current value; a negative orientation leaves the rotation alone.
/// </summary>
public static int QueueMode(string gdiName, int width, int height, int frequency, int bitsPerPixel, int? x, int? y)
public static int QueueMode(string gdiName, int width, int height, int frequency, int bitsPerPixel, int? x, int? y, int orientation = -1)
{
var dm = new NativeWindows.DEVMODE { dmSize = (short)Marshal.SizeOf<NativeWindows.DEVMODE>() };
if (!NativeWindows.EnumDisplaySettings(gdiName, NativeWindows.ENUM_CURRENT_SETTINGS, ref dm)) return -1;
Expand All @@ -178,6 +178,12 @@ public static int QueueMode(string gdiName, int width, int height, int frequency
dm.dmPositionY = y.Value;
dm.dmFields |= DmPosition;
}
// Only when it differs from the current one: setups that never rotate keep the exact same call.
if (orientation >= 0 && orientation != dm.dmDisplayOrientation)
{
dm.dmDisplayOrientation = orientation;
dm.dmFields |= DmDisplayOrientation;
}
if (dm.dmFields == 0) return 0;
var code = ChangeDisplaySettingsEx(gdiName, ref dm, 0, CdsUpdateRegistry | CdsNoReset, 0);
if (code != 0) AppLog.Write($"Telas: modo {width}x{height}@{frequency} em {gdiName} => {code}");
Expand Down Expand Up @@ -272,7 +278,7 @@ private static nint FindHMonitor(string gdiName)
private static long Key(LUID id) => ((long)id.HighPart << 32) | id.LowPart;

private const int ErrorInsufficientBuffer = 122;
private const int DmPosition = 0x20, DmBitsPerPel = 0x40000, DmPelsWidth = 0x80000, DmPelsHeight = 0x100000, DmDisplayFrequency = 0x400000;
private const int DmPosition = 0x20, DmDisplayOrientation = 0x80, DmBitsPerPel = 0x40000, DmPelsWidth = 0x80000, DmPelsHeight = 0x100000, DmDisplayFrequency = 0x400000;
private const uint CdsUpdateRegistry = 0x1, CdsNoReset = 0x10000000;

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
Expand Down
7 changes: 7 additions & 0 deletions src/ConsoleMode/Native/NativeWindows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -348,4 +348,11 @@ public static DisplayModeInfo[] EnumerateDisplayModes(string deviceName)
return BuildDisplayModeInfo(dm.dmPelsWidth, dm.dmPelsHeight, dm.dmDisplayFrequency, dm.dmBitsPerPel,
ConsoleMode.Services.LocalizationService.Get("CurrentModeSuffix"));
}

/// <summary>DEVMODE.dmDisplayOrientation of the current mode (0 landscape, 1 = 90°, 2 = 180°, 3 = 270°); 0 when unreadable.</summary>
public static int GetCurrentOrientation(string deviceName)
{
var dm = new DEVMODE { dmSize = (short)Marshal.SizeOf<DEVMODE>() };
return EnumDisplaySettings(deviceName, ENUM_CURRENT_SETTINGS, ref dm) ? dm.dmDisplayOrientation : 0;
}
}
7 changes: 5 additions & 2 deletions src/ConsoleMode/Services/DisplayBackend.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ public void SaveLayout(string path)
var current = m.IsActive ? NativeWindows.GetCurrentDisplayMode(m.Name) : null;
MonitorService.ParseLeftTop(m.LeftTop, out var x, out var y);
return new LayoutEntry(m.Name, m.MonitorId, m.SerialNumber, current?.BitsPerPel ?? 0,
current?.Width ?? 0, current?.Height ?? 0, current?.Frequency ?? 0, x ?? 0, y ?? 0);
current?.Width ?? 0, current?.Height ?? 0, current?.Frequency ?? 0, x ?? 0, y ?? 0,
m.IsActive ? NativeWindows.GetCurrentOrientation(m.Name) : 0);
});
File.WriteAllText(path, DisplayIdentity.FormatLayout(entries));
}
Expand All @@ -99,7 +100,9 @@ public void LoadLayout(string path)
int.TryParse(spec.GetValueOrDefault("BitsPerPixel"), out var bits);
int? x = int.TryParse(spec.GetValueOrDefault("PositionX"), out var px) ? px : null;
int? y = int.TryParse(spec.GetValueOrDefault("PositionY"), out var py) ? py : null;
NativeDisplays.QueueMode(name, w, h, f, bits, x, y);
// Missing (older backup) means: leave the rotation alone.
var orientation = int.TryParse(spec.GetValueOrDefault("DisplayOrientation"), out var o) ? o : -1;
NativeDisplays.QueueMode(name, w, h, f, bits, x, y, orientation);
}
var code = NativeDisplays.ApplyPending();
AppLog.Write($"Telas: layout restaurado => {code}");
Expand Down
9 changes: 6 additions & 3 deletions src/ConsoleMode/Services/DisplayIdentity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@

namespace ConsoleMode.Services;

/// <summary>One monitor in the layout backup (MultiMonitorTool's .cfg format, kept for compatibility).</summary>
/// <summary>
/// One monitor in the layout backup (MultiMonitorTool's .cfg format, kept for compatibility).
/// <c>Orientation</c> is DEVMODE.dmDisplayOrientation: 0 landscape, 1 = 90°, 2 = 180°, 3 = 270°.
/// </summary>
public sealed record LayoutEntry(string Name, string MonitorId, string Serial, int BitsPerPixel,
int Width, int Height, int Frequency, int PositionX, int PositionY);
int Width, int Height, int Frequency, int PositionX, int PositionY, int Orientation = 0);

/// <summary>What the monitor's EDID says about itself.</summary>
public sealed record EdidInfo(string Name, string Serial, int PreferredWidth, int PreferredHeight);
Expand Down Expand Up @@ -123,7 +126,7 @@ public static string FormatLayout(IEnumerable<LayoutEntry> entries)
sb.Append($"Height={e.Height}\r\n");
sb.Append("DisplayFlags=0\r\n");
sb.Append($"DisplayFrequency={e.Frequency}\r\n");
sb.Append("DisplayOrientation=0\r\n");
sb.Append($"DisplayOrientation={e.Orientation}\r\n");
sb.Append($"PositionX={e.PositionX}\r\n");
sb.Append($"PositionY={e.PositionY}\r\n");
}
Expand Down
49 changes: 49 additions & 0 deletions tests/ConsoleMode.Tests/DisplayIdentityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,55 @@ public void Layout_file_round_trips_and_follows_renamed_monitors()
Assert.Equal(@"\\.\DISPLAY2", remapped[@"\\.\DISPLAY2"]["Name"]);
}

[Fact]
public void Layout_file_keeps_a_rotated_monitor_portrait()
{
// A monitor turned to portrait: 1080x1920 at 90 degrees. Restoring it needs the rotation too.
var text = DisplayIdentity.FormatLayout(
[
new LayoutEntry(@"\\.\DISPLAY2", @"MONITOR\HWP3318\{x}\0005", "SERIAL1", 32, 1080, 1920, 60, 1920, -301, Orientation: 1)
]);
var specs = DisplayIdentity.ParseLayout(text.Split("\r\n"));

Assert.Equal("1", specs[@"\\.\DISPLAY2"]["DisplayOrientation"]);
Assert.Equal("1080", specs[@"\\.\DISPLAY2"]["Width"]);
Assert.Equal("1920", specs[@"\\.\DISPLAY2"]["Height"]);
}

[Fact]
public void Layout_file_writes_landscape_when_no_orientation_is_given()
{
var text = DisplayIdentity.FormatLayout(
[new LayoutEntry(@"\\.\DISPLAY1", @"MONITOR\DEL4083\{x}\0001", "", 32, 1920, 1080, 60, 0, 0)]);

Assert.Contains("DisplayOrientation=0\r\n", text);
}

[Fact]
public void A_backup_made_by_1_5_is_written_back_unchanged()
{
// The shape of a real 1.5 backup (MultiMonitorTool): a portrait side monitor and a TV that was off.
string[] original =
[
"[Monitor0]", @"Name=\\.\DISPLAY1", @"MonitorID=MONITOR\TCL1003\{4d36e96e-e325-11ce-bfc1-08002be10318}\0003",
"SerialNumber=", "BitsPerPixel=32", "Width=1920", "Height=1080", "DisplayFlags=0", "DisplayFrequency=240",
"DisplayOrientation=0", "PositionX=0", "PositionY=0",
"[Monitor1]", @"Name=\\.\DISPLAY2", @"MonitorID=MONITOR\HWP3318\{4d36e96e-e325-11ce-bfc1-08002be10318}\0005",
"SerialNumber=SN00000001", "BitsPerPixel=32", "Width=1080", "Height=1920", "DisplayFlags=0", "DisplayFrequency=60",
"DisplayOrientation=1", "PositionX=1920", "PositionY=-301",
"[Monitor2]", @"Name=\\.\DISPLAY3", @"MonitorID=MONITOR\GSM0001\{4d36e96e-e325-11ce-bfc1-08002be10318}\0002",
"SerialNumber=", "BitsPerPixel=0", "Width=0", "Height=0", "DisplayFlags=0", "DisplayFrequency=0",
"DisplayOrientation=0", "PositionX=0", "PositionY=0"
];

var entries = DisplayIdentity.ParseLayout(original).Values.Select(s => new LayoutEntry(
s["Name"], s["MonitorID"], s.GetValueOrDefault("SerialNumber") ?? "", int.Parse(s["BitsPerPixel"]),
int.Parse(s["Width"]), int.Parse(s["Height"]), int.Parse(s["DisplayFrequency"]),
int.Parse(s["PositionX"]), int.Parse(s["PositionY"]), int.Parse(s["DisplayOrientation"]))).ToList();

Assert.Equal(original, DisplayIdentity.FormatLayout(entries).Split("\r\n", StringSplitOptions.RemoveEmptyEntries));
}

private static void WriteText(byte[] edid, int offset, byte tag, string text)
{
edid[offset + 3] = tag;
Expand Down
Loading