Sdl backend (#670)

* [audio] added sdl audio backend and in-tree atrac9 decoder

* [input] replaced per-platform pad readers with sdl gamepad input

* [video] added sdl window and host display plumbing

* [gui] added host display options and per-game render settings

* [bink] synced host movie playback to the guest audio clock

* [cpu] hooked windows write faults into guest image tracking

* [perf] added guest and render profiling, reserved host cpu lanes

* [kernel] fixed stale pthread mutex handle alias

* [host] wired the sdl session, save-data paths and project references

* [audio] hoisted ajm trace stackalloc out of its loop

* [video] Add guest image sync setting

* [build] Strip native symbols

* reuse
This commit is contained in:
Berk
2026-07-28 03:33:26 +03:00
committed by GitHub
parent b4cc5f88ca
commit 2b6bd5a532
111 changed files with 9846 additions and 4479 deletions
+238 -10
View File
@@ -5,6 +5,7 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
using SharpEmu.Libs.VideoOut;
namespace SharpEmu.GUI;
@@ -12,6 +13,9 @@ public sealed class PerGameSettingsDialog : Window
{
private static readonly string[] LogLevels =
{ "Trace", "Debug", "Info", "Warning", "Error", "Critical" };
private static readonly string[] WindowModes = { "Windowed", "Borderless", "Exclusive" };
private static readonly string[] ScalingModes = { "Fit", "Cover", "Stretch", "Integer" };
private static readonly string[] HdrModes = { "Auto", "On", "Off" };
private static readonly string[] EnvToggles =
{
@@ -23,9 +27,12 @@ public sealed class PerGameSettingsDialog : Window
"SHARPEMU_LOG_DIRECT_MEMORY",
"SHARPEMU_LOG_IO",
"SHARPEMU_LOG_NP",
"SHARPEMU_GUEST_IMAGE_CPU_SYNC",
};
private readonly string _titleId;
private IReadOnlyList<HostDisplayOption> _hostDisplays = [];
private bool _updatingHostDisplayOptions;
private readonly SettingRow _logLevelRow;
private readonly ComboBox _logLevel = new() { ItemsSource = LogLevels, Width = 160 };
@@ -42,6 +49,27 @@ public sealed class PerGameSettingsDialog : Window
private readonly SettingRow _logToFileRow;
private readonly ToggleSwitch _logToFile = new();
private readonly SettingRow _windowModeRow;
private readonly ComboBox _windowMode = new() { ItemsSource = WindowModes, Width = 160 };
private readonly SettingRow _resolutionRow;
private readonly ComboBox _resolution = new() { Width = 160 };
private readonly SettingRow _displayIndexRow;
private readonly ComboBox _displayIndex = new() { Width = 240 };
private readonly SettingRow _refreshRateRow;
private readonly ComboBox _refreshRate = new() { Width = 160 };
private readonly SettingRow _scalingModeRow;
private readonly ComboBox _scalingMode = new() { ItemsSource = ScalingModes, Width = 160 };
private readonly SettingRow _vsyncRow;
private readonly ToggleSwitch _vsync = new();
private readonly SettingRow _hdrModeRow;
private readonly ComboBox _hdrMode = new() { ItemsSource = HdrModes, Width = 160 };
private readonly SettingRow _envRow;
private readonly StackPanel _envList = new() { Orientation = Orientation.Vertical, Spacing = 8, Margin = new(0, 4, 0, 0) };
private readonly List<(string Name, ToggleSwitch Box)> _envBoxes = new();
@@ -60,13 +88,20 @@ public sealed class PerGameSettingsDialog : Window
Background = new SolidColorBrush(Color.Parse("#0D1017"));
_strict.OnContent = _logToFile.OnContent = loc.Get("Common.On");
_strict.OffContent = _logToFile.OffContent = loc.Get("Common.Off");
_strict.OnContent = _logToFile.OnContent = _vsync.OnContent = loc.Get("Common.On");
_strict.OffContent = _logToFile.OffContent = _vsync.OffContent = loc.Get("Common.Off");
_logLevelRow = Row(loc.Get("Options.LogLevel.Label"), loc.Get("Options.LogLevel.Desc"), _logLevel);
_traceRow = Row(loc.Get("Options.TraceImports.Label"), loc.Get("Options.TraceImports.Desc"), _trace);
_strictRow = Row(loc.Get("Options.Strict.Label"), loc.Get("Options.Strict.Desc"), _strict);
_logToFileRow = Row(loc.Get("Options.LogToFile.Label"), loc.Get("Options.LogToFile.Desc"), _logToFile);
_windowModeRow = Row(loc.Get("Options.WindowMode.Label"), loc.Get("Options.WindowMode.Desc"), _windowMode);
_resolutionRow = Row(loc.Get("Options.Resolution.Label"), loc.Get("Options.Resolution.Desc"), _resolution);
_displayIndexRow = Row(loc.Get("Options.Display.Label"), loc.Get("Options.Display.Desc"), _displayIndex);
_refreshRateRow = Row(loc.Get("Options.RefreshRate.Label"), loc.Get("Options.RefreshRate.Desc"), _refreshRate);
_scalingModeRow = Row(loc.Get("Options.Scaling.Label"), loc.Get("Options.Scaling.Desc"), _scalingMode);
_vsyncRow = Row(loc.Get("Options.VSync.Label"), loc.Get("Options.VSync.Desc"), _vsync);
_hdrModeRow = Row(loc.Get("Options.Hdr.Label"), loc.Get("Options.Hdr.Desc"), _hdrMode);
_envRow = new SettingRow
{
Label = loc.Get("PerGame.EnvToggles.Label"),
@@ -81,6 +116,22 @@ public sealed class PerGameSettingsDialog : Window
_envList.Children.Add(box);
}
var general = new StackPanel { Orientation = Orientation.Vertical, Spacing = 12, Margin = new(0, 12, 0, 0) };
general.Children.Add(Card(loc.Get("Options.Section.Emulation"), _strictRow));
general.Children.Add(Card(loc.Get("Options.Section.Logging"), _logLevelRow, _traceRow, _logToFileRow));
general.Children.Add(Card(loc.Get("Options.Section.Environment"), _envRow, _envList));
var graphics = new StackPanel { Orientation = Orientation.Vertical, Spacing = 12, Margin = new(0, 12, 0, 0) };
graphics.Children.Add(Card(
loc.Get("Options.Section.Display"),
_windowModeRow,
_resolutionRow,
_displayIndexRow,
_refreshRateRow,
_scalingModeRow,
_vsyncRow,
_hdrModeRow));
var content = new StackPanel { Orientation = Orientation.Vertical, Spacing = 12, Margin = new(16) };
content.Children.Add(new TextBlock
{
@@ -88,9 +139,14 @@ public sealed class PerGameSettingsDialog : Window
Foreground = new SolidColorBrush(Color.Parse("#8B94A7")),
FontSize = 12,
});
content.Children.Add(Card(loc.Get("Options.Section.Emulation"), _strictRow));
content.Children.Add(Card(loc.Get("Options.Section.Logging"), _logLevelRow, _traceRow, _logToFileRow));
content.Children.Add(Card(loc.Get("Options.Section.Environment"), _envRow, _envList));
content.Children.Add(new TabControl
{
ItemsSource = new[]
{
new TabItem { Header = loc.Get("PerGame.Tab.General"), Content = general },
new TabItem { Header = loc.Get("PerGame.Tab.Graphics"), Content = graphics },
},
});
var save = new Button { Content = loc.Get("Common.Save"), Classes = { "accent" } };
var cancel = new Button { Content = loc.Get("Common.Cancel"), Classes = { "ghost" } };
@@ -119,6 +175,8 @@ public sealed class PerGameSettingsDialog : Window
root.Children.Add(buttonBar);
Content = root;
_displayIndex.SelectionChanged += (_, _) => OnHostDisplayChanged();
_resolution.SelectionChanged += (_, _) => OnHostResolutionChanged();
LoadValues(global);
_envRow.PropertyChanged += (_, e) =>
{
@@ -154,16 +212,38 @@ public sealed class PerGameSettingsDialog : Window
private void LoadValues(GuiSettings global)
{
var existing = PerGameSettings.Load(_titleId);
var displayIndex = Math.Max(0, existing?.DisplayIndex ?? global.DisplayIndex);
var resolution = existing?.Resolution ?? global.Resolution;
var refreshRate = Math.Clamp(existing?.RefreshRate ?? global.RefreshRate, 0, 1000);
_updatingHostDisplayOptions = true;
try
{
_hostDisplays = HostDisplayOptions.BuildDisplays(HostDisplayCatalog.Query(), displayIndex);
_displayIndex.ItemsSource = _hostDisplays;
var display = HostDisplayOptions.SelectDisplay(_hostDisplays, displayIndex);
_displayIndex.SelectedItem = display;
PopulateHostModes(display, resolution, refreshRate);
}
finally
{
_updatingHostDisplayOptions = false;
}
_logLevel.SelectedItem = Array.IndexOf(LogLevels, global.LogLevel) >= 0 ? global.LogLevel : "Info";
_trace.Value = global.ImportTraceLimit;
_strict.IsChecked = global.StrictDynlibResolution;
_logToFile.IsChecked = global.LogToFile;
_windowMode.SelectedItem = ChoiceOrDefault(WindowModes, global.WindowMode, "Windowed");
_scalingMode.SelectedItem = ChoiceOrDefault(ScalingModes, global.ScalingMode, "Fit");
_vsync.IsChecked = global.VSync;
_hdrMode.SelectedItem = ChoiceOrDefault(HdrModes, global.HdrMode, "Auto");
foreach (var (name, box) in _envBoxes)
{
box.IsChecked = global.EnvironmentToggles.Contains(name);
box.IsChecked = IsEnvironmentEnabled(global.EnvironmentToggles, name, defaultValue: name == "SHARPEMU_GUEST_IMAGE_CPU_SYNC");
}
var existing = PerGameSettings.Load(_titleId);
if (existing is null)
{
return;
@@ -178,16 +258,120 @@ public sealed class PerGameSettingsDialog : Window
if (existing.ImportTraceLimit is { } t) { _traceRow.IsOverridden = true; _trace.Value = t; }
if (existing.StrictDynlibResolution is { } s) { _strictRow.IsOverridden = true; _strict.IsChecked = s; }
if (existing.LogToFile is { } l) { _logToFileRow.IsOverridden = true; _logToFile.IsChecked = l; }
if (existing.WindowMode is { } windowMode && WindowModes.Contains(windowMode, StringComparer.OrdinalIgnoreCase))
{
_windowModeRow.IsOverridden = true;
_windowMode.SelectedItem = ChoiceOrDefault(WindowModes, windowMode, "Windowed");
}
if (existing.Resolution is not null)
{
_resolutionRow.IsOverridden = true;
}
if (existing.DisplayIndex is not null)
{
_displayIndexRow.IsOverridden = true;
}
if (existing.RefreshRate is not null)
{
_refreshRateRow.IsOverridden = true;
}
if (existing.ScalingMode is { } scalingMode && ScalingModes.Contains(scalingMode, StringComparer.OrdinalIgnoreCase))
{
_scalingModeRow.IsOverridden = true;
_scalingMode.SelectedItem = ChoiceOrDefault(ScalingModes, scalingMode, "Fit");
}
if (existing.VSync is { } vsync)
{
_vsyncRow.IsOverridden = true;
_vsync.IsChecked = vsync;
}
if (existing.HdrMode is { } hdrMode && HdrModes.Contains(hdrMode, StringComparer.OrdinalIgnoreCase))
{
_hdrModeRow.IsOverridden = true;
_hdrMode.SelectedItem = ChoiceOrDefault(HdrModes, hdrMode, "Auto");
}
if (existing.EnvironmentToggles is { } env)
{
_envRow.IsOverridden = true;
foreach (var (name, box) in _envBoxes)
{
box.IsChecked = env.Contains(name);
box.IsChecked = IsEnvironmentEnabled(env, name, defaultValue: name == "SHARPEMU_GUEST_IMAGE_CPU_SYNC");
}
}
}
private static string ChoiceOrDefault(string[] choices, string? value, string fallback) =>
choices.FirstOrDefault(choice => string.Equals(choice, value, StringComparison.OrdinalIgnoreCase)) ?? fallback;
private void OnHostDisplayChanged()
{
if (_updatingHostDisplayOptions || _displayIndex.SelectedItem is not HostDisplayOption display)
{
return;
}
_updatingHostDisplayOptions = true;
try
{
PopulateHostModes(
display,
_resolution.SelectedItem as string ?? "1920x1080",
SelectedRefreshRate());
}
finally
{
_updatingHostDisplayOptions = false;
}
}
private void OnHostResolutionChanged()
{
if (_updatingHostDisplayOptions || _displayIndex.SelectedItem is not HostDisplayOption display)
{
return;
}
var selectedRefreshRate = SelectedRefreshRate();
_updatingHostDisplayOptions = true;
try
{
PopulateRefreshRates(display, _resolution.SelectedItem as string, selectedRefreshRate);
}
finally
{
_updatingHostDisplayOptions = false;
}
}
private void PopulateHostModes(
HostDisplayOption display,
string selectedResolution,
int selectedRefreshRate)
{
var resolutions = HostDisplayOptions.BuildResolutions(display, selectedResolution);
_resolution.ItemsSource = resolutions;
_resolution.SelectedItem = resolutions.FirstOrDefault(resolution =>
string.Equals(resolution, selectedResolution, StringComparison.OrdinalIgnoreCase)) ?? resolutions[0];
PopulateRefreshRates(display, _resolution.SelectedItem as string, selectedRefreshRate);
}
private void PopulateRefreshRates(
HostDisplayOption display,
string? resolution,
int selectedRefreshRate)
{
var rates = HostDisplayOptions.BuildRefreshRates(
display,
resolution,
selectedRefreshRate,
Localization.Instance.Get("Options.RefreshRate.Automatic"));
_refreshRate.ItemsSource = rates;
_refreshRate.SelectedItem = rates.FirstOrDefault(rate => rate.Value == selectedRefreshRate) ?? rates[0];
}
private int SelectedRefreshRate() =>
_refreshRate.SelectedItem is HostRefreshRateOption refreshRate ? refreshRate.Value : 0;
private void Persist()
{
var settings = new PerGameSettings
@@ -196,10 +380,54 @@ public sealed class PerGameSettingsDialog : Window
ImportTraceLimit = _traceRow.IsOverridden ? (int)(_trace.Value ?? 0) : null,
StrictDynlibResolution = _strictRow.IsOverridden ? _strict.IsChecked == true : null,
LogToFile = _logToFileRow.IsOverridden ? _logToFile.IsChecked == true : null,
EnvironmentToggles = _envRow.IsOverridden
? _envBoxes.Where(e => e.Box.IsChecked == true).Select(e => e.Name).ToList()
WindowMode = _windowModeRow.IsOverridden ? _windowMode.SelectedItem as string : null,
Resolution = _resolutionRow.IsOverridden ? _resolution.SelectedItem as string : null,
DisplayIndex = _displayIndexRow.IsOverridden && _displayIndex.SelectedItem is HostDisplayOption display
? display.Index
: null,
RefreshRate = _refreshRateRow.IsOverridden ? SelectedRefreshRate() : null,
ScalingMode = _scalingModeRow.IsOverridden ? _scalingMode.SelectedItem as string : null,
VSync = _vsyncRow.IsOverridden ? _vsync.IsChecked == true : null,
HdrMode = _hdrModeRow.IsOverridden ? _hdrMode.SelectedItem as string : null,
EnvironmentToggles = _envRow.IsOverridden ? BuildEnvironmentEntries() : null,
};
settings.Save(_titleId);
}
private List<string> BuildEnvironmentEntries()
{
const string guestImageCpuSync = "SHARPEMU_GUEST_IMAGE_CPU_SYNC";
var entries = _envBoxes
.Where(entry => entry.Name != guestImageCpuSync && entry.Box.IsChecked == true)
.Select(entry => entry.Name)
.ToList();
if (_envBoxes.First(entry => entry.Name == guestImageCpuSync).Box.IsChecked != true)
{
entries.Add(guestImageCpuSync + "=0");
}
return entries;
}
private static bool IsEnvironmentEnabled(
IEnumerable<string> entries,
string name,
bool defaultValue)
{
foreach (var entry in entries)
{
if (string.Equals(entry, name + "=0", StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (string.Equals(entry, name, StringComparison.OrdinalIgnoreCase) ||
string.Equals(entry, name + "=1", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return defaultValue;
}
}