diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index d39999b..4bad687 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -4,6 +4,13 @@ English (US) release notes, mirroring CHANGELOG.md (Brazilian Portuguese). Befor ## [Unreleased] +## [1.6.0-beta.7] +### Heads-up: beta version, not the stable one +- This is a test version of 1.6. The stable version is still 1.5.1, and people on it don't get this update automatically: only those who turned on Settings → "Receive test versions (alpha and beta)" or are already on an alpha/beta. If something breaks, go back to 1.5.1 and report it with the feedback button (attach `consolemode.log`). + +### What's new +- FPS counter over the game, through RivaTuner (RTSS), in Console Mode's style: pick it in Settings or cycle it with A on the new "FPS counter" row of the session menu. Styles: Afterburner (default; changes nothing), Off (nothing on screen), Compact (FPS only), Detailed (API, FPS and frame time) and Custom, where you pick the items, whether they share one line and the number color. Except on "Afterburner", MSI Afterburner's OSD is hidden during the session and comes back by itself when you go back to the PC; Afterburner's settings are not changed. (#138) + ## [1.6.0-beta.6] ### Heads-up: beta version, not the stable one - This is a test version of 1.6. The stable version is still 1.5.1, and people on it don't get this update automatically: only those who turned on Settings → "Receive test versions (alpha and beta)" or are already on an alpha/beta. If something breaks, go back to 1.5.1 and report it with the feedback button (attach `consolemode.log`). diff --git a/CHANGELOG.md b/CHANGELOG.md index f0d911f..4bd76a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ Notas em português do Brasil; a versão em inglês (Estados Unidos) fica em `CH ## [Unreleased] +## [1.6.0-beta.7] +### Atenção: versão beta, não é a estável +- Esta é uma versão de teste da 1.6. A versão estável continua sendo a 1.5.1, e quem está nela não recebe esta atualização automaticamente: só quem ligou Ajustes → "Receber versões de teste (alpha e beta)" ou já está numa alpha/beta. Se algo falhar, volte para a 1.5.1 e conte pelo botão de feedback (anexe o `consolemode.log`). + +### Novidades +- Contador de FPS sobre o jogo, pelo RivaTuner (RTSS), no estilo do Console Mode: escolha em Ajustes ou alterne com A na nova linha "Contador de FPS" do menu da sessão. Estilos: Afterburner (padrão; não mexe em nada), Desligado (nada na tela), Compacto (só o FPS), Detalhado (API, FPS e tempo de quadro) e Personalizado, em que você escolhe os itens, se ficam numa linha só e a cor dos números. Fora de "Afterburner", o OSD do MSI Afterburner fica escondido durante a sessão e volta sozinho ao voltar ao PC; a configuração do Afterburner não é alterada. (#138) + ## [1.6.0-beta.6] ### Atenção: versão beta, não é a estável - Esta é uma versão de teste da 1.6. A versão estável continua sendo a 1.5.1, e quem está nela não recebe esta atualização automaticamente: só quem ligou Ajustes → "Receber versões de teste (alpha e beta)" ou já está numa alpha/beta. Se algo falhar, volte para a 1.5.1 e conte pelo botão de feedback (anexe o `consolemode.log`). diff --git a/docs/TESTING.md b/docs/TESTING.md index 569fbcc..9531d2e 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -376,3 +376,20 @@ Pré-condições: 1.6 instalada por cima da 1.5, com a configuração da 1.5 (te - [ ] Tente usar uma combinação já atribuída e confirme que a mensagem de conflito aparece inteira e não corta os botões. - [ ] Reduza a altura da janela e confirme que o editor rola até **Voltar**, sem esconder os cartões. - [ ] Feche o editor durante uma captura e confirme que ela para; reabra-o e confirme que nenhum botão fica preso em captura. + +## Contador de FPS (RTSS) + +Pré-requisito: RivaTuner Statistics Server instalado, com "Show On-Screen Display" ligado; MSI Afterburner aberto com o OSD dele ligado. + +- [ ] Sem o RTSS instalado, a linha "Contador de FPS" não aparece no menu da sessão e o cartão em Ajustes fica desabilitado. +- [ ] Na prévia (menu aberto fora de uma sessão), a linha não aparece. +- [ ] Config nova: o estilo é "Afterburner (não mexer)" e o OSD do Afterburner aparece normal na sessão. +- [ ] Em sessão, A na linha alterna Afterburner → Desligado → Compacto → Detalhado → Personalizado → Afterburner, e o jogo muda na hora. +- [ ] Desligado: nada na tela, nem o nosso nem o do Afterburner (no máximo um piscar rápido do Afterburner de vez em quando). +- [ ] Compacto / Detalhado / Personalizado: só o nosso aparece; o do Afterburner fica escondido. +- [ ] Afterburner: o nosso some e o do Afterburner volta em até 1 s. +- [ ] Ajustes → Contador de FPS → Personalizado: marcar/desmarcar API, FPS e tempo de quadro, "Tudo em uma linha" e a cor muda o contador no jogo na hora (com a sessão ativa). +- [ ] Personalizado sem nenhum item marcado ainda mostra o FPS. +- [ ] Com o RTSS fechado, ligar Compacto abre o RTSS e o contador aparece. +- [ ] Voltar ao PC: o nosso some e o do Afterburner volta. +- [ ] A escolha e o layout ficam salvos para a próxima sessão. diff --git a/src/ConsoleMode/ConsoleMode.csproj b/src/ConsoleMode/ConsoleMode.csproj index 83fc632..50e9a09 100644 --- a/src/ConsoleMode/ConsoleMode.csproj +++ b/src/ConsoleMode/ConsoleMode.csproj @@ -17,10 +17,10 @@ true latest x64 - 1.6.0-beta.6 + 1.6.0-beta.7 1.6.0.0 1.6.0.0 - 1.6.0-beta.6 + 1.6.0-beta.7 Console Mode Console Mode lippdev diff --git a/src/ConsoleMode/Models/Models.cs b/src/ConsoleMode/Models/Models.cs index 7f06762..16726e3 100644 --- a/src/ConsoleMode/Models/Models.cs +++ b/src/ConsoleMode/Models/Models.cs @@ -20,6 +20,9 @@ public sealed class AppConfig public string AudioDeviceName { get; set; } = ""; public bool AudioAutoSwitch { get; set; } public int FpsLimit { get; set; } + /// RtssOverlay style: "external" | "off" | "compact" | "detailed" | "custom". Applied over the game during a session. + public string FpsOverlay { get; set; } = RtssOverlay.External; + public FpsOverlayLayout FpsOverlayLayout { get; set; } = new(); public Dictionary MonitorModes { get; set; } = new(StringComparer.OrdinalIgnoreCase); public bool HdrEnable { get; set; } public bool VrrEnable { get; set; } diff --git a/src/ConsoleMode/Resources/Strings.en-US.json b/src/ConsoleMode/Resources/Strings.en-US.json index 62526e5..62f6435 100644 --- a/src/ConsoleMode/Resources/Strings.en-US.json +++ b/src/ConsoleMode/Resources/Strings.en-US.json @@ -226,6 +226,24 @@ "PlaynitePathInvalid": "Could not find Playnite.FullscreenApp.exe at {0}.", "PlaynitePathPickError": "Could not open the file picker: {0}", "LaunchXbox": "Xbox mode (experimental)", + "FpsOverlayCard": "FPS counter", + "FpsOverlayExternal": "Afterburner (leave as is)", + "FpsOverlayOff": "Off (hide everything)", + "FpsOverlayCustom": "Custom", + "FpsOverlayDescription": "Shows the FPS over the game during a session, through RTSS. Except on \"Afterburner\", MSI Afterburner's OSD is hidden while console mode is on.", + "FpsOverlayItems": "Items", + "FpsOverlayShowApi": "API (D3D/Vulkan)", + "FpsOverlayShowFps": "FPS", + "FpsOverlayShowFrameTime": "Frame time (ms)", + "FpsOverlaySingleLine": "All on one line", + "FpsOverlayColor": "Number color", + "FpsOverlayColorWhite": "White", + "FpsOverlayColorBlue": "Blue", + "FpsOverlayColorGreen": "Green", + "FpsOverlayColorYellow": "Yellow", + "FpsOverlayColorOrange": "Orange", + "FpsOverlayCompact": "Compact", + "FpsOverlayDetailed": "Detailed", "FpsNoLimit": "No limit", "FpsCustom": "Custom", "FpsAvailable": "Applies to all games while console mode is active. Uses RTSS.", diff --git a/src/ConsoleMode/Resources/Strings.es-ES.json b/src/ConsoleMode/Resources/Strings.es-ES.json index fb9cc6a..f76fb90 100644 --- a/src/ConsoleMode/Resources/Strings.es-ES.json +++ b/src/ConsoleMode/Resources/Strings.es-ES.json @@ -226,6 +226,24 @@ "PlaynitePathInvalid": "No se encontró Playnite.FullscreenApp.exe en {0}.", "PlaynitePathPickError": "No se pudo abrir el selector de archivos: {0}", "LaunchXbox": "Modo Xbox (experimental)", + "FpsOverlayCard": "Contador de FPS", + "FpsOverlayExternal": "Afterburner (no tocar)", + "FpsOverlayOff": "Desactivado (oculta todo)", + "FpsOverlayCustom": "Personalizado", + "FpsOverlayDescription": "Muestra los FPS sobre el juego durante la sesión, mediante RTSS. Salvo en \"Afterburner\", el OSD de MSI Afterburner se oculta mientras el modo consola esté activo.", + "FpsOverlayItems": "Elementos", + "FpsOverlayShowApi": "API (D3D/Vulkan)", + "FpsOverlayShowFps": "FPS", + "FpsOverlayShowFrameTime": "Tiempo de fotograma (ms)", + "FpsOverlaySingleLine": "Todo en una línea", + "FpsOverlayColor": "Color de los números", + "FpsOverlayColorWhite": "Blanco", + "FpsOverlayColorBlue": "Azul", + "FpsOverlayColorGreen": "Verde", + "FpsOverlayColorYellow": "Amarillo", + "FpsOverlayColorOrange": "Naranja", + "FpsOverlayCompact": "Compacto", + "FpsOverlayDetailed": "Detallado", "FpsNoLimit": "Sin límite", "FpsCustom": "Personalizado", "FpsAvailable": "Se aplica a todos los juegos mientras el modo consola está activo. Usa RTSS.", diff --git a/src/ConsoleMode/Resources/Strings.pt-BR.json b/src/ConsoleMode/Resources/Strings.pt-BR.json index b09d8ee..5b20c6b 100644 --- a/src/ConsoleMode/Resources/Strings.pt-BR.json +++ b/src/ConsoleMode/Resources/Strings.pt-BR.json @@ -226,6 +226,24 @@ "PlaynitePathInvalid": "Não achei o Playnite.FullscreenApp.exe em {0}.", "PlaynitePathPickError": "Não foi possível abrir o seletor de arquivos: {0}", "LaunchXbox": "Modo Xbox (experimental)", + "FpsOverlayCard": "Contador de FPS", + "FpsOverlayExternal": "Afterburner (não mexer)", + "FpsOverlayOff": "Desligado (esconde tudo)", + "FpsOverlayCustom": "Personalizado", + "FpsOverlayDescription": "Mostra o FPS sobre o jogo durante a sessão, pelo RTSS. Fora de \"Afterburner\", o OSD do MSI Afterburner fica escondido enquanto o modo console estiver ativo.", + "FpsOverlayItems": "Itens", + "FpsOverlayShowApi": "API (D3D/Vulkan)", + "FpsOverlayShowFps": "FPS", + "FpsOverlayShowFrameTime": "Tempo de quadro (ms)", + "FpsOverlaySingleLine": "Tudo em uma linha", + "FpsOverlayColor": "Cor dos números", + "FpsOverlayColorWhite": "Branco", + "FpsOverlayColorBlue": "Azul", + "FpsOverlayColorGreen": "Verde", + "FpsOverlayColorYellow": "Amarelo", + "FpsOverlayColorOrange": "Laranja", + "FpsOverlayCompact": "Compacto", + "FpsOverlayDetailed": "Detalhado", "FpsNoLimit": "Sem limite", "FpsCustom": "Personalizado", "FpsAvailable": "Vale para todos os jogos enquanto o modo console estiver ativo. Usa o RTSS.", diff --git a/src/ConsoleMode/Services/ConsoleEngine.cs b/src/ConsoleMode/Services/ConsoleEngine.cs index 4d14c32..f55ea61 100644 --- a/src/ConsoleMode/Services/ConsoleEngine.cs +++ b/src/ConsoleMode/Services/ConsoleEngine.cs @@ -177,6 +177,10 @@ public void Start(AppConfig config, MonitorInfo? focusInfo, Func LocalizationService.Get(nameof(VrrCard)); public string VrrDescription => LocalizationService.Get(nameof(VrrDescription)); public string FpsCard => LocalizationService.Get(nameof(FpsCard)); + public string FpsOverlayCard => LocalizationService.Get(nameof(FpsOverlayCard)); + public string FpsOverlayItems => LocalizationService.Get(nameof(FpsOverlayItems)); + public string FpsOverlayShowApi => LocalizationService.Get(nameof(FpsOverlayShowApi)); + public string FpsOverlayShowFps => LocalizationService.Get(nameof(FpsOverlayShowFps)); + public string FpsOverlayShowFrameTime => LocalizationService.Get(nameof(FpsOverlayShowFrameTime)); + public string FpsOverlaySingleLine => LocalizationService.Get(nameof(FpsOverlaySingleLine)); + public string FpsOverlayColor => LocalizationService.Get(nameof(FpsOverlayColor)); public string FpsPlaceholder => LocalizationService.Get(nameof(FpsPlaceholder)); public string AppSection => LocalizationService.Get(nameof(AppSection)); public string StartupCard => LocalizationService.Get(nameof(StartupCard)); diff --git a/src/ConsoleMode/Services/RtssOverlay.cs b/src/ConsoleMode/Services/RtssOverlay.cs new file mode 100644 index 0000000..00a26b3 --- /dev/null +++ b/src/ConsoleMode/Services/RtssOverlay.cs @@ -0,0 +1,88 @@ +using System.Text.RegularExpressions; + +namespace ConsoleMode.Services; + +/// What the custom FPS counter shows and how (Settings → FPS counter → Custom). +public sealed class FpsOverlayLayout +{ + public bool ShowApi { get; set; } = true; + public bool ShowFps { get; set; } = true; + public bool ShowFrameTime { get; set; } = true; + /// Everything on one line instead of one item per line. + public bool SingleLine { get; set; } + /// RRGGBB of the numbers; one of . + public string Color { get; set; } = RtssOverlay.Colors[0]; +} + +/// +/// Our FPS counter on the game, drawn by RTSS in its own OSD slot (). +/// Every style except takes the screen: MSI Afterburner's OSD is hidden +/// while it is on, so "Off" really means nothing on screen. External leaves Afterburner/RTSS alone. +/// +public static partial class RtssOverlay +{ + public const string External = "external"; + public const string Off = "off"; + public const string Compact = "compact"; + public const string Detailed = "detailed"; + public const string Custom = "custom"; + + /// szOSDOwner of our slot. + public const string Owner = "ConsoleMode"; + + /// Menu order; External first, the default, so nobody's Afterburner disappears unasked. + public static readonly string[] Styles = [External, Off, Compact, Detailed, Custom]; + + /// White, blue, green, yellow, orange. + public static readonly string[] Colors = ["FFFFFF", "4CC2FF", "6FE07A", "FFD54A", "FF8A3D"]; + + public static string Normalize(string? style) => + Styles.FirstOrDefault(s => string.Equals(s, style, StringComparison.OrdinalIgnoreCase)) ?? External; + + /// A on the session menu row walks and wraps around. + public static string Next(string? style) => Styles[(Array.IndexOf(Styles, Normalize(style)) + 1) % Styles.Length]; + + /// Whether this style keeps Afterburner's OSD off the screen. + public static bool HidesOthers(string? style) => Normalize(style) != External; + + /// A color from the config, back to white if it isn't plain RRGGBB (it goes inside an RTSS tag). + public static string NormalizeColor(string? color) => + color is not null && HexColor().IsMatch(color) ? color.ToUpperInvariant() : Colors[0]; + + // RTSS hypertext: / define, … / … use; framerate, + // frametime and 3D API are filled in by RTSS for the game being drawn. + private const string Api = ""; + private const string Fps = " FPS"; + private const string FrameTime = " ms"; + + /// The OSD text for a style; empty for Off and External (nothing of ours on screen). + public static string Text(string? style, FpsOverlayLayout? layout = null) + { + string[] items; + var color = Colors[0]; + var separator = "\n"; + switch (Normalize(style)) + { + case Compact: + items = [Fps]; + break; + case Detailed: + items = [Api, Fps, FrameTime]; + break; + case Custom: + layout ??= new FpsOverlayLayout(); + items = [.. new[] { (layout.ShowApi, Api), (layout.ShowFps, Fps), (layout.ShowFrameTime, FrameTime) } + .Where(i => i.Item1).Select(i => i.Item2)]; + if (items.Length == 0) items = [Fps]; // nothing ticked still shows something + color = NormalizeColor(layout.Color); + if (layout.SingleLine) separator = " "; + break; + default: + return ""; + } + return $"" + string.Join(separator, items); + } + + [GeneratedRegex("^[0-9A-Fa-f]{6}$")] + private static partial Regex HexColor(); +} diff --git a/src/ConsoleMode/Services/RtssService.cs b/src/ConsoleMode/Services/RtssService.cs index 664b2e1..0a5e15a 100644 --- a/src/ConsoleMode/Services/RtssService.cs +++ b/src/ConsoleMode/Services/RtssService.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.IO.MemoryMappedFiles; using System.Text.Json; using ConsoleMode.Models; @@ -143,6 +144,196 @@ public void Restore(ConsoleRuntimeState state) } } + /// + /// Shows our FPS counter (RtssOverlay.Text) in our own OSD slot, or clears that slot when + /// the text is empty. With , MSI Afterburner's OSD is kept hidden + /// meanwhile (see HideAfterburnerLoop): an empty text then means nothing on screen at all. + /// + public bool SetOverlay(string text, bool hideOthers) + { + if (string.IsNullOrEmpty(text)) + { + WriteOverlaySlot(null); + // Off doesn't start RTSS just to hide something that isn't there. + if (hideOthers && IsRunning()) HideAfterburner(); + else _hideAfterburner = false; + return true; + } + if (!EnsureRunning()) return false; + // Right after RTSS starts its shared memory may not be there yet. + for (var attempt = 0; attempt < 10; attempt++) + { + if (WriteOverlaySlot(text)) + { + if (hideOthers) HideAfterburner(); + else _hideAfterburner = false; + return true; + } + Thread.Sleep(300); + } + return false; + } + + /// Applies a style from . + public bool ApplyOverlay(string? style, FpsOverlayLayout? layout) => + SetOverlay(RtssOverlay.Text(style, layout), RtssOverlay.HidesOthers(style)); + + private volatile bool _hideAfterburner; + private Thread? _afterburnerHider; + + private void HideAfterburner() + { + _hideAfterburner = true; + if (_afterburnerHider is { IsAlive: true }) return; + _afterburnerHider = new Thread(HideAfterburnerLoop) { IsBackground = true, Name = "RTSS Afterburner", Priority = ThreadPriority.AboveNormal }; + _afterburnerHider.Start(); + } + + /// + /// RTSS draws every slot and has no way to hide one, and Afterburner's own OSD hotkey turns off + /// the whole OSD (ours too). So, while our counter is on, empty Afterburner's slot text right + /// after each rewrite (it does one per polling period, ~1 s): at worst it shows for a frame. + /// Once we stop, its next rewrite brings it back by itself. + /// + private unsafe void HideAfterburnerLoop() + { + try + { + using var map = MemoryMappedFile.OpenExisting("RTSSSharedMemoryV2", MemoryMappedFileRights.ReadWrite); + using var view = map.CreateViewAccessor(0, 0, MemoryMappedFileAccess.ReadWrite); + byte* mem = null; + view.SafeMemoryMappedViewHandle.AcquirePointer(ref mem); + try + { + mem += view.PointerOffset; + var afterburner = System.Text.Encoding.ASCII.GetBytes(AfterburnerOwner); + var version = *(uint*)(mem + OffVersion); + var entrySize = *(uint*)(mem + OffOsdEntrySize); + var slot = mem + *(uint*)(mem + OffOsdArrOffset); // Afterburner keeps slot 0 + var hasEx2 = version >= 0x00020014 && entrySize > EntryOsdEx2; + while (_hideAfterburner && *(uint*)mem == Signature) + { + if (IsOwner(slot, afterburner) && (slot[EntryOsd] != 0 || slot[EntryOsdEx] != 0 || (hasEx2 && slot[EntryOsdEx2] != 0))) + { + slot[EntryOsd] = 0; + slot[EntryOsdEx] = 0; + if (hasEx2) slot[EntryOsdEx2] = 0; + Interlocked.Increment(ref *(int*)(mem + OffOsdFrame)); + } + Thread.Sleep(1); + } + } + finally + { + view.SafeMemoryMappedViewHandle.ReleasePointer(); + } + } + catch (FileNotFoundException) + { + // RTSS closed: nothing to hide + } + catch (Exception ex) + { + AppLog.Write($"RTSS: não foi possível esconder o OSD do Afterburner: {ex.Message}"); + } + } + + // RTSSSharedMemoryV2 (RTSS SDK, RTSSSharedMemory.h): header offsets and OSD entry layout. + private const uint Signature = 0x52545353; // 'RTSS' + private const int OffVersion = 4, OffOsdEntrySize = 20, OffOsdArrOffset = 24, OffOsdArrSize = 28, OffOsdFrame = 32, OffBusy = 36; + private const int EntryOsd = 0, EntryOwner = 256, EntryOsdEx = 512, OsdSize = 256, OwnerSize = 256, OsdExSize = 4096; + private const int EntryOsdEx2 = EntryOsdEx + OsdExSize + 262144; // after szOSDEx and the 256 KB object buffer + private const string AfterburnerOwner = "MSIAfterburner"; + + /// null = release our slot. Mirrors UpdateOSD/ReleaseOSD from the SDK sample. + private static unsafe bool WriteOverlaySlot(string? text) + { + try + { + using var map = MemoryMappedFile.OpenExisting("RTSSSharedMemoryV2", MemoryMappedFileRights.ReadWrite); + using var view = map.CreateViewAccessor(0, 0, MemoryMappedFileAccess.ReadWrite); + byte* mem = null; + view.SafeMemoryMappedViewHandle.AcquirePointer(ref mem); + try + { + mem += view.PointerOffset; + var version = *(uint*)(mem + OffVersion); + if (*(uint*)mem != Signature || version < 0x00020000) return false; + + var entrySize = *(uint*)(mem + OffOsdEntrySize); + var arrOffset = *(uint*)(mem + OffOsdArrOffset); + var arrSize = *(uint*)(mem + OffOsdArrSize); + var owner = System.Text.Encoding.ASCII.GetBytes(RtssOverlay.Owner); + var entries = mem + arrOffset; + + if (text is null) + { + // Slot 0 belongs to the primary client (Afterburner); third parties start at 1. + for (uint i = 1; i < arrSize; i++) + { + var entry = entries + i * entrySize; + if (!IsOwner(entry, owner)) continue; + new Span(entry, (int)entrySize).Clear(); + Interlocked.Increment(ref *(int*)(mem + OffOsdFrame)); + } + return true; + } + + // The slot we hold already, else the first free one. + byte* slot = null; + for (uint i = 1; i < arrSize && slot is null; i++) + if (IsOwner(entries + i * entrySize, owner)) slot = entries + i * entrySize; + for (uint i = 1; i < arrSize && slot is null; i++) + { + var entry = entries + i * entrySize; + if (entry[EntryOwner] != 0) continue; + owner.CopyTo(new Span(entry + EntryOwner, OwnerSize)); + slot = entry; + } + if (slot is null) return false; + + var bytes = System.Text.Encoding.ASCII.GetBytes(text); + var extended = version >= 0x00020007; + var target = new Span(slot + (extended ? EntryOsdEx : EntryOsd), extended ? OsdExSize : OsdSize); + // v2.14+: bit 0 of dwBusy locks the OSD while the renderer reads it. + var lockable = version >= 0x0002000e; + ref var busy = ref *(int*)(mem + OffBusy); + for (var spin = 0; lockable && (Interlocked.Or(ref busy, 1) & 1) != 0; spin++) + { + if (spin > 200) return false; + Thread.Sleep(1); + } + try + { + target.Clear(); + bytes.AsSpan(0, Math.Min(bytes.Length, target.Length - 1)).CopyTo(target); + } + finally + { + if (lockable) Interlocked.And(ref busy, ~1); + } + Interlocked.Increment(ref *(int*)(mem + OffOsdFrame)); + return true; + } + finally + { + view.SafeMemoryMappedViewHandle.ReleasePointer(); + } + } + catch (FileNotFoundException) + { + return false; // RTSS isn't running: nothing on screen to show or clear + } + catch (Exception ex) + { + AppLog.Write($"RTSS: não foi possível atualizar o contador de FPS: {ex.Message}"); + return false; + } + } + + private static unsafe bool IsOwner(byte* entry, byte[] owner) => + new ReadOnlySpan(entry + EntryOwner, owner.Length + 1).SequenceEqual([.. owner, (byte)0]); + private bool Backup(ConsoleRuntimeState state) { if (!AppPaths.HasRtssCli || !EnsureRunning()) return false; diff --git a/src/ConsoleMode/SessionMenuWindow.xaml b/src/ConsoleMode/SessionMenuWindow.xaml index 02f7eb3..aefd5a1 100644 --- a/src/ConsoleMode/SessionMenuWindow.xaml +++ b/src/ConsoleMode/SessionMenuWindow.xaml @@ -237,6 +237,18 @@ + +