mirror of
https://github.com/par274/sharpemu.git
synced 2026-08-03 16:39:51 +08:00
490 lines
17 KiB
C#
490 lines
17 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.VisualTree;
|
|
using SharpEmu.Libs.VideoOut;
|
|
|
|
namespace SharpEmu.GUI;
|
|
|
|
/// <summary>Inline per-game settings navigation, loading and persistence.</summary>
|
|
public partial class MainWindow
|
|
{
|
|
private static readonly string[] GameEnvironmentToggleNames =
|
|
[
|
|
"SHARPEMU_BTHID_UNAVAILABLE",
|
|
"SHARPEMU_DISABLE_IMPORT_LOOP_GUARD",
|
|
"SHARPEMU_WRITABLE_APP0",
|
|
"SHARPEMU_VK_VALIDATION",
|
|
"SHARPEMU_DUMP_SPIRV",
|
|
"SHARPEMU_LOG_DIRECT_MEMORY",
|
|
"SHARPEMU_LOG_IO",
|
|
"SHARPEMU_LOG_NP",
|
|
"SHARPEMU_GUEST_IMAGE_CPU_SYNC",
|
|
];
|
|
|
|
private readonly List<string> _gameEnvironmentPassthrough = new();
|
|
private IReadOnlyList<HostDisplayOption> _gameHostDisplays = [];
|
|
private bool _isGameSettingsOpen;
|
|
private bool _isLoadingGameSettings;
|
|
private bool _updatingGameHostDisplayOptions;
|
|
private int _gameOptionsIndicatorIndex;
|
|
private int _gameOptionsSectionIndex;
|
|
private string? _gameSettingsTitleId;
|
|
|
|
private void WireGameOptions()
|
|
{
|
|
GameSettingsButton.Click += (_, _) => OpenSelectedGameSettings();
|
|
|
|
GameLogLevelBox.ItemsSource = _logLevelChoices;
|
|
GameWindowModeBox.ItemsSource = _windowModeChoices;
|
|
GameScalingModeBox.ItemsSource = _scalingModeChoices;
|
|
GameHdrModeBox.ItemsSource = _hdrModeChoices;
|
|
|
|
var navigationButtons = GameOptionsNavigationButtons();
|
|
for (var index = 0; index < navigationButtons.Length; index++)
|
|
{
|
|
var section = index;
|
|
navigationButtons[index].Click += (_, _) =>
|
|
{
|
|
if (section < GameOptionsSectionPanels().Length)
|
|
{
|
|
SetGameOptionsSection(section);
|
|
}
|
|
else
|
|
{
|
|
CloseGameSettings();
|
|
}
|
|
};
|
|
navigationButtons[index].PointerEntered += (_, _) =>
|
|
{
|
|
if (_isGameSettingsOpen)
|
|
{
|
|
SetGameOptionsNavigationIndicator(section);
|
|
}
|
|
};
|
|
navigationButtons[index].GotFocus += (_, _) =>
|
|
{
|
|
if (_isGameSettingsOpen)
|
|
{
|
|
SetGameOptionsNavigationIndicator(section);
|
|
}
|
|
};
|
|
}
|
|
|
|
GameOptionsNavHost.PointerExited += (_, _) =>
|
|
{
|
|
if (_isGameSettingsOpen)
|
|
{
|
|
SetGameOptionsNavigationIndicator(_gameOptionsSectionIndex);
|
|
}
|
|
};
|
|
|
|
GameOptionsLaunchButton.Click += (_, _) =>
|
|
{
|
|
CloseGameSettings();
|
|
LaunchSelected();
|
|
};
|
|
GameOptionsCloseButton.Click += (_, _) => CloseGameSettings();
|
|
GameOptionsOpenFolderButton.Click += (_, _) => OpenSelectedGameFolder();
|
|
GameOptionsCopyPathButton.Click += async (_, _) =>
|
|
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.Path);
|
|
GameOptionsCopyTitleIdButton.Click += async (_, _) =>
|
|
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.TitleId);
|
|
GameOptionsRemoveButton.Click += (_, _) =>
|
|
{
|
|
CloseGameSettings();
|
|
RemoveSelectedFromLibrary();
|
|
};
|
|
|
|
GameStrictToggle.IsCheckedChanged += (_, _) => PersistOpenGameSettings();
|
|
GameLogLevelBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
|
|
GameTraceImportsBox.ValueChanged += (_, _) => PersistOpenGameSettings();
|
|
GameLogToFileToggle.IsCheckedChanged += (_, _) => PersistOpenGameSettings();
|
|
GameWindowModeBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
|
|
GameDisplayBox.SelectionChanged += (_, _) => OnGameHostDisplayChanged();
|
|
GameResolutionBox.SelectionChanged += (_, _) => OnGameHostResolutionChanged();
|
|
GameRefreshRateBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
|
|
GameScalingModeBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
|
|
GameVSyncToggle.IsCheckedChanged += (_, _) => PersistOpenGameSettings();
|
|
GameHdrModeBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
|
|
foreach (var (_, toggle) in GameEnvironmentToggles())
|
|
{
|
|
toggle.IsCheckedChanged += (_, _) => PersistOpenGameSettings();
|
|
}
|
|
|
|
SetGameOptionsSection(0, animateIndicator: false);
|
|
}
|
|
|
|
private void OpenSelectedGameSettings()
|
|
{
|
|
if (GameList.SelectedItem is not GameEntry game)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(game.TitleId))
|
|
{
|
|
AppendConsoleLine(
|
|
"[GUI][WARN] Per-game settings require a title ID, which this game does not have.",
|
|
WarningLineBrush);
|
|
return;
|
|
}
|
|
|
|
_gameSettingsTitleId = game.TitleId;
|
|
GameOptionsOverlay.DataContext = game;
|
|
LoadGameSettings(game.TitleId);
|
|
SetGameOptionsSection(0, animateIndicator: false);
|
|
GameOptionsLaunchButton.IsEnabled = !_isRunning;
|
|
GameOptionsCopyTitleIdButton.IsEnabled =
|
|
!string.IsNullOrWhiteSpace(game.TitleId);
|
|
|
|
_isGameSettingsOpen = true;
|
|
SetGameOptionsPagesSpan(coversConsoleRow: true);
|
|
SetGameOptionsOpenClass(BackdropLayer, active: true);
|
|
SetGameOptionsOpenClass(CarouselHost, active: true);
|
|
SetGameOptionsOpenClass(LibrarySelectedDetails, active: true);
|
|
SetGameOptionsOpenClass(GameOptionsOverlay, active: true);
|
|
GameList.IsHitTestVisible = false;
|
|
LibraryToolbar.IsHitTestVisible = false;
|
|
GameOptionsOverlay.IsHitTestVisible = true;
|
|
GameOptionsGeneralNav.Focus();
|
|
}
|
|
|
|
private void CloseGameSettings(bool restoreLibrary = true)
|
|
{
|
|
if (!_isGameSettingsOpen)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_isGameSettingsOpen = false;
|
|
SetGameOptionsPagesSpan(coversConsoleRow: false);
|
|
SetGameOptionsNavigationIndicator(_gameOptionsIndicatorIndex, animate: false);
|
|
_gameSettingsTitleId = null;
|
|
_gameEnvironmentPassthrough.Clear();
|
|
SetGameOptionsOpenClass(BackdropLayer, active: false);
|
|
SetGameOptionsOpenClass(CarouselHost, active: false);
|
|
SetGameOptionsOpenClass(LibrarySelectedDetails, active: false);
|
|
SetGameOptionsOpenClass(GameOptionsOverlay, active: false);
|
|
GameOptionsOverlay.IsHitTestVisible = false;
|
|
GameOptionsOverlay.DataContext = null;
|
|
GameList.IsHitTestVisible = true;
|
|
LibraryToolbar.IsHitTestVisible = true;
|
|
|
|
if (restoreLibrary && _activePageIndex == 0)
|
|
{
|
|
GameList.Focus();
|
|
}
|
|
}
|
|
|
|
private void LoadGameSettings(string titleId)
|
|
{
|
|
var effective = EffectiveLaunchSettings.Resolve(
|
|
_settings,
|
|
PerGameSettings.Load(titleId));
|
|
|
|
_isLoadingGameSettings = true;
|
|
_updatingGameHostDisplayOptions = true;
|
|
try
|
|
{
|
|
GameStrictToggle.IsChecked = effective.StrictDynlibResolution;
|
|
GameLogLevelBox.SelectedItem = FindChoice(
|
|
_logLevelChoices,
|
|
effective.LogLevel,
|
|
"Info");
|
|
GameTraceImportsBox.Value = Math.Clamp(effective.ImportTraceLimit, 0, 4096);
|
|
GameLogToFileToggle.IsChecked = effective.LogToFile;
|
|
GameWindowModeBox.SelectedItem = FindChoice(
|
|
_windowModeChoices,
|
|
effective.WindowMode,
|
|
"Windowed");
|
|
GameScalingModeBox.SelectedItem = FindChoice(
|
|
_scalingModeChoices,
|
|
effective.ScalingMode,
|
|
"Fit");
|
|
GameVSyncToggle.IsChecked = effective.VSync;
|
|
GameHdrModeBox.SelectedItem = FindChoice(
|
|
_hdrModeChoices,
|
|
effective.HdrMode,
|
|
"Auto");
|
|
|
|
_gameHostDisplays = HostDisplayOptions.BuildDisplays(
|
|
HostDisplayCatalog.Query(),
|
|
effective.DisplayIndex);
|
|
GameDisplayBox.ItemsSource = _gameHostDisplays;
|
|
var display = HostDisplayOptions.SelectDisplay(
|
|
_gameHostDisplays,
|
|
effective.DisplayIndex);
|
|
GameDisplayBox.SelectedItem = display;
|
|
PopulateGameHostModes(
|
|
display,
|
|
effective.Resolution,
|
|
effective.RefreshRate);
|
|
|
|
_gameEnvironmentPassthrough.Clear();
|
|
foreach (var entry in effective.EnvironmentToggles)
|
|
{
|
|
if (!IsKnownGameEnvironmentEntry(entry))
|
|
{
|
|
_gameEnvironmentPassthrough.Add(entry);
|
|
}
|
|
}
|
|
|
|
foreach (var (name, toggle) in GameEnvironmentToggles())
|
|
{
|
|
toggle.IsChecked = IsEnvironmentEnabled(
|
|
effective.EnvironmentToggles,
|
|
name);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_updatingGameHostDisplayOptions = false;
|
|
_isLoadingGameSettings = false;
|
|
}
|
|
}
|
|
|
|
private void PersistOpenGameSettings()
|
|
{
|
|
if (!_isGameSettingsOpen ||
|
|
_isLoadingGameSettings ||
|
|
_updatingGameHostDisplayOptions ||
|
|
string.IsNullOrWhiteSpace(_gameSettingsTitleId))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var settings = new PerGameSettings
|
|
{
|
|
LogLevel = SelectedComboText(GameLogLevelBox, "Info"),
|
|
ImportTraceLimit = (int)(GameTraceImportsBox.Value ?? 0),
|
|
StrictDynlibResolution = GameStrictToggle.IsChecked == true,
|
|
LogToFile = GameLogToFileToggle.IsChecked == true,
|
|
WindowMode = SelectedComboText(GameWindowModeBox, "Windowed"),
|
|
Resolution = SelectedComboText(GameResolutionBox, "1920x1080"),
|
|
DisplayIndex = GameDisplayBox.SelectedItem is HostDisplayOption display
|
|
? display.Index
|
|
: 0,
|
|
RefreshRate = SelectedGameRefreshRate(),
|
|
ScalingMode = SelectedComboText(GameScalingModeBox, "Fit"),
|
|
VSync = GameVSyncToggle.IsChecked == true,
|
|
HdrMode = SelectedComboText(GameHdrModeBox, "Auto"),
|
|
EnvironmentToggles = BuildGameEnvironmentEntries(),
|
|
};
|
|
settings.RemoveInheritedValues(_settings);
|
|
settings.Save(_gameSettingsTitleId);
|
|
}
|
|
|
|
private void OnGameHostDisplayChanged()
|
|
{
|
|
if (_isLoadingGameSettings ||
|
|
_updatingGameHostDisplayOptions ||
|
|
GameDisplayBox.SelectedItem is not HostDisplayOption display)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_updatingGameHostDisplayOptions = true;
|
|
try
|
|
{
|
|
PopulateGameHostModes(
|
|
display,
|
|
GameResolutionBox.SelectedItem as string ?? "1920x1080",
|
|
SelectedGameRefreshRate());
|
|
}
|
|
finally
|
|
{
|
|
_updatingGameHostDisplayOptions = false;
|
|
}
|
|
|
|
PersistOpenGameSettings();
|
|
}
|
|
|
|
private void OnGameHostResolutionChanged()
|
|
{
|
|
if (_isLoadingGameSettings ||
|
|
_updatingGameHostDisplayOptions ||
|
|
GameDisplayBox.SelectedItem is not HostDisplayOption display)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_updatingGameHostDisplayOptions = true;
|
|
try
|
|
{
|
|
PopulateGameRefreshRates(
|
|
display,
|
|
GameResolutionBox.SelectedItem as string,
|
|
SelectedGameRefreshRate());
|
|
}
|
|
finally
|
|
{
|
|
_updatingGameHostDisplayOptions = false;
|
|
}
|
|
|
|
PersistOpenGameSettings();
|
|
}
|
|
|
|
private void PopulateGameHostModes(
|
|
HostDisplayOption display,
|
|
string selectedResolution,
|
|
int selectedRefreshRate)
|
|
{
|
|
var resolutions = HostDisplayOptions.BuildResolutions(display, selectedResolution);
|
|
GameResolutionBox.ItemsSource = resolutions;
|
|
GameResolutionBox.SelectedItem = resolutions.FirstOrDefault(resolution =>
|
|
string.Equals(resolution, selectedResolution, StringComparison.OrdinalIgnoreCase))
|
|
?? resolutions[0];
|
|
PopulateGameRefreshRates(
|
|
display,
|
|
GameResolutionBox.SelectedItem as string,
|
|
selectedRefreshRate);
|
|
}
|
|
|
|
private void PopulateGameRefreshRates(
|
|
HostDisplayOption display,
|
|
string? resolution,
|
|
int selectedRefreshRate)
|
|
{
|
|
var refreshRates = HostDisplayOptions.BuildRefreshRates(
|
|
display,
|
|
resolution,
|
|
selectedRefreshRate,
|
|
Localization.Instance.Get("Options.RefreshRate.Automatic"));
|
|
GameRefreshRateBox.ItemsSource = refreshRates;
|
|
GameRefreshRateBox.SelectedItem = refreshRates.FirstOrDefault(
|
|
refreshRate => refreshRate.Value == selectedRefreshRate) ?? refreshRates[0];
|
|
}
|
|
|
|
private int SelectedGameRefreshRate() =>
|
|
GameRefreshRateBox.SelectedItem is HostRefreshRateOption refreshRate
|
|
? refreshRate.Value
|
|
: 0;
|
|
|
|
private List<string> BuildGameEnvironmentEntries()
|
|
{
|
|
var entries = new List<string>(_gameEnvironmentPassthrough);
|
|
foreach (var (name, toggle) in GameEnvironmentToggles())
|
|
{
|
|
if (toggle.IsChecked == true)
|
|
{
|
|
entries.Add(name);
|
|
}
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
private static LocalizedChoice FindChoice(
|
|
IEnumerable<LocalizedChoice> choices,
|
|
string value,
|
|
string fallback) =>
|
|
choices.FirstOrDefault(choice =>
|
|
string.Equals(choice.Value, value, StringComparison.OrdinalIgnoreCase))
|
|
?? choices.First(choice =>
|
|
string.Equals(choice.Value, fallback, StringComparison.OrdinalIgnoreCase));
|
|
|
|
private static bool IsEnvironmentEnabled(
|
|
IEnumerable<string> entries,
|
|
string name)
|
|
{
|
|
foreach (var entry in entries)
|
|
{
|
|
var parts = entry.Split('=', 2, StringSplitOptions.TrimEntries);
|
|
if (!string.Equals(parts[0], name, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
return parts.Length == 1 || parts[1] != "0";
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool IsKnownGameEnvironmentEntry(string entry)
|
|
{
|
|
var name = entry.Split('=', 2, StringSplitOptions.TrimEntries)[0];
|
|
return GameEnvironmentToggleNames.Contains(name, StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private void SetGameOptionsSection(int section, bool animateIndicator = true)
|
|
{
|
|
var buttons = GameOptionsSectionButtons();
|
|
var panels = GameOptionsSectionPanels();
|
|
section = Math.Clamp(section, 0, buttons.Length - 1);
|
|
_gameOptionsSectionIndex = section;
|
|
SetGameOptionsNavigationIndicator(section, animateIndicator);
|
|
|
|
for (var index = 0; index < buttons.Length; index++)
|
|
{
|
|
var active = index == section;
|
|
SetActiveClass(buttons[index], active);
|
|
SetOptionsPanelInteraction(panels[index], active);
|
|
}
|
|
|
|
SetActiveClass(GameOptionsBackNav, active: false);
|
|
}
|
|
|
|
private void SetGameOptionsNavigationIndicator(int section, bool animate = true)
|
|
{
|
|
var buttons = GameOptionsNavigationButtons();
|
|
_gameOptionsIndicatorIndex = Math.Clamp(section, 0, buttons.Length - 1);
|
|
var button = buttons[_gameOptionsIndicatorIndex];
|
|
MoveNavigationIndicator(
|
|
GameOptionsNavIndicator,
|
|
GameOptionsNavHost,
|
|
button,
|
|
_gameOptionsIndicatorIndex,
|
|
animate);
|
|
}
|
|
|
|
private void SetGameOptionsPagesSpan(bool coversConsoleRow)
|
|
{
|
|
Grid.SetRowSpan(PagesHost, coversConsoleRow ? 2 : 1);
|
|
}
|
|
|
|
private Button[] GameOptionsNavigationButtons() =>
|
|
[
|
|
GameOptionsGeneralNav,
|
|
GameOptionsLoggingNav,
|
|
GameOptionsRenderingNav,
|
|
GameOptionsEnvironmentNav,
|
|
GameOptionsBackNav,
|
|
];
|
|
|
|
private Button[] GameOptionsSectionButtons() =>
|
|
[
|
|
GameOptionsGeneralNav,
|
|
GameOptionsLoggingNav,
|
|
GameOptionsRenderingNav,
|
|
GameOptionsEnvironmentNav,
|
|
];
|
|
|
|
private Control[] GameOptionsSectionPanels() =>
|
|
[
|
|
GameOptionsGeneralPanel,
|
|
GameOptionsLoggingPanel,
|
|
GameOptionsRenderingPanel,
|
|
GameOptionsEnvironmentPanel,
|
|
];
|
|
|
|
private (string Name, ToggleSwitch Toggle)[] GameEnvironmentToggles() =>
|
|
[
|
|
("SHARPEMU_BTHID_UNAVAILABLE", GameEnvBthidToggle),
|
|
("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD", GameEnvLoopGuardToggle),
|
|
("SHARPEMU_WRITABLE_APP0", GameEnvWritableApp0Toggle),
|
|
("SHARPEMU_VK_VALIDATION", GameEnvVkValidationToggle),
|
|
("SHARPEMU_DUMP_SPIRV", GameEnvDumpSpirvToggle),
|
|
("SHARPEMU_LOG_DIRECT_MEMORY", GameEnvLogDirectMemoryToggle),
|
|
("SHARPEMU_LOG_IO", GameEnvLogIoToggle),
|
|
("SHARPEMU_LOG_NP", GameEnvLogNpToggle),
|
|
("SHARPEMU_GUEST_IMAGE_CPU_SYNC", GameEnvGuestImageCpuSyncToggle),
|
|
];
|
|
|
|
private static void SetGameOptionsOpenClass(Control control, bool active) =>
|
|
SetClass(control, "gameOptionsOpen", active);
|
|
}
|