mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-26 04:39:17 +08:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1aa084704a | |||
| 0bd63c5a74 | |||
| 25ea376d41 | |||
| 94463576f8 | |||
| c707f4d2ce | |||
| 0c750ffa61 | |||
| cd27d3824b | |||
| 74c18837f3 | |||
| 6bb7cd5192 | |||
| 7d4b6c1187 | |||
| f90b5fa82c | |||
| bd33ce464e | |||
| 3ac4c7a0bb | |||
| 3983c70475 | |||
| d0e7adec3b | |||
| 30d6e9eda6 | |||
| 919b9d92bb | |||
| 8c0903f297 | |||
| 5594d89cbd |
@@ -63,11 +63,6 @@ internal static partial class Program
|
||||
|
||||
private static int Run(string[] args)
|
||||
{
|
||||
if (Updater.TryApply(args, out var updateExitCode))
|
||||
{
|
||||
return updateExitCode;
|
||||
}
|
||||
|
||||
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
||||
if (args.Length == 0 && !isMitigatedChild)
|
||||
{
|
||||
|
||||
@@ -12,7 +12,6 @@ using SharpEmu.Libs.Kernel;
|
||||
using SharpEmu.Libs.AppContent;
|
||||
using SharpEmu.Libs.SaveData;
|
||||
using SharpEmu.Libs.Fiber;
|
||||
using SharpEmu.Libs.SystemService;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -142,7 +141,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
var image = LoadImage(normalizedEbootPath);
|
||||
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
|
||||
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
|
||||
SystemServiceExports.ConfigureApplicationInfo(image.TitleId);
|
||||
_ = RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
||||
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
|
||||
Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}");
|
||||
|
||||
@@ -23,6 +23,9 @@ public sealed class GuiSettings
|
||||
|
||||
public bool StrictDynlibResolution { get; set; }
|
||||
|
||||
/// <summary>GPU implementation selected for newly launched games.</summary>
|
||||
public string RenderingBackend { get; set; } = "Legacy";
|
||||
|
||||
/// <summary>
|
||||
/// Mirror emulator output to user/logs/<titleId>-<timestamp>.log, if <see cref="LogFilePath"/> is null.
|
||||
/// </summary>
|
||||
@@ -48,8 +51,6 @@ public sealed class GuiSettings
|
||||
/// <summary>Publish launcher/game status to Discord Rich Presence.</summary>
|
||||
public bool DiscordRichPresence { get; set; } = true;
|
||||
|
||||
public bool CheckForUpdatesOnStartup { get; set; } = true;
|
||||
|
||||
/// <summary>Names of SHARPEMU_* switches set to "1" in the emulator's environment at launch.</summary>
|
||||
public List<string> EnvironmentToggles { get; set; } = new();
|
||||
|
||||
|
||||
@@ -31,11 +31,9 @@
|
||||
"Options.Env.Desc": "Switches passed to the emulator as environment variables at launch.",
|
||||
"Options.Env.Bthid.Desc": "Report Bluetooth HID as unavailable for titles whose wheel/FFB middleware polls forever.\nLeave off normally. Some titles freeze when init fails.",
|
||||
"Options.Env.LoopGuard.Desc": "Do not force quit titles that repeat the same call for too long.\nTry this when a game exits on its own while loading.",
|
||||
"Options.Env.WritableApp0.Desc": "Allow titles to create and write files inside their install folder.\nNeeded by unpackaged dumps that write their save or config data under /app0.",
|
||||
"Options.Env.VkValidation.Desc": "Enable Vulkan validation layers for GPU debugging.\nSlow. Requires the Vulkan SDK to be installed.",
|
||||
"Options.Env.DumpSpirv.Desc": "Dump AGC shaders and their SPIR-V translations to the shader-dumps folder.\nUse when reporting shader or rendering bugs.",
|
||||
"Options.Env.LogDirectMemory.Desc": "Log direct memory allocations and failures to the console.\nUse when a game aborts or exits during boot.",
|
||||
"Options.Env.LogIo.Desc": "Log file open, read, and path-resolve activity to the console.\nUse when a game cannot find its data files during boot.",
|
||||
"Options.Env.LogNp.Desc": "Log NP (PlayStation Network) library calls to the console.",
|
||||
"Options.Section.Emulation": "EMULATION",
|
||||
"Options.Section.Logging": "LOGGING",
|
||||
@@ -45,6 +43,12 @@
|
||||
"Options.CpuEngine.Desc": "Execution engine used to run game code.",
|
||||
"Options.CpuEngine.Native": "Native",
|
||||
|
||||
"Options.RenderingBackend.Label": "Rendering backend",
|
||||
"Options.RenderingBackend.Desc": "Graphics implementation used for newly launched games. Native Vulkan requires a separate library.",
|
||||
"Options.RenderingBackend.Native": "Native Vulkan (Experimental)",
|
||||
"Options.RenderingBackend.Legacy": "Silk.NET Vulkan (Default)",
|
||||
"Launch.NativeBackendMissing": "WARNING: Native Vulkan is selected, but {0} was not found next to SharpEmu. Download it from https://github.com/sharpemu/sharpemu.vulkan or select Silk.NET Vulkan in Options.",
|
||||
|
||||
"Options.Strict.Label": "Strict dynlib resolution",
|
||||
"Options.Strict.Desc": "Fail the launch when an imported symbol cannot be resolved.",
|
||||
|
||||
@@ -144,20 +148,5 @@
|
||||
"About.Discord.Label": "Discord",
|
||||
"About.Discord.Desc": "Join the community, get support and follow development.",
|
||||
"About.GithubButton": "Contribute in GitHub!",
|
||||
"About.DiscordButton": "Join our Discord!",
|
||||
|
||||
"Updater.Auto.Label": "Check for updates on startup",
|
||||
"Updater.Auto.Desc": "Checks GitHub without delaying startup.",
|
||||
"Updater.Label": "Updates",
|
||||
"Updater.Check": "Check for updates",
|
||||
"Updater.DownloadRestart": "Download and restart",
|
||||
"Updater.Status.Ready": "Current build: {0}",
|
||||
"Updater.Status.Checking": "Checking for updates…",
|
||||
"Updater.Status.Current": "You are up to date ({0}).",
|
||||
"Updater.Status.Available": "A new build is available: {0}",
|
||||
"Updater.Status.Downloading": "Downloading update… {0}%",
|
||||
"Updater.Status.Installing": "Installing update…",
|
||||
"Updater.Status.Timeout": "Update check timed out after 10 seconds.",
|
||||
"Updater.Status.Failed": "Could not check for updates.",
|
||||
"Updater.Status.Unsupported": "Automatic updating requires a Windows, Linux or macOS x64 build."
|
||||
"About.DiscordButton": "Join our Discord!"
|
||||
}
|
||||
|
||||
@@ -125,20 +125,5 @@
|
||||
"Dialog.PsExecutables": "PS çalıştırılabilirleri",
|
||||
"Dialog.SaveLogFile": "Günlük dosyasının kaydedileceği yeri seçin",
|
||||
"Dialog.PlainTextFiles": "Düz Metin Dosyaları",
|
||||
"Dialog.LogFiles": "Günlük Dosyaları",
|
||||
|
||||
"Updater.Auto.Label": "Açılışta güncellemeleri denetle",
|
||||
"Updater.Auto.Desc": "Açılışı geciktirmeden GitHub'ı denetler.",
|
||||
"Updater.Label": "Güncellemeler",
|
||||
"Updater.Check": "Güncellemeleri denetle",
|
||||
"Updater.DownloadRestart": "İndir ve yeniden başlat",
|
||||
"Updater.Status.Ready": "Mevcut build: {0}",
|
||||
"Updater.Status.Checking": "Güncellemeler denetleniyor…",
|
||||
"Updater.Status.Current": "Güncelsiniz ({0}).",
|
||||
"Updater.Status.Available": "Yeni build mevcut: {0}",
|
||||
"Updater.Status.Downloading": "Güncelleme indiriliyor… %{0}",
|
||||
"Updater.Status.Installing": "Güncelleme kuruluyor…",
|
||||
"Updater.Status.Timeout": "Güncelleme denetimi 10 saniye sonra zaman aşımına uğradı.",
|
||||
"Updater.Status.Failed": "Güncellemeler denetlenemedi.",
|
||||
"Updater.Status.Unsupported": "Otomatik güncelleme Windows, Linux veya macOS x64 build'i gerektirir."
|
||||
"Dialog.LogFiles": "Günlük Dosyaları"
|
||||
}
|
||||
|
||||
@@ -203,6 +203,19 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="RenderingBackendLabel" Text="Rendering backend" FontSize="13" />
|
||||
<TextBlock x:Name="RenderingBackendDesc" Text="Graphics implementation used for newly launched games."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ComboBox Grid.Column="1" x:Name="RenderingBackendBox" Width="200" SelectedIndex="0"
|
||||
VerticalAlignment="Center" CornerRadius="8">
|
||||
<ComboBoxItem x:Name="RenderingBackendLegacyItem" Content="Silk.NET Vulkan (Default)" />
|
||||
<ComboBoxItem x:Name="RenderingBackendNativeItem" Content="Native Vulkan (Experimental)" />
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="StrictLabel" Text="Strict dynlib resolution" FontSize="13" />
|
||||
@@ -315,16 +328,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<ToggleSwitch Grid.Column="1" x:Name="DiscordToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="AutoUpdateLabel" Text="Check for updates on startup" FontSize="13" />
|
||||
<TextBlock x:Name="AutoUpdateDesc" Text="Checks GitHub without delaying startup."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="AutoUpdateToggle" OnContent="On" OffContent="Off"
|
||||
IsChecked="True" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border Classes="card">
|
||||
@@ -332,16 +335,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<TextBlock x:Name="AboutSectionTitle"
|
||||
Classes="sectionTitle"
|
||||
Text="ABOUT" />
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="2" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="UpdateLabel" Text="Updates" FontSize="13" />
|
||||
<TextBlock x:Name="UpdateStatusText" Text="Current build: dev" FontSize="11"
|
||||
Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" x:Name="UpdateButton" Classes="ghost"
|
||||
Content="Check for updates" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<!--Github-->
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
@@ -440,17 +433,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="SHARPEMU_WRITABLE_APP0" FontSize="13" FontFamily="Consolas,monospace" />
|
||||
<TextBlock x:Name="EnvWritableApp0Desc"
|
||||
Text="Allow titles to create and write files inside their install folder. Needed by unpackaged dumps that write their save or config data under /app0."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="EnvWritableApp0Toggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="SHARPEMU_VK_VALIDATION" FontSize="13" FontFamily="Consolas,monospace" />
|
||||
@@ -484,17 +466,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="SHARPEMU_LOG_IO" FontSize="13" FontFamily="Consolas,monospace" />
|
||||
<TextBlock x:Name="EnvLogIoDesc"
|
||||
Text="Log file open, read, and path-resolve activity to the console. Use when a game cannot find its data files during boot."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="EnvLogIoToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="SHARPEMU_LOG_NP" FontSize="13" FontFamily="Consolas,monospace" />
|
||||
|
||||
@@ -57,9 +57,6 @@ public partial class MainWindow : Window
|
||||
private bool _isRunning;
|
||||
private int _autoScrollTicks;
|
||||
private int _activePageIndex;
|
||||
private Updater.UpdateInfo? _availableUpdate;
|
||||
private string _updateStatusKey = "Updater.Status.Ready";
|
||||
private object?[] _updateStatusArgs = [BuildInfo.CommitSha ?? "dev"];
|
||||
|
||||
// Discord Rich Presence state.
|
||||
private readonly long _launcherStartUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
@@ -119,6 +116,8 @@ public partial class MainWindow : Window
|
||||
// The settings page edits _settings live, so a launch started while
|
||||
// it is open already uses the new values.
|
||||
LogLevelBox.SelectionChanged += (_, _) => _settings.LogLevel = SelectedLogLevel();
|
||||
RenderingBackendBox.SelectionChanged += (_, _) =>
|
||||
_settings.RenderingBackend = SelectedRenderingBackend();
|
||||
TraceImportsBox.ValueChanged += (_, _) => _settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
|
||||
StrictToggle.IsCheckedChanged += (_, _) => _settings.StrictDynlibResolution = StrictToggle.IsChecked == true;
|
||||
LogToFileToggle.IsCheckedChanged += (_, _) => _settings.LogToFile = LogToFileToggle.IsChecked == true;
|
||||
@@ -134,24 +133,17 @@ public partial class MainWindow : Window
|
||||
_settings.DiscordRichPresence = DiscordToggle.IsChecked == true;
|
||||
UpdateDiscordPresence();
|
||||
};
|
||||
AutoUpdateToggle.IsCheckedChanged += (_, _) =>
|
||||
_settings.CheckForUpdatesOnStartup = AutoUpdateToggle.IsChecked == true;
|
||||
UpdateButton.Click += async (_, _) => await OnUpdateButtonAsync();
|
||||
SelectLogFilePathButton.Click += async (_, _) => await SelectLogFilePathAsync();
|
||||
EnvBthidToggle.IsCheckedChanged += (_, _) =>
|
||||
SetEnvironmentToggle("SHARPEMU_BTHID_UNAVAILABLE", EnvBthidToggle.IsChecked == true);
|
||||
EnvLoopGuardToggle.IsCheckedChanged += (_, _) =>
|
||||
SetEnvironmentToggle("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD", EnvLoopGuardToggle.IsChecked == true);
|
||||
EnvWritableApp0Toggle.IsCheckedChanged += (_, _) =>
|
||||
SetEnvironmentToggle("SHARPEMU_WRITABLE_APP0", EnvWritableApp0Toggle.IsChecked == true);
|
||||
EnvVkValidationToggle.IsCheckedChanged += (_, _) =>
|
||||
SetEnvironmentToggle("SHARPEMU_VK_VALIDATION", EnvVkValidationToggle.IsChecked == true);
|
||||
EnvDumpSpirvToggle.IsCheckedChanged += (_, _) =>
|
||||
SetEnvironmentToggle("SHARPEMU_DUMP_SPIRV", EnvDumpSpirvToggle.IsChecked == true);
|
||||
EnvLogDirectMemoryToggle.IsCheckedChanged += (_, _) =>
|
||||
SetEnvironmentToggle("SHARPEMU_LOG_DIRECT_MEMORY", EnvLogDirectMemoryToggle.IsChecked == true);
|
||||
EnvLogIoToggle.IsCheckedChanged += (_, _) =>
|
||||
SetEnvironmentToggle("SHARPEMU_LOG_IO", EnvLogIoToggle.IsChecked == true);
|
||||
EnvLogNpToggle.IsCheckedChanged += (_, _) =>
|
||||
SetEnvironmentToggle("SHARPEMU_LOG_NP", EnvLogNpToggle.IsChecked == true);
|
||||
LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
|
||||
@@ -381,10 +373,6 @@ public partial class MainWindow : Window
|
||||
ApplySettingsToControls();
|
||||
LocateEmulator();
|
||||
UpdateDiscordPresence();
|
||||
if (_settings.CheckForUpdatesOnStartup)
|
||||
{
|
||||
_ = CheckForUpdatesAsync();
|
||||
}
|
||||
await RescanLibraryAsync();
|
||||
}
|
||||
|
||||
@@ -441,11 +429,9 @@ public partial class MainWindow : Window
|
||||
EnvDesc.Text = loc.Get("Options.Env.Desc");
|
||||
EnvBthidDesc.Text = loc.Get("Options.Env.Bthid.Desc");
|
||||
EnvLoopGuardDesc.Text = loc.Get("Options.Env.LoopGuard.Desc");
|
||||
EnvWritableApp0Desc.Text = loc.Get("Options.Env.WritableApp0.Desc");
|
||||
EnvVkValidationDesc.Text = loc.Get("Options.Env.VkValidation.Desc");
|
||||
EnvDumpSpirvDesc.Text = loc.Get("Options.Env.DumpSpirv.Desc");
|
||||
EnvLogDirectMemoryDesc.Text = loc.Get("Options.Env.LogDirectMemory.Desc");
|
||||
EnvLogIoDesc.Text = loc.Get("Options.Env.LogIo.Desc");
|
||||
EnvLogNpDesc.Text = loc.Get("Options.Env.LogNp.Desc");
|
||||
EmulationSectionTitle.Text = loc.Get("Options.Section.Emulation");
|
||||
LoggingSectionTitle.Text = loc.Get("Options.Section.Logging");
|
||||
@@ -455,6 +441,11 @@ public partial class MainWindow : Window
|
||||
CpuEngineDesc.Text = loc.Get("Options.CpuEngine.Desc");
|
||||
CpuEngineNativeItem.Content = loc.Get("Options.CpuEngine.Native");
|
||||
|
||||
RenderingBackendLabel.Text = loc.Get("Options.RenderingBackend.Label");
|
||||
RenderingBackendDesc.Text = loc.Get("Options.RenderingBackend.Desc");
|
||||
RenderingBackendNativeItem.Content = loc.Get("Options.RenderingBackend.Native");
|
||||
RenderingBackendLegacyItem.Content = loc.Get("Options.RenderingBackend.Legacy");
|
||||
|
||||
StrictLabel.Text = loc.Get("Options.Strict.Label");
|
||||
StrictDesc.Text = loc.Get("Options.Strict.Desc");
|
||||
|
||||
@@ -488,10 +479,8 @@ public partial class MainWindow : Window
|
||||
|
||||
DiscordLabel.Text = loc.Get("Options.Discord.Label");
|
||||
DiscordDesc.Text = loc.Get("Options.Discord.Desc");
|
||||
AutoUpdateLabel.Text = loc.Get("Updater.Auto.Label");
|
||||
AutoUpdateDesc.Text = loc.Get("Updater.Auto.Desc");
|
||||
|
||||
foreach (var toggle in new[] { StrictToggle, LogToFileToggle, OverrideLogFileToggle, TitleMusicToggle, DiscordToggle, AutoUpdateToggle })
|
||||
foreach (var toggle in new[] { StrictToggle, LogToFileToggle, OverrideLogFileToggle, TitleMusicToggle, DiscordToggle })
|
||||
{
|
||||
toggle.OnContent = loc.Get("Common.On");
|
||||
toggle.OffContent = loc.Get("Common.Off");
|
||||
@@ -515,8 +504,6 @@ public partial class MainWindow : Window
|
||||
DiscordServerDesc.Text = loc.Get("About.Discord.Desc");
|
||||
GithubButton.Content = loc.Get("About.GithubButton");
|
||||
DiscordButton.Content = loc.Get("About.DiscordButton");
|
||||
UpdateLabel.Text = loc.Get("Updater.Label");
|
||||
RefreshUpdateText();
|
||||
|
||||
UpdateEmptyStateTexts();
|
||||
UpdateSelectedGameTexts();
|
||||
@@ -616,6 +603,10 @@ public partial class MainWindow : Window
|
||||
|
||||
private void ApplySettingsToControls()
|
||||
{
|
||||
RenderingBackendBox.SelectedIndex = string.Equals(
|
||||
_settings.RenderingBackend,
|
||||
"Native",
|
||||
StringComparison.OrdinalIgnoreCase) ? 1 : 0;
|
||||
LogLevelBox.SelectedIndex = _settings.LogLevel.ToLowerInvariant() switch
|
||||
{
|
||||
"trace" => 0,
|
||||
@@ -632,87 +623,15 @@ public partial class MainWindow : Window
|
||||
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
|
||||
TitleMusicToggle.IsChecked = _settings.PlayTitleMusic;
|
||||
DiscordToggle.IsChecked = _settings.DiscordRichPresence;
|
||||
AutoUpdateToggle.IsChecked = _settings.CheckForUpdatesOnStartup;
|
||||
EnvBthidToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_BTHID_UNAVAILABLE");
|
||||
EnvLoopGuardToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD");
|
||||
EnvWritableApp0Toggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_WRITABLE_APP0");
|
||||
EnvVkValidationToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_VK_VALIDATION");
|
||||
EnvDumpSpirvToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_DUMP_SPIRV");
|
||||
EnvLogDirectMemoryToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_DIRECT_MEMORY");
|
||||
EnvLogIoToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_IO");
|
||||
EnvLogNpToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_NP");
|
||||
UpdateLogFilePathText();
|
||||
}
|
||||
|
||||
private async Task OnUpdateButtonAsync()
|
||||
{
|
||||
if (_availableUpdate is null)
|
||||
{
|
||||
await CheckForUpdatesAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateButton.IsEnabled = false;
|
||||
try
|
||||
{
|
||||
var progress = new Progress<int>(value =>
|
||||
SetUpdateStatus("Updater.Status.Downloading", value));
|
||||
await Updater.DownloadAndRestartAsync(_availableUpdate, progress);
|
||||
SetUpdateStatus("Updater.Status.Installing");
|
||||
Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
SetUpdateStatus("Updater.Status.Failed");
|
||||
UpdateButton.IsEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CheckForUpdatesAsync()
|
||||
{
|
||||
_availableUpdate = null;
|
||||
UpdateButton.IsEnabled = false;
|
||||
SetUpdateStatus("Updater.Status.Checking");
|
||||
try
|
||||
{
|
||||
_availableUpdate = await Updater.CheckAsync(BuildInfo.CommitSha);
|
||||
SetUpdateStatus(
|
||||
_availableUpdate is null ? "Updater.Status.Current" : "Updater.Status.Available",
|
||||
_availableUpdate?.Sha ?? BuildInfo.CommitSha ?? "dev");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
SetUpdateStatus("Updater.Status.Timeout");
|
||||
}
|
||||
catch (PlatformNotSupportedException)
|
||||
{
|
||||
SetUpdateStatus("Updater.Status.Unsupported");
|
||||
}
|
||||
catch
|
||||
{
|
||||
SetUpdateStatus("Updater.Status.Failed");
|
||||
}
|
||||
finally
|
||||
{
|
||||
UpdateButton.IsEnabled = true;
|
||||
RefreshUpdateText();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetUpdateStatus(string key, params object?[] args)
|
||||
{
|
||||
_updateStatusKey = key;
|
||||
_updateStatusArgs = args;
|
||||
RefreshUpdateText();
|
||||
}
|
||||
|
||||
private void RefreshUpdateText()
|
||||
{
|
||||
UpdateStatusText.Text = Localization.Instance.Format(_updateStatusKey, _updateStatusArgs);
|
||||
UpdateButton.Content = Localization.Instance.Get(
|
||||
_availableUpdate is null ? "Updater.Check" : "Updater.DownloadRestart");
|
||||
}
|
||||
|
||||
// Environment variables set on this process at the previous launch; children
|
||||
// inherit the process environment, so stale names must be cleared explicitly.
|
||||
private readonly HashSet<string> _appliedEnvironmentVariables = new(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -746,6 +665,9 @@ public partial class MainWindow : Window
|
||||
};
|
||||
}
|
||||
|
||||
private string SelectedRenderingBackend() =>
|
||||
RenderingBackendBox.SelectedIndex == 1 ? "Native" : "Legacy";
|
||||
|
||||
private void UpdateLogFilePathText()
|
||||
{
|
||||
LogFilePathText.Text = string.IsNullOrWhiteSpace(_settings.LogFilePath)
|
||||
@@ -1565,6 +1487,31 @@ public partial class MainWindow : Window
|
||||
_appliedEnvironmentVariables.Add(name);
|
||||
}
|
||||
|
||||
// The GUI owns this setting when it launches the emulator. Set both
|
||||
// choices explicitly so a SHARPEMU_GPU_BACKEND value inherited by the
|
||||
// launcher cannot silently override the Options menu.
|
||||
Environment.SetEnvironmentVariable(
|
||||
"SHARPEMU_GPU_BACKEND",
|
||||
string.Equals(_settings.RenderingBackend, "Legacy", StringComparison.OrdinalIgnoreCase)
|
||||
? "legacy"
|
||||
: "native");
|
||||
|
||||
if (string.Equals(_settings.RenderingBackend, "Native", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var nativeLibraryName = OperatingSystem.IsWindows()
|
||||
? "sharpemu_gpu_vulkan.dll"
|
||||
: OperatingSystem.IsMacOS()
|
||||
? "libsharpemu_gpu_vulkan.dylib"
|
||||
: "libsharpemu_gpu_vulkan.so";
|
||||
var emulatorDirectory = Path.GetDirectoryName(_emulatorExePath) ?? AppContext.BaseDirectory;
|
||||
if (!File.Exists(Path.Combine(emulatorDirectory, nativeLibraryName)))
|
||||
{
|
||||
AppendConsoleLine(
|
||||
Localization.Instance.Format("Launch.NativeBackendMissing", nativeLibraryName),
|
||||
WarningLineBrush);
|
||||
}
|
||||
}
|
||||
|
||||
var emulator = new EmulatorProcess();
|
||||
emulator.OutputReceived += (line, isError) => _pendingLines.Enqueue((line, isError));
|
||||
emulator.Exited += code => Dispatcher.UIThread.Post(() => OnEmulatorExited(code));
|
||||
|
||||
@@ -1,253 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Formats.Tar;
|
||||
using System.IO.Compression;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
/// <summary>Self-contained Windows updater; the emulator layers do not depend on it.</summary>
|
||||
public static class Updater
|
||||
{
|
||||
private const string ApplyArgument = "--sharpemu-apply-update";
|
||||
private const string LatestReleaseUrl = "https://api.github.com/repos/sharpemu/sharpemu/releases/latest";
|
||||
private static readonly TimeSpan CheckTimeout = TimeSpan.FromSeconds(10);
|
||||
private static readonly HttpClient Http = CreateHttpClient();
|
||||
|
||||
public sealed record UpdateInfo(string Sha, string Name, string DownloadUrl, long Size);
|
||||
|
||||
public static async Task<UpdateInfo?> CheckAsync(string? currentSha, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var platform = CurrentPlatform();
|
||||
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
timeout.CancelAfter(CheckTimeout);
|
||||
|
||||
using var response = await Http.GetAsync(LatestReleaseUrl, timeout.Token);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return ParseRelease(
|
||||
await response.Content.ReadAsStringAsync(timeout.Token),
|
||||
currentSha,
|
||||
platform.Rid,
|
||||
platform.Extension);
|
||||
}
|
||||
|
||||
public static async Task DownloadAndRestartAsync(
|
||||
UpdateInfo update,
|
||||
IProgress<int>? progress = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), "SharpEmu.Update");
|
||||
var payload = Path.Combine(root, "payload");
|
||||
if (Directory.Exists(root))
|
||||
{
|
||||
Directory.Delete(root, recursive: true);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(root);
|
||||
var archive = Path.Combine(root, update.Name);
|
||||
using (var response = await Http.GetAsync(update.DownloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using var input = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||
await using var output = File.Create(archive);
|
||||
var buffer = new byte[81920];
|
||||
long written = 0;
|
||||
int read;
|
||||
while ((read = await input.ReadAsync(buffer, cancellationToken)) > 0)
|
||||
{
|
||||
await output.WriteAsync(buffer.AsMemory(0, read), cancellationToken);
|
||||
written += read;
|
||||
progress?.Report(update.Size == 0 ? 0 : (int)(written * 100 / update.Size));
|
||||
}
|
||||
|
||||
if (written != update.Size)
|
||||
{
|
||||
throw new InvalidDataException($"Downloaded {written} bytes; expected {update.Size}.");
|
||||
}
|
||||
}
|
||||
|
||||
var platform = CurrentPlatform();
|
||||
var stagedExe = ExtractArchive(archive, payload, platform.Extension, platform.ExecutableName);
|
||||
|
||||
var start = new ProcessStartInfo(stagedExe)
|
||||
{
|
||||
UseShellExecute = false,
|
||||
WorkingDirectory = payload,
|
||||
};
|
||||
start.ArgumentList.Add(ApplyArgument);
|
||||
start.ArgumentList.Add(Environment.ProcessId.ToString());
|
||||
start.ArgumentList.Add(AppContext.BaseDirectory);
|
||||
using var helper = Process.Start(start)
|
||||
?? throw new InvalidOperationException("The update installer could not be started.");
|
||||
}
|
||||
|
||||
/// <summary>Runs from the downloaded executable after the old GUI exits.</summary>
|
||||
public static bool TryApply(string[] args, out int exitCode)
|
||||
{
|
||||
exitCode = 0;
|
||||
if (args.Length != 3 || args[0] != ApplyArgument)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (int.TryParse(args[1], out var oldPid))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Process.GetProcessById(oldPid).WaitForExit(30_000))
|
||||
{
|
||||
throw new TimeoutException("SharpEmu did not close within 30 seconds.");
|
||||
}
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// The old process has already exited.
|
||||
}
|
||||
}
|
||||
|
||||
var source = AppContext.BaseDirectory;
|
||||
var target = Path.GetFullPath(args[2]);
|
||||
foreach (var file in Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
var relative = Path.GetRelativePath(source, file);
|
||||
if (relative.Equals("gui-settings.json", StringComparison.OrdinalIgnoreCase) ||
|
||||
relative.StartsWith("user" + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) ||
|
||||
relative.StartsWith("logs" + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) ||
|
||||
relative.StartsWith("Languages" + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var destination = Path.Combine(target, relative);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
|
||||
File.Copy(file, destination, overwrite: true);
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
File.SetUnixFileMode(destination, File.GetUnixFileMode(file));
|
||||
}
|
||||
}
|
||||
|
||||
using var restarted = Process.Start(new ProcessStartInfo(
|
||||
Path.Combine(target, CurrentPlatform().ExecutableName))
|
||||
{
|
||||
UseShellExecute = false,
|
||||
WorkingDirectory = target,
|
||||
}) ?? throw new InvalidOperationException("The updated SharpEmu could not be started.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exitCode = 1;
|
||||
try
|
||||
{
|
||||
File.WriteAllText(Path.Combine(args[2], "update-error.log"), ex.ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best-effort diagnostics only.
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static UpdateInfo? ParseRelease(
|
||||
string json,
|
||||
string? currentSha,
|
||||
string rid,
|
||||
string extension)
|
||||
{
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var candidates = new List<(DateTimeOffset Created, UpdateInfo Update)>();
|
||||
foreach (var asset in document.RootElement.GetProperty("assets").EnumerateArray())
|
||||
{
|
||||
var name = asset.GetProperty("name").GetString() ?? "";
|
||||
var marker = $"-{rid}-";
|
||||
var markerIndex = name.LastIndexOf(marker, StringComparison.OrdinalIgnoreCase);
|
||||
if (!name.EndsWith(extension, StringComparison.OrdinalIgnoreCase) ||
|
||||
markerIndex < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var sha = name[(markerIndex + marker.Length)..^extension.Length];
|
||||
if (sha.Length < 7 || !sha.All(Uri.IsHexDigit))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
candidates.Add((
|
||||
asset.GetProperty("created_at").GetDateTimeOffset(),
|
||||
new UpdateInfo(
|
||||
sha,
|
||||
name,
|
||||
asset.GetProperty("browser_download_url").GetString()!,
|
||||
asset.GetProperty("size").GetInt64())));
|
||||
}
|
||||
|
||||
var latest = candidates.OrderByDescending(candidate => candidate.Created).FirstOrDefault().Update;
|
||||
return latest is null || string.Equals(latest.Sha, currentSha, StringComparison.OrdinalIgnoreCase)
|
||||
? null
|
||||
: latest;
|
||||
}
|
||||
|
||||
private static string ExtractArchive(
|
||||
string archive,
|
||||
string payload,
|
||||
string extension,
|
||||
string executableName)
|
||||
{
|
||||
if (extension == ".zip")
|
||||
{
|
||||
ZipFile.ExtractToDirectory(archive, payload);
|
||||
}
|
||||
else
|
||||
{
|
||||
Directory.CreateDirectory(payload);
|
||||
using var compressed = File.OpenRead(archive);
|
||||
using var gzip = new GZipStream(compressed, CompressionMode.Decompress);
|
||||
TarFile.ExtractToDirectory(gzip, payload, overwriteFiles: false);
|
||||
}
|
||||
|
||||
var executable = Path.Combine(payload, executableName);
|
||||
if (!File.Exists(executable))
|
||||
{
|
||||
throw new InvalidDataException($"The update archive does not contain {executableName}.");
|
||||
}
|
||||
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
File.SetUnixFileMode(executable, File.GetUnixFileMode(executable) | UnixFileMode.UserExecute);
|
||||
}
|
||||
|
||||
return executable;
|
||||
}
|
||||
|
||||
private static HttpClient CreateHttpClient()
|
||||
{
|
||||
var client = new HttpClient { Timeout = Timeout.InfiniteTimeSpan };
|
||||
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("SharpEmu", "0.0.1"));
|
||||
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json"));
|
||||
return client;
|
||||
}
|
||||
|
||||
private static PlatformInfo CurrentPlatform()
|
||||
{
|
||||
if (RuntimeInformation.ProcessArchitecture != Architecture.X64)
|
||||
{
|
||||
throw new PlatformNotSupportedException("SharpEmu releases require an x64 process.");
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsWindows()) return new("win-x64", ".zip", "SharpEmu.exe");
|
||||
if (OperatingSystem.IsLinux()) return new("linux-x64", ".tar.gz", "SharpEmu");
|
||||
if (OperatingSystem.IsMacOS()) return new("osx-x64", ".tar.gz", "SharpEmu");
|
||||
throw new PlatformNotSupportedException();
|
||||
}
|
||||
|
||||
private sealed record PlatformInfo(string Rid, string Extension, string ExecutableName);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Gpu.Vulkan;
|
||||
using SharpEmu.Libs.Gpu.NativeVulkan;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu;
|
||||
|
||||
@@ -12,7 +13,25 @@ namespace SharpEmu.Libs.Gpu;
|
||||
/// </summary>
|
||||
internal static class GuestGpu
|
||||
{
|
||||
private static readonly Lazy<IGuestGpuBackend> Instance = new(static () => new VulkanGuestGpuBackend());
|
||||
private static readonly Lazy<IGuestGpuBackend> Instance = new(CreateBackend);
|
||||
|
||||
public static IGuestGpuBackend Current => Instance.Value;
|
||||
|
||||
private static IGuestGpuBackend CreateBackend()
|
||||
{
|
||||
if (!string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_GPU_BACKEND"),
|
||||
"native",
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new VulkanGuestGpuBackend();
|
||||
}
|
||||
|
||||
if (!NativeVulkanApi.IsAvailable(out var error))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][WARN] {error}");
|
||||
}
|
||||
|
||||
return new NativeVulkanGuestGpuBackend();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.HLE.Host;
|
||||
using SharpEmu.HLE.Host.Posix;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.NativeVulkan;
|
||||
|
||||
internal sealed unsafe class NativeGpuInputSource : IPosixWindowInputSource
|
||||
{
|
||||
internal static NativeGpuInputSource Instance { get; } = new();
|
||||
private readonly object _gate = new();
|
||||
private readonly uint[] _keys = new uint[8];
|
||||
private bool _focused;
|
||||
private bool _gamepadConnected;
|
||||
private HostGamepadState _gamepad;
|
||||
private string? _gamepadName;
|
||||
|
||||
private NativeGpuInputSource() { }
|
||||
|
||||
internal void Attach() => PosixHostInput.SetSource(this);
|
||||
|
||||
internal void Update(NativeVulkanApi.Input* state)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_focused = state->KeyboardFocused != 0;
|
||||
for (var index = 0; index < _keys.Length; ++index) _keys[index] = state->VirtualKeys[index];
|
||||
_gamepadConnected = state->GamepadConnected != 0;
|
||||
_gamepad = new HostGamepadState(
|
||||
_gamepadConnected,
|
||||
(HostGamepadButtons)state->GamepadButtons,
|
||||
state->LeftX,
|
||||
state->LeftY,
|
||||
state->RightX,
|
||||
state->RightY,
|
||||
state->LeftTrigger,
|
||||
state->RightTrigger);
|
||||
_gamepadName = _gamepadConnected
|
||||
? Marshal.PtrToStringUTF8((nint)state->GamepadNameUtf8)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasKeyboardFocus
|
||||
{
|
||||
get { lock (_gate) return _focused; }
|
||||
}
|
||||
|
||||
public bool IsKeyDown(int virtualKey)
|
||||
{
|
||||
if ((uint)virtualKey >= 256) return false;
|
||||
lock (_gate) return (_keys[virtualKey / 32] & (1u << (virtualKey % 32))) != 0;
|
||||
}
|
||||
|
||||
public int GetGamepadStates(Span<HostGamepadState> destination)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_gamepadConnected || destination.IsEmpty) return 0;
|
||||
destination[0] = _gamepad;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
public string? DescribeConnectedGamepad()
|
||||
{
|
||||
lock (_gate) return _gamepadConnected ? _gamepadName ?? "SDL gamepad" : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.Libs.Gpu.Vulkan;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.NativeVulkan;
|
||||
|
||||
internal static unsafe class NativeGpuPacket
|
||||
{
|
||||
internal static NativeGpuResult SubmitDraw(
|
||||
nint backend,
|
||||
VulkanCompiledGuestShader pixelShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> memoryBuffers,
|
||||
uint width,
|
||||
uint height,
|
||||
uint attributeCount,
|
||||
VulkanCompiledGuestShader? vertexShader,
|
||||
uint vertexCount,
|
||||
uint instanceCount,
|
||||
uint primitiveType,
|
||||
GuestIndexBuffer? indexBuffer,
|
||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers,
|
||||
GuestRenderState? renderState,
|
||||
IReadOnlyList<GuestRenderTarget>? targets,
|
||||
bool publishTargets)
|
||||
{
|
||||
using var storage = new Storage();
|
||||
var nativeTextures = storage.Allocate<NativeVulkanApi.Texture>(textures.Count);
|
||||
for (var index = 0; index < textures.Count; ++index)
|
||||
nativeTextures[index] = Texture(textures[index], storage);
|
||||
var nativeMemory = storage.Allocate<NativeVulkanApi.MemoryBuffer>(memoryBuffers.Count);
|
||||
for (var index = 0; index < memoryBuffers.Count; ++index)
|
||||
nativeMemory[index] = new() { Address = memoryBuffers[index].BaseAddress, Data = storage.Pin(memoryBuffers[index].Data) };
|
||||
var vertices = vertexBuffers ?? [];
|
||||
var nativeVertices = storage.Allocate<NativeVulkanApi.VertexBuffer>(vertices.Count);
|
||||
for (var index = 0; index < vertices.Count; ++index)
|
||||
{
|
||||
var source = vertices[index];
|
||||
nativeVertices[index] = new()
|
||||
{
|
||||
StructSize = (uint)sizeof(NativeVulkanApi.VertexBuffer), Location = source.Location,
|
||||
ComponentCount = source.ComponentCount, DataFormat = source.DataFormat,
|
||||
NumberFormat = source.NumberFormat, Address = source.BaseAddress, Stride = source.Stride,
|
||||
OffsetBytes = source.OffsetBytes, Data = storage.Pin(source.Data),
|
||||
};
|
||||
}
|
||||
var targetList = targets ?? [];
|
||||
var nativeTargets = storage.Allocate<NativeVulkanApi.RenderTarget>(targetList.Count);
|
||||
for (var index = 0; index < targetList.Count; ++index)
|
||||
{
|
||||
var source = targetList[index];
|
||||
nativeTargets[index] = new()
|
||||
{
|
||||
StructSize = (uint)sizeof(NativeVulkanApi.RenderTarget), Address = source.Address,
|
||||
Width = source.Width, Height = source.Height, Format = source.Format,
|
||||
NumberType = source.NumberType, MipLevels = source.MipLevels,
|
||||
};
|
||||
}
|
||||
var state = renderState ?? GuestRenderState.Default;
|
||||
var blends = storage.Allocate<NativeVulkanApi.Blend>(state.Blends.Count);
|
||||
for (var index = 0; index < state.Blends.Count; ++index)
|
||||
{
|
||||
var source = state.Blends[index];
|
||||
blends[index] = new()
|
||||
{
|
||||
Enable = source.Enable ? 1u : 0u, ColorSrc = source.ColorSrcFactor,
|
||||
ColorDst = source.ColorDstFactor, ColorFunc = source.ColorFunc,
|
||||
AlphaSrc = source.AlphaSrcFactor, AlphaDst = source.AlphaDstFactor,
|
||||
AlphaFunc = source.AlphaFunc, SeparateAlpha = source.SeparateAlphaBlend ? 1u : 0u,
|
||||
WriteMask = source.WriteMask,
|
||||
};
|
||||
}
|
||||
NativeVulkanApi.IndexBuffer nativeIndex = default;
|
||||
NativeVulkanApi.IndexBuffer* nativeIndexPointer = null;
|
||||
if (indexBuffer is not null)
|
||||
{
|
||||
nativeIndex = new() { Data = storage.Pin(indexBuffer.Data), Is32Bit = indexBuffer.Is32Bit ? 1u : 0u };
|
||||
nativeIndexPointer = &nativeIndex;
|
||||
}
|
||||
NativeVulkanApi.Rect nativeScissor = default; NativeVulkanApi.Rect* scissorPointer = null;
|
||||
if (state.Scissor is { } scissor)
|
||||
{
|
||||
nativeScissor = new() { X = scissor.X, Y = scissor.Y, Width = scissor.Width, Height = scissor.Height };
|
||||
scissorPointer = &nativeScissor;
|
||||
}
|
||||
NativeVulkanApi.Viewport nativeViewport = default; NativeVulkanApi.Viewport* viewportPointer = null;
|
||||
if (state.Viewport is { } viewport)
|
||||
{
|
||||
nativeViewport = new()
|
||||
{
|
||||
X = viewport.X, Y = viewport.Y, Width = viewport.Width, Height = viewport.Height,
|
||||
MinDepth = viewport.MinDepth, MaxDepth = viewport.MaxDepth,
|
||||
};
|
||||
viewportPointer = &nativeViewport;
|
||||
}
|
||||
var draw = new NativeVulkanApi.Draw
|
||||
{
|
||||
StructSize = (uint)sizeof(NativeVulkanApi.Draw),
|
||||
Width = width,
|
||||
Height = height,
|
||||
VertexSpirv = storage.Pin(vertexShader?.Spirv ?? SpirvFixedShaders.CreateFullscreenVertex(attributeCount)),
|
||||
PixelSpirv = storage.Pin(pixelShader.Spirv), Textures = nativeTextures,
|
||||
TextureCount = (uint)textures.Count, MemoryBuffers = nativeMemory,
|
||||
MemoryBufferCount = (uint)memoryBuffers.Count, VertexBuffers = nativeVertices,
|
||||
VertexBufferCount = (uint)vertices.Count, Targets = nativeTargets,
|
||||
TargetCount = (uint)targetList.Count, Blends = blends, BlendCount = (uint)state.Blends.Count,
|
||||
IndexBuffer = nativeIndexPointer, Scissor = scissorPointer, ViewportState = viewportPointer,
|
||||
AttributeCount = attributeCount, VertexCount = vertexCount, InstanceCount = instanceCount,
|
||||
PrimitiveType = primitiveType, PublishTargets = publishTargets ? 1u : 0u,
|
||||
};
|
||||
return NativeVulkanApi.SubmitDraw(backend, &draw);
|
||||
}
|
||||
|
||||
internal static NativeGpuResult SubmitCompute(
|
||||
nint backend,
|
||||
ulong shaderAddress,
|
||||
VulkanCompiledGuestShader shader,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> buffers,
|
||||
uint x, uint y, uint z)
|
||||
{
|
||||
using var storage = new Storage();
|
||||
var nativeTextures = storage.Allocate<NativeVulkanApi.Texture>(textures.Count);
|
||||
for (var index = 0; index < textures.Count; ++index) nativeTextures[index] = Texture(textures[index], storage);
|
||||
var nativeBuffers = storage.Allocate<NativeVulkanApi.MemoryBuffer>(buffers.Count);
|
||||
for (var index = 0; index < buffers.Count; ++index)
|
||||
nativeBuffers[index] = new() { Address = buffers[index].BaseAddress, Data = storage.Pin(buffers[index].Data) };
|
||||
var compute = new NativeVulkanApi.Compute
|
||||
{
|
||||
StructSize = (uint)sizeof(NativeVulkanApi.Compute), ShaderAddress = shaderAddress,
|
||||
Spirv = storage.Pin(shader.Spirv), Textures = nativeTextures, TextureCount = (uint)textures.Count,
|
||||
MemoryBuffers = nativeBuffers, MemoryBufferCount = (uint)buffers.Count,
|
||||
GroupsX = x, GroupsY = y, GroupsZ = z,
|
||||
};
|
||||
return NativeVulkanApi.SubmitCompute(backend, &compute);
|
||||
}
|
||||
|
||||
private static NativeVulkanApi.Texture Texture(GuestDrawTexture source, Storage storage)
|
||||
{
|
||||
var result = new NativeVulkanApi.Texture
|
||||
{
|
||||
StructSize = (uint)sizeof(NativeVulkanApi.Texture), Address = source.Address,
|
||||
Width = source.Width, Height = source.Height, Format = source.Format,
|
||||
NumberType = source.NumberType, RgbaPixels = storage.Pin(source.RgbaPixels),
|
||||
IsFallback = source.IsFallback ? 1u : 0u, IsStorage = source.IsStorage ? 1u : 0u,
|
||||
MipLevels = source.MipLevels, MipLevel = source.MipLevel, Pitch = source.Pitch,
|
||||
TileMode = source.TileMode, DstSelect = source.DstSelect,
|
||||
};
|
||||
result.SamplerState.Words[0] = source.Sampler.Word0;
|
||||
result.SamplerState.Words[1] = source.Sampler.Word1;
|
||||
result.SamplerState.Words[2] = source.Sampler.Word2;
|
||||
result.SamplerState.Words[3] = source.Sampler.Word3;
|
||||
return result;
|
||||
}
|
||||
|
||||
private sealed class Storage : IDisposable
|
||||
{
|
||||
private readonly List<GCHandle> _pins = [];
|
||||
private readonly List<nint> _allocations = [];
|
||||
internal NativeVulkanApi.Bytes Pin(byte[] data)
|
||||
{
|
||||
if (data.Length == 0) return default;
|
||||
var pin = GCHandle.Alloc(data, GCHandleType.Pinned); _pins.Add(pin);
|
||||
return new() { Data = (void*)pin.AddrOfPinnedObject(), Size = (nuint)data.Length };
|
||||
}
|
||||
internal T* Allocate<T>(int count) where T : unmanaged
|
||||
{
|
||||
if (count == 0) return null;
|
||||
var pointer = NativeMemory.AllocZeroed((nuint)count, (nuint)sizeof(T));
|
||||
if (pointer is null) throw new OutOfMemoryException();
|
||||
_allocations.Add((nint)pointer); return (T*)pointer;
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var pin in _pins) pin.Free();
|
||||
foreach (var allocation in _allocations) NativeMemory.Free((void*)allocation);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.NativeVulkan;
|
||||
|
||||
internal enum NativeGpuResult
|
||||
{
|
||||
Success = 0,
|
||||
NotFound = 1,
|
||||
NotReady = 2,
|
||||
InvalidArgument = -1,
|
||||
IncompatibleAbi = -2,
|
||||
PlatformError = -3,
|
||||
VulkanError = -4,
|
||||
OutOfMemory = -5,
|
||||
InternalError = -6,
|
||||
}
|
||||
|
||||
internal static unsafe partial class NativeVulkanApi
|
||||
{
|
||||
internal const uint AbiVersion = 1;
|
||||
private const string Library = "sharpemu_gpu_vulkan";
|
||||
|
||||
internal static string LibraryFileName =>
|
||||
OperatingSystem.IsWindows() ? "sharpemu_gpu_vulkan.dll" :
|
||||
OperatingSystem.IsMacOS() ? "libsharpemu_gpu_vulkan.dylib" :
|
||||
"libsharpemu_gpu_vulkan.so";
|
||||
|
||||
internal static bool IsAvailable(out string error)
|
||||
{
|
||||
if (NativeLibrary.TryLoad(
|
||||
Library,
|
||||
typeof(NativeVulkanApi).Assembly,
|
||||
DllImportSearchPath.SafeDirectories | DllImportSearchPath.AssemblyDirectory,
|
||||
out var handle))
|
||||
{
|
||||
NativeLibrary.Free(handle);
|
||||
error = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
error =
|
||||
$"The optional native Vulkan backend was selected, but {LibraryFileName} could not be loaded. " +
|
||||
"Place the library from https://github.com/sharpemu/sharpemu.vulkan next to the SharpEmu executable " +
|
||||
"and ensure its SDL3 and Vulkan runtime dependencies are installed.";
|
||||
return false;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct CreateInfo
|
||||
{
|
||||
internal uint StructSize;
|
||||
internal uint AbiVersion;
|
||||
internal uint Width;
|
||||
internal uint Height;
|
||||
internal uint EnableValidation;
|
||||
internal byte* TitleUtf8;
|
||||
internal delegate* unmanaged[Cdecl]<int, byte*, void*, void> Log;
|
||||
internal void* LogUser;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)] internal struct Bytes { internal void* Data; internal nuint Size; }
|
||||
[StructLayout(LayoutKind.Sequential)] internal struct Sampler { internal fixed uint Words[4]; }
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct Texture
|
||||
{
|
||||
internal uint StructSize; internal ulong Address; internal uint Width, Height, Format, NumberType;
|
||||
internal Bytes RgbaPixels; internal uint IsFallback, IsStorage, MipLevels, MipLevel;
|
||||
internal uint Pitch, TileMode, DstSelect; internal Sampler SamplerState;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)] internal struct MemoryBuffer { internal ulong Address; internal Bytes Data; }
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct VertexBuffer
|
||||
{
|
||||
internal uint StructSize, Location, ComponentCount, DataFormat, NumberFormat;
|
||||
internal ulong Address; internal uint Stride, OffsetBytes; internal Bytes Data;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)] internal struct IndexBuffer { internal Bytes Data; internal uint Is32Bit; }
|
||||
[StructLayout(LayoutKind.Sequential)] internal struct Rect { internal int X, Y; internal uint Width, Height; }
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct Viewport { internal float X, Y, Width, Height, MinDepth, MaxDepth; }
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct Blend
|
||||
{
|
||||
internal uint Enable, ColorSrc, ColorDst, ColorFunc, AlphaSrc, AlphaDst, AlphaFunc;
|
||||
internal uint SeparateAlpha, WriteMask;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct RenderTarget
|
||||
{
|
||||
internal uint StructSize; internal ulong Address;
|
||||
internal uint Width, Height, Format, NumberType, MipLevels;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct Draw
|
||||
{
|
||||
internal uint StructSize, Width, Height; internal Bytes VertexSpirv, PixelSpirv;
|
||||
internal Texture* Textures; internal uint TextureCount;
|
||||
internal MemoryBuffer* MemoryBuffers; internal uint MemoryBufferCount;
|
||||
internal VertexBuffer* VertexBuffers; internal uint VertexBufferCount;
|
||||
internal RenderTarget* Targets; internal uint TargetCount;
|
||||
internal Blend* Blends; internal uint BlendCount;
|
||||
internal IndexBuffer* IndexBuffer; internal Rect* Scissor; internal Viewport* ViewportState;
|
||||
internal uint AttributeCount, VertexCount, InstanceCount, PrimitiveType, PublishTargets;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct Compute
|
||||
{
|
||||
internal uint StructSize; internal ulong ShaderAddress; internal Bytes Spirv;
|
||||
internal Texture* Textures; internal uint TextureCount;
|
||||
internal MemoryBuffer* MemoryBuffers; internal uint MemoryBufferCount;
|
||||
internal uint GroupsX, GroupsY, GroupsZ;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct Input
|
||||
{
|
||||
internal uint StructSize, KeyboardFocused;
|
||||
internal fixed uint VirtualKeys[8];
|
||||
internal uint GamepadConnected, GamepadButtons;
|
||||
internal byte LeftX, LeftY, RightX, RightY, LeftTrigger, RightTrigger;
|
||||
internal fixed byte Reserved[2]; internal fixed byte GamepadNameUtf8[128];
|
||||
}
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_abi_version")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial uint GetAbiVersion();
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_last_error")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
private static partial byte* LastError(nint backend);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_create")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult Create(CreateInfo* info, out nint backend);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_destroy")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial void Destroy(nint backend);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_poll")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult Poll(nint backend, out uint shouldClose);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_input_snapshot")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult InputSnapshot(nint backend, Input* input);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_present_bgra")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult PresentBgra(
|
||||
nint backend,
|
||||
void* pixels,
|
||||
nuint size,
|
||||
uint width,
|
||||
uint height,
|
||||
uint pitch);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_submit_draw")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult SubmitDraw(nint backend, Draw* draw);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_submit_compute")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult SubmitCompute(nint backend, Compute* compute);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_register_display_buffer")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult RegisterDisplayBuffer(nint backend, ulong address, uint format);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_present_guest_image")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult PresentGuestImage(
|
||||
nint backend, ulong address, uint width, uint height, uint pitch);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_has_guest_image")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult HasGuestImage(nint backend, ulong address, uint format, uint numberType);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_blit_guest_image")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult BlitGuestImage(
|
||||
nint backend, ulong sourceAddress, uint sourceWidth, uint sourceHeight, uint sourceFormat,
|
||||
ulong destinationAddress, uint destinationWidth, uint destinationHeight, uint destinationFormat);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_render_target_output_kind")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult RenderTargetOutputKind(uint format, uint numberType, out uint outputKind);
|
||||
|
||||
internal static string GetError(nint backend)
|
||||
{
|
||||
var pointer = LastError(backend);
|
||||
return pointer is null ? "Unknown native GPU error" : Marshal.PtrToStringUTF8((nint)pointer)!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.Libs.Gpu.Vulkan;
|
||||
using SharpEmu.ShaderCompiler;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.NativeVulkan;
|
||||
|
||||
/// <summary>Native C++ Vulkan implementation of the guest-domain GPU seam.</summary>
|
||||
internal sealed unsafe class NativeVulkanGuestGpuBackend : IGuestGpuBackend
|
||||
{
|
||||
private static readonly IGuestCompiledShader DepthOnlyFragmentShader =
|
||||
new VulkanCompiledGuestShader(SpirvFixedShaders.CreateDepthOnlyFragment());
|
||||
|
||||
private readonly object _startGate = new();
|
||||
private readonly BlockingCollection<Action<nint>> _commands = new(new ConcurrentQueue<Action<nint>>(), 256);
|
||||
private readonly ManualResetEventSlim _ready = new(false);
|
||||
private Thread? _thread;
|
||||
private Exception? _startError;
|
||||
|
||||
public void EnsureStarted(uint width, uint height)
|
||||
{
|
||||
if (width == 0 || height == 0) return;
|
||||
lock (_startGate)
|
||||
{
|
||||
if (_thread is null)
|
||||
{
|
||||
_thread = new Thread(() => Run(width, height))
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "SharpEmu native Vulkan",
|
||||
};
|
||||
_thread.Start();
|
||||
}
|
||||
}
|
||||
_ready.Wait();
|
||||
if (_startError is not null)
|
||||
{
|
||||
if (_startError is DllNotFoundException)
|
||||
{
|
||||
_ = NativeVulkanApi.IsAvailable(out var error);
|
||||
throw new InvalidOperationException(error, _startError);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Native Vulkan startup failed", _startError);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryCompileVertexShader(Gen5ShaderState state, Gen5ShaderEvaluation evaluation,
|
||||
out IGuestCompiledShader? shader, out string error, int globalBufferBase = 0,
|
||||
int totalGlobalBufferCount = -1, int imageBindingBase = 0, int scalarRegisterBufferIndex = -1,
|
||||
int requiredVertexOutputCount = 0, ulong storageBufferOffsetAlignment = 1)
|
||||
{
|
||||
shader = null;
|
||||
if (!Gen5SpirvTranslator.TryCompileVertexShader(state, evaluation, out var compiled, out error,
|
||||
globalBufferBase, totalGlobalBufferCount, imageBindingBase, scalarRegisterBufferIndex,
|
||||
requiredVertexOutputCount, storageBufferOffsetAlignment)) return false;
|
||||
shader = new VulkanCompiledGuestShader(compiled.Spirv); return true;
|
||||
}
|
||||
|
||||
public bool TryCompilePixelShader(Gen5ShaderState state, Gen5ShaderEvaluation evaluation,
|
||||
IReadOnlyList<Gen5PixelOutputBinding> outputs, out IGuestCompiledShader? shader, out string error,
|
||||
int globalBufferBase = 0, int totalGlobalBufferCount = -1, int imageBindingBase = 0,
|
||||
int scalarRegisterBufferIndex = -1, uint pixelInputEnable = 0, uint pixelInputAddress = 0,
|
||||
ulong storageBufferOffsetAlignment = 1)
|
||||
{
|
||||
shader = null;
|
||||
if (!Gen5SpirvTranslator.TryCompilePixelShader(state, evaluation, outputs, out var compiled, out error,
|
||||
globalBufferBase, totalGlobalBufferCount, imageBindingBase, scalarRegisterBufferIndex,
|
||||
pixelInputEnable, pixelInputAddress, storageBufferOffsetAlignment)) return false;
|
||||
shader = new VulkanCompiledGuestShader(compiled.Spirv); return true;
|
||||
}
|
||||
|
||||
public bool TryCompileComputeShader(Gen5ShaderState state, Gen5ShaderEvaluation evaluation,
|
||||
uint localSizeX, uint localSizeY, uint localSizeZ, out IGuestCompiledShader? shader, out string error,
|
||||
int totalGlobalBufferCount = -1, int initialScalarBufferIndex = -1, uint waveLaneCount = 32,
|
||||
ulong storageBufferOffsetAlignment = 1)
|
||||
{
|
||||
shader = null;
|
||||
if (!Gen5SpirvTranslator.TryCompileComputeShader(state, evaluation, localSizeX, localSizeY, localSizeZ,
|
||||
out var compiled, out error, totalGlobalBufferCount, initialScalarBufferIndex, waveLaneCount,
|
||||
storageBufferOffsetAlignment)) return false;
|
||||
shader = new VulkanCompiledGuestShader(compiled.Spirv); return true;
|
||||
}
|
||||
|
||||
public IGuestCompiledShader GetDepthOnlyFragmentShader() => DepthOnlyFragmentShader;
|
||||
|
||||
public void HideSplashScreen() { }
|
||||
|
||||
public void Submit(byte[] bgraFrame, uint width, uint height)
|
||||
{
|
||||
if (bgraFrame.Length != checked((int)(width * height * 4))) return;
|
||||
EnsureStarted(width, height);
|
||||
Enqueue(handle =>
|
||||
{
|
||||
fixed (byte* pixels = bgraFrame)
|
||||
Check(handle, NativeVulkanApi.PresentBgra(handle, pixels, (nuint)bgraFrame.Length, width, height, width * 4));
|
||||
});
|
||||
}
|
||||
|
||||
public void SubmitGuestDraw(GuestDrawKind drawKind, uint width, uint height)
|
||||
{
|
||||
if (drawKind != GuestDrawKind.FullscreenBarycentric || width == 0 || height == 0) return;
|
||||
EnsureStarted(width, height);
|
||||
var pixel = new VulkanCompiledGuestShader(SpirvFixedShaders.CreateBarycentricFragment());
|
||||
Enqueue(handle => Check(handle, NativeGpuPacket.SubmitDraw(handle, pixel, [], [],
|
||||
width, height, 1, null, 3, 1, 4, null, null, null, null, false)));
|
||||
}
|
||||
|
||||
public void SubmitTranslatedDraw(IGuestCompiledShader pixelShader, IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers, uint width, uint height, uint attributeCount,
|
||||
IGuestCompiledShader? vertexShader = null, uint vertexCount = 3, uint instanceCount = 1,
|
||||
uint primitiveType = 4, GuestIndexBuffer? indexBuffer = null,
|
||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null, GuestRenderState? renderState = null)
|
||||
{
|
||||
EnsureStarted(width, height);
|
||||
var ps = Spirv(pixelShader); var vs = vertexShader is null ? null : Spirv(vertexShader);
|
||||
var textureCopy = textures.ToArray(); var memoryCopy = globalMemoryBuffers.ToArray();
|
||||
var vertexCopy = vertexBuffers?.ToArray();
|
||||
Enqueue(handle => Check(handle, NativeGpuPacket.SubmitDraw(handle, ps, textureCopy, memoryCopy,
|
||||
width, height, attributeCount, vs, vertexCount, instanceCount, primitiveType, indexBuffer,
|
||||
vertexCopy, renderState, null, false)));
|
||||
}
|
||||
|
||||
public void SubmitOffscreenTranslatedDraw(IGuestCompiledShader pixelShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures, IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint attributeCount, IReadOnlyList<GuestRenderTarget> targets, IGuestCompiledShader? vertexShader = null,
|
||||
uint vertexCount = 3, uint instanceCount = 1, uint primitiveType = 4,
|
||||
GuestIndexBuffer? indexBuffer = null, IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||
GuestRenderState? renderState = null, GuestDepthTarget? depthTarget = null, ulong shaderAddress = 0)
|
||||
{
|
||||
if (targets.Count == 0) return;
|
||||
EnsureStarted(targets[0].Width, targets[0].Height);
|
||||
var ps = Spirv(pixelShader); var vs = vertexShader is null ? null : Spirv(vertexShader);
|
||||
var textureCopy = textures.ToArray(); var memoryCopy = globalMemoryBuffers.ToArray();
|
||||
var targetCopy = targets.ToArray(); var vertexCopy = vertexBuffers?.ToArray();
|
||||
Enqueue(handle => Check(handle, NativeGpuPacket.SubmitDraw(handle, ps, textureCopy, memoryCopy,
|
||||
targetCopy[0].Width, targetCopy[0].Height, attributeCount, vs, vertexCount, instanceCount,
|
||||
primitiveType, indexBuffer, vertexCopy, renderState, targetCopy, true)));
|
||||
}
|
||||
|
||||
public void SubmitDepthOnlyTranslatedDraw(IGuestCompiledShader pixelShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures, IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint attributeCount, GuestDepthTarget depthTarget, IGuestCompiledShader? vertexShader = null,
|
||||
uint vertexCount = 3, uint instanceCount = 1, uint primitiveType = 4,
|
||||
GuestIndexBuffer? indexBuffer = null, IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||
GuestRenderState? renderState = null, ulong shaderAddress = 0)
|
||||
{
|
||||
EnsureStarted(depthTarget.Width, depthTarget.Height);
|
||||
var ps = Spirv(pixelShader); var vs = vertexShader is null ? null : Spirv(vertexShader);
|
||||
var textureCopy = textures.ToArray(); var memoryCopy = globalMemoryBuffers.ToArray();
|
||||
var vertexCopy = vertexBuffers?.ToArray();
|
||||
Enqueue(handle => Check(handle, NativeGpuPacket.SubmitDraw(handle, ps, textureCopy, memoryCopy,
|
||||
depthTarget.Width, depthTarget.Height, attributeCount, vs, vertexCount, instanceCount,
|
||||
primitiveType, indexBuffer, vertexCopy, renderState, null, false)));
|
||||
}
|
||||
|
||||
public void SubmitStorageTranslatedDraw(IGuestCompiledShader pixelShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures, IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint attributeCount, uint width, uint height, ulong shaderAddress = 0)
|
||||
{
|
||||
EnsureStarted(width, height); var ps = Spirv(pixelShader);
|
||||
var textureCopy = textures.ToArray(); var memoryCopy = globalMemoryBuffers.ToArray();
|
||||
GuestRenderTarget[] targets = [new(0, width, height, 12, 7)];
|
||||
Enqueue(handle => Check(handle, NativeGpuPacket.SubmitDraw(handle, ps, textureCopy, memoryCopy,
|
||||
width, height, attributeCount, null, 3, 1, 4, null, null, null, targets, false)));
|
||||
}
|
||||
|
||||
public long SubmitComputeDispatch(ulong shaderAddress, IGuestCompiledShader computeShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures, IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint groupCountX, uint groupCountY, uint groupCountZ, uint baseGroupX, uint baseGroupY,
|
||||
uint baseGroupZ, uint localSizeX, uint localSizeY, uint localSizeZ, bool isIndirect,
|
||||
bool writesGlobalMemory, uint threadCountX = uint.MaxValue, uint threadCountY = uint.MaxValue,
|
||||
uint threadCountZ = uint.MaxValue)
|
||||
{
|
||||
EnsureStarted(1280, 720); var shader = Spirv(computeShader);
|
||||
var textureCopy = textures.ToArray(); var memoryCopy = globalMemoryBuffers.ToArray();
|
||||
Enqueue(handle => Check(handle, NativeGpuPacket.SubmitCompute(handle, shaderAddress, shader,
|
||||
textureCopy, memoryCopy, groupCountX, groupCountY, groupCountZ)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
public bool TrySubmitGuestImage(ulong address, uint width, uint height, uint pitchInPixel)
|
||||
{
|
||||
EnsureStarted(width, height);
|
||||
return Invoke(handle => NativeVulkanApi.PresentGuestImage(handle, address, width, height, pitchInPixel)) ==
|
||||
NativeGpuResult.Success;
|
||||
}
|
||||
|
||||
public bool TrySubmitOrderedGuestImageFlip(int videoOutHandle, int displayBufferIndex, ulong address,
|
||||
uint width, uint height, uint pitchInPixel) =>
|
||||
TrySubmitGuestImage(address, width, height, pitchInPixel);
|
||||
|
||||
public void RegisterKnownDisplayBuffer(ulong address, uint guestFormat)
|
||||
{
|
||||
EnsureStarted(1280, 720);
|
||||
Enqueue(handle => Check(handle, NativeVulkanApi.RegisterDisplayBuffer(handle, address, guestFormat)));
|
||||
}
|
||||
|
||||
public bool IsGpuGuestImageAvailable(ulong address, uint format, uint numberType)
|
||||
{
|
||||
EnsureStarted(1280, 720);
|
||||
return Invoke(handle => NativeVulkanApi.HasGuestImage(handle, address, format, numberType)) ==
|
||||
NativeGpuResult.Success;
|
||||
}
|
||||
|
||||
public bool TrySubmitGuestImageBlit(ulong sourceAddress, uint sourceWidth, uint sourceHeight,
|
||||
uint sourceFormat, uint sourceNumberType, ulong destinationAddress, uint destinationWidth,
|
||||
uint destinationHeight, uint destinationFormat, uint destinationNumberType)
|
||||
{
|
||||
EnsureStarted(destinationWidth, destinationHeight);
|
||||
return Invoke(handle => NativeVulkanApi.BlitGuestImage(handle, sourceAddress, sourceWidth, sourceHeight,
|
||||
sourceFormat, destinationAddress, destinationWidth, destinationHeight, destinationFormat)) ==
|
||||
NativeGpuResult.Success;
|
||||
}
|
||||
|
||||
public bool TryGetRenderTargetOutputKind(uint dataFormat, uint numberType,
|
||||
out Gen5PixelOutputKind outputKind)
|
||||
{
|
||||
var result = NativeVulkanApi.RenderTargetOutputKind(dataFormat, numberType, out var nativeKind);
|
||||
outputKind = (Gen5PixelOutputKind)nativeKind; return result == NativeGpuResult.Success;
|
||||
}
|
||||
|
||||
private void Run(uint width, uint height)
|
||||
{
|
||||
nint backend = 0;
|
||||
try
|
||||
{
|
||||
if (NativeVulkanApi.GetAbiVersion() != NativeVulkanApi.AbiVersion)
|
||||
throw new InvalidOperationException("Native Vulkan ABI version mismatch");
|
||||
var title = Marshal.StringToCoTaskMemUTF8("SharpEmu");
|
||||
try
|
||||
{
|
||||
var info = new NativeVulkanApi.CreateInfo
|
||||
{
|
||||
StructSize = (uint)sizeof(NativeVulkanApi.CreateInfo), AbiVersion = NativeVulkanApi.AbiVersion,
|
||||
Width = width, Height = height, TitleUtf8 = (byte*)title,
|
||||
EnableValidation = Environment.GetEnvironmentVariable("SHARPEMU_VK_VALIDATION") == "1" ? 1u : 0u,
|
||||
};
|
||||
var result = NativeVulkanApi.Create(&info, out backend);
|
||||
if (result != NativeGpuResult.Success)
|
||||
throw new InvalidOperationException($"se_gpu_create failed with {result}: {NativeVulkanApi.GetError(0)}");
|
||||
}
|
||||
finally { Marshal.FreeCoTaskMem(title); }
|
||||
_ready.Set();
|
||||
NativeGpuInputSource.Instance.Attach();
|
||||
while (true)
|
||||
{
|
||||
if (_commands.TryTake(out var command, 8))
|
||||
{
|
||||
command(backend);
|
||||
for (var drained = 1; drained < 128 && _commands.TryTake(out command); ++drained)
|
||||
command(backend);
|
||||
}
|
||||
var result = NativeVulkanApi.Poll(backend, out var shouldClose);
|
||||
if (result != NativeGpuResult.Success || shouldClose != 0) break;
|
||||
var input = new NativeVulkanApi.Input { StructSize = (uint)sizeof(NativeVulkanApi.Input) };
|
||||
if (NativeVulkanApi.InputSnapshot(backend, &input) == NativeGpuResult.Success)
|
||||
NativeGpuInputSource.Instance.Update(&input);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_startError ??= exception;
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] Native Vulkan backend failed: {exception}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ready.Set();
|
||||
if (backend != 0) NativeVulkanApi.Destroy(backend);
|
||||
}
|
||||
}
|
||||
|
||||
private void Enqueue(Action<nint> command)
|
||||
{
|
||||
if (!_commands.TryAdd(command)) Console.Error.WriteLine("[LOADER][WARN] Native GPU queue is full; dropping work");
|
||||
}
|
||||
|
||||
private NativeGpuResult Invoke(Func<nint, NativeGpuResult> operation)
|
||||
{
|
||||
var completion = new TaskCompletionSource<NativeGpuResult>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
Enqueue(handle =>
|
||||
{
|
||||
try { completion.SetResult(operation(handle)); }
|
||||
catch (Exception exception) { completion.SetException(exception); }
|
||||
});
|
||||
return completion.Task.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private static void Check(nint backend, NativeGpuResult result)
|
||||
{
|
||||
if (result is NativeGpuResult.Success or NativeGpuResult.NotReady) return;
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] Native GPU operation failed: {result}: {NativeVulkanApi.GetError(backend)}");
|
||||
}
|
||||
|
||||
private static VulkanCompiledGuestShader Spirv(IGuestCompiledShader shader) =>
|
||||
shader as VulkanCompiledGuestShader ?? throw new InvalidOperationException(
|
||||
$"Shader type {shader.GetType().Name} was not compiled by the native Vulkan backend");
|
||||
}
|
||||
@@ -1574,27 +1574,7 @@ public static partial class KernelMemoryCompatExports
|
||||
ExportName = "stat",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int PosixStat(CpuContext ctx)
|
||||
{
|
||||
var result = KernelStat(ctx);
|
||||
if (result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// stat(2) follows the libc/POSIX ABI: failures return -1 and expose
|
||||
// the reason through errno. Returning the raw Orbis kernel code here
|
||||
// makes callers treat a missing file as a non-negative success value.
|
||||
var errno = result switch
|
||||
{
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT => Einval,
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT => Efault,
|
||||
_ => 2, // ENOENT
|
||||
};
|
||||
KernelRuntimeCompatExports.TrySetErrno(ctx, errno);
|
||||
ctx[CpuRegister.Rax] = ulong.MaxValue;
|
||||
return -1;
|
||||
}
|
||||
public static int PosixStat(CpuContext ctx) => KernelStat(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "gEpBkcwxUjw",
|
||||
@@ -4033,7 +4013,7 @@ public static partial class KernelMemoryCompatExports
|
||||
_ => unchecked((int)argumentSource.NextGpArg())
|
||||
};
|
||||
|
||||
var formatted = value.ToString(CultureInfo.InvariantCulture);
|
||||
var formatted = value.ToString();
|
||||
if (showSign && value >= 0)
|
||||
formatted = "+" + formatted;
|
||||
else if (spaceForSign && value >= 0)
|
||||
@@ -4053,7 +4033,7 @@ public static partial class KernelMemoryCompatExports
|
||||
_ => (uint)argumentSource.NextGpArg()
|
||||
};
|
||||
|
||||
var formatted = value.ToString(CultureInfo.InvariantCulture);
|
||||
var formatted = value.ToString();
|
||||
sb.Append(PadString(formatted, width, leftAlign, padWithZero && !leftAlign));
|
||||
}
|
||||
break;
|
||||
@@ -4070,8 +4050,8 @@ public static partial class KernelMemoryCompatExports
|
||||
};
|
||||
|
||||
var formatted = specifier == 'x'
|
||||
? value.ToString("x", CultureInfo.InvariantCulture)
|
||||
: value.ToString("X", CultureInfo.InvariantCulture);
|
||||
? value.ToString("x")
|
||||
: value.ToString("X");
|
||||
|
||||
if (alternateForm && value != 0)
|
||||
formatted = specifier == 'x' ? "0x" + formatted : "0X" + formatted;
|
||||
@@ -4175,10 +4155,7 @@ public static partial class KernelMemoryCompatExports
|
||||
var formatStr = precision >= 0
|
||||
? $"{{0:{specifier}{precision}}}"
|
||||
: $"{{0:{specifier}}}";
|
||||
var formatted = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
formatStr,
|
||||
value);
|
||||
var formatted = string.Format(formatStr, value);
|
||||
|
||||
if (showSign && value >= 0)
|
||||
formatted = "+" + formatted;
|
||||
@@ -4242,50 +4219,31 @@ public static partial class KernelMemoryCompatExports
|
||||
{
|
||||
private readonly CpuContext _ctx;
|
||||
private int _gpIndex;
|
||||
private int _fpIndex;
|
||||
private int _stackIndex;
|
||||
|
||||
public RegisterPrintfArgumentSource(CpuContext ctx, int gpIndex)
|
||||
{
|
||||
_ctx = ctx;
|
||||
_gpIndex = gpIndex;
|
||||
_fpIndex = 0;
|
||||
_stackIndex = 0;
|
||||
}
|
||||
|
||||
public ulong NextGpArg()
|
||||
{
|
||||
var index = _gpIndex;
|
||||
if (index < 6)
|
||||
var index = _gpIndex++;
|
||||
return index switch
|
||||
{
|
||||
_gpIndex++;
|
||||
return index switch
|
||||
{
|
||||
0 => _ctx[CpuRegister.Rdi],
|
||||
1 => _ctx[CpuRegister.Rsi],
|
||||
2 => _ctx[CpuRegister.Rdx],
|
||||
3 => _ctx[CpuRegister.Rcx],
|
||||
4 => _ctx[CpuRegister.R8],
|
||||
_ => _ctx[CpuRegister.R9],
|
||||
};
|
||||
}
|
||||
|
||||
return ReadStackArg(_ctx, (ulong)(_stackIndex++) * 8);
|
||||
0 => _ctx[CpuRegister.Rdi],
|
||||
1 => _ctx[CpuRegister.Rsi],
|
||||
2 => _ctx[CpuRegister.Rdx],
|
||||
3 => _ctx[CpuRegister.Rcx],
|
||||
4 => _ctx[CpuRegister.R8],
|
||||
5 => _ctx[CpuRegister.R9],
|
||||
_ => ReadStackArg(_ctx, (ulong)(index - 6) * 8)
|
||||
};
|
||||
}
|
||||
|
||||
public double NextFloatArg()
|
||||
{
|
||||
ulong bits;
|
||||
if (_fpIndex < 8)
|
||||
{
|
||||
_ctx.GetXmmRegister(_fpIndex++, out bits, out _);
|
||||
}
|
||||
else
|
||||
{
|
||||
bits = ReadStackArg(_ctx, (ulong)(_stackIndex++) * 8);
|
||||
}
|
||||
|
||||
return BitConverter.Int64BitsToDouble(unchecked((long)bits));
|
||||
return BitConverter.Int64BitsToDouble(unchecked((long)NextGpArg()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4848,19 +4806,8 @@ public static partial class KernelMemoryCompatExports
|
||||
private static bool IsMutatingOpen(int flags) =>
|
||||
(flags & (O_WRONLY | O_RDWR | O_CREAT | O_TRUNC | O_APPEND)) != 0;
|
||||
|
||||
// Dev-build dumps (unpackaged UE titles, etc.) may write their Saved/ tree under
|
||||
// /app0, which is read-only on retail hardware. Opt in via SHARPEMU_WRITABLE_APP0=1
|
||||
// to allow those writes so such dumps can boot; defaults off to keep retail semantics.
|
||||
private static readonly bool _writableApp0 =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_WRITABLE_APP0"), "1", StringComparison.Ordinal);
|
||||
|
||||
public static bool IsReadOnlyGuestMutationPath(string guestPath)
|
||||
{
|
||||
if (_writableApp0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalized = NormalizeGuestStatCachePath(guestPath);
|
||||
return normalized is not null &&
|
||||
(string.Equals(normalized, "/app0", StringComparison.OrdinalIgnoreCase) ||
|
||||
|
||||
@@ -43,19 +43,6 @@ public static class NpWebApi2Exports
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "sk54bi6FtYM",
|
||||
ExportName = "sceNpWebApi2CreateUserContext",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNpWebApi2")]
|
||||
public static int NpWebApi2CreateUserContext(CpuContext ctx)
|
||||
{
|
||||
// No PSN backend: refuse user-context creation so the title's online
|
||||
// layer backs off instead of driving a half-created context handle.
|
||||
TraceNpWebApi2("create-user-context", unchecked((int)ctx[CpuRegister.Rdi]), ctx[CpuRegister.Rsi]);
|
||||
return ctx.SetReturn(NpWebApi2ErrorInvalidArgument);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "bEvXpcEk200",
|
||||
ExportName = "sceNpWebApi2Terminate",
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Pad;
|
||||
@@ -18,19 +16,6 @@ public static class BluetoothHidExports
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
|
||||
// EXPERIMENT: fire the registered callback once with a zeroed event struct.
|
||||
// Direct execution shares the host address space with the guest, so an
|
||||
// AllocHGlobal buffer is directly readable by guest code.
|
||||
// SHARPEMU_BTHID_FIRE_CALLBACK=1 enables; SHARPEMU_BTHID_EVENT_CODE and
|
||||
// SHARPEMU_BTHID_EVENT_SIZE (default 0 / 256) shape the synthetic event.
|
||||
private static readonly bool _fireCallback = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_BTHID_FIRE_CALLBACK"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
|
||||
private static ulong _callbackFunction;
|
||||
private static int _fired;
|
||||
|
||||
private static int Result(CpuContext ctx) =>
|
||||
ctx.SetReturn(_reportUnavailable ? BluetoothHidUnavailable : 0);
|
||||
|
||||
@@ -46,101 +31,12 @@ public static class BluetoothHidExports
|
||||
ExportName = "sceBluetoothHidRegisterDevice",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceBluetoothHid")]
|
||||
public static int BluetoothHidRegisterDevice(CpuContext ctx)
|
||||
{
|
||||
var result = Result(ctx);
|
||||
if (_fireCallback && _callbackFunction >= 0x10000 &&
|
||||
Interlocked.Exchange(ref _fired, 1) == 0)
|
||||
{
|
||||
FireEnumerationComplete(ctx);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
public static int BluetoothHidRegisterDevice(CpuContext ctx) => Result(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "4Ypfo9RIwfM",
|
||||
ExportName = "sceBluetoothHidRegisterCallback",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceBluetoothHid")]
|
||||
public static int BluetoothHidRegisterCallback(CpuContext ctx)
|
||||
{
|
||||
_callbackFunction = ctx[CpuRegister.Rdi];
|
||||
|
||||
// EXPERIMENT: failing ONLY the callback registration (with a generic kernel
|
||||
// error, unlike the BT-specific unavailable code) may make wheel/FFB
|
||||
// middleware disable its Bluetooth search loop instead of polling forever.
|
||||
// SHARPEMU_BTHID_CB_FAIL=nf -> NOT_FOUND, =ni -> NOT_IMPLEMENTED.
|
||||
var mode = Environment.GetEnvironmentVariable("SHARPEMU_BTHID_CB_FAIL");
|
||||
if (string.Equals(mode, "nf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (string.Equals(mode, "ni", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
|
||||
}
|
||||
|
||||
return Result(ctx);
|
||||
}
|
||||
|
||||
private static void FireEnumerationComplete(CpuContext ctx)
|
||||
{
|
||||
var scheduler = GuestThreadExecution.Scheduler;
|
||||
if (scheduler is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var eventCode = ParseEnvUInt64("SHARPEMU_BTHID_EVENT_CODE", 0);
|
||||
var eventSize = (int)ParseEnvUInt64("SHARPEMU_BTHID_EVENT_SIZE", 256);
|
||||
if (eventSize < 1)
|
||||
{
|
||||
eventSize = 256;
|
||||
}
|
||||
|
||||
// Leaked by design: the guest may retain the pointer past the callback.
|
||||
var eventStruct = Marshal.AllocHGlobal(eventSize);
|
||||
for (var offset = 0; offset < eventSize; offset++)
|
||||
{
|
||||
Marshal.WriteByte(eventStruct, offset, 0);
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[BTHID][EXPERIMENT] firing callback=0x{_callbackFunction:X} code={eventCode} size={eventSize} struct=0x{eventStruct.ToInt64():X}");
|
||||
if (!scheduler.TryCallGuestFunction(
|
||||
ctx,
|
||||
_callbackFunction,
|
||||
unchecked((ulong)eventStruct.ToInt64()),
|
||||
eventCode,
|
||||
0,
|
||||
0,
|
||||
"sceBluetoothHid synthetic enumeration event",
|
||||
out var error))
|
||||
{
|
||||
Console.Error.WriteLine($"[BTHID][EXPERIMENT] callback failed: {error}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine("[BTHID][EXPERIMENT] callback returned OK");
|
||||
}
|
||||
}
|
||||
|
||||
private static ulong ParseEnvUInt64(string name, ulong fallback)
|
||||
{
|
||||
var value = Environment.GetEnvironmentVariable(name);
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
|
||||
value = value.Trim();
|
||||
var hex = value.StartsWith("0x", StringComparison.OrdinalIgnoreCase);
|
||||
return ulong.TryParse(
|
||||
hex ? value[2..] : value,
|
||||
hex ? System.Globalization.NumberStyles.HexNumber : System.Globalization.NumberStyles.Integer,
|
||||
null,
|
||||
out var parsed) ? parsed : fallback;
|
||||
}
|
||||
public static int BluetoothHidRegisterCallback(CpuContext ctx) => Result(ctx);
|
||||
}
|
||||
|
||||
@@ -23,11 +23,6 @@ public static class PadExports
|
||||
private const int PrimaryPadHandle = 1;
|
||||
private const int ControllerInformationSize = 0x1C;
|
||||
private const int PadDataSize = 0x78;
|
||||
|
||||
// Real firmware hands out small non-negative handles; 0 is valid. Some titles
|
||||
// (Monster Truck Championship) read pad state with handle 0, and rejecting it
|
||||
// leaves their controller/FFB init path polling a never-valid state forever.
|
||||
private static bool IsPrimaryPadHandle(int handle) => handle is 0 or PrimaryPadHandle;
|
||||
private static readonly long InputSampleIntervalTicks = Math.Max(1, Stopwatch.Frequency / 1000);
|
||||
|
||||
[ThreadStatic]
|
||||
@@ -109,7 +104,7 @@ public static class PadExports
|
||||
public static int PadClose(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return IsPrimaryPadHandle(handle)
|
||||
return handle == PrimaryPadHandle
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
@@ -122,7 +117,7 @@ public static class PadExports
|
||||
public static int PadSetMotionSensorState(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return IsPrimaryPadHandle(handle)
|
||||
return handle == PrimaryPadHandle
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
@@ -136,7 +131,7 @@ public static class PadExports
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var informationAddress = ctx[CpuRegister.Rsi];
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (handle != PrimaryPadHandle)
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
@@ -171,7 +166,7 @@ public static class PadExports
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var informationAddress = ctx[CpuRegister.Rsi];
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (handle != PrimaryPadHandle)
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
@@ -212,7 +207,7 @@ public static class PadExports
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var dataAddress = ctx[CpuRegister.Rsi];
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (handle != PrimaryPadHandle)
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
@@ -237,7 +232,7 @@ public static class PadExports
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var dataAddress = ctx[CpuRegister.Rsi];
|
||||
var count = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (handle != PrimaryPadHandle)
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
@@ -271,7 +266,7 @@ public static class PadExports
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var parameterAddress = ctx[CpuRegister.Rsi];
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (handle != PrimaryPadHandle)
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
@@ -315,7 +310,7 @@ public static class PadExports
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var parameterAddress = ctx[CpuRegister.Rsi];
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (handle != PrimaryPadHandle)
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
@@ -345,7 +340,7 @@ public static class PadExports
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var parameterAddress = ctx[CpuRegister.Rsi];
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (handle != PrimaryPadHandle)
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
@@ -374,7 +369,7 @@ public static class PadExports
|
||||
public static int PadResetLightBar(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!IsPrimaryPadHandle(handle))
|
||||
if (handle != PrimaryPadHandle)
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
@@ -418,18 +418,12 @@ public static class PlayGoExports
|
||||
$"[LOADER][TRACE] playgo.unknown_chunk_id id={chunkId} entries={numberOfEntries} " +
|
||||
$"known=[{string.Join(',', knownChunkIds)}]");
|
||||
}
|
||||
|
||||
// Real firmware rejects chunk ids outside the package's chunk set.
|
||||
// Titles rely on this as an enumeration terminator: Monster Truck
|
||||
// scans ids 0,1,2,... until BAD_CHUNK_ID, and answering OK for every
|
||||
// id makes that scan wrap the ushort range and spin forever.
|
||||
return OrbisPlayGoErrorBadChunkId;
|
||||
}
|
||||
|
||||
loci[i] = PlayGoLocusLocalFast;
|
||||
}
|
||||
|
||||
TracePlayGoLocus(ctx, numberOfEntries, chunkIds, outLoci);
|
||||
TracePlayGoLocus(numberOfEntries, chunkIds, outLoci);
|
||||
return ctx.Memory.TryWrite(outLoci, loci)
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
@@ -669,7 +663,7 @@ public static class PlayGoExports
|
||||
{
|
||||
lock (_stateGate)
|
||||
{
|
||||
return Array.BinarySearch(_metadata.ChunkIds, chunkId) >= 0;
|
||||
return _metadata.ChunkIds.Length == 0 || Array.BinarySearch(_metadata.ChunkIds, chunkId) >= 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -678,9 +672,7 @@ public static class PlayGoExports
|
||||
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
|
||||
if (string.IsNullOrWhiteSpace(app0Root))
|
||||
{
|
||||
// No app0 override to probe for sidecar files: same fully-installed
|
||||
// single-chunk fallback as below, or scePlayGoOpen fails fatally.
|
||||
return new PlayGoMetadata(true, [(ushort)0]);
|
||||
return PlayGoMetadata.Empty;
|
||||
}
|
||||
|
||||
var playGoDat = Path.Combine(app0Root, "sce_sys", "playgo-chunk.dat");
|
||||
@@ -690,23 +682,12 @@ public static class PlayGoExports
|
||||
var hasMetadata = File.Exists(playGoDat) || File.Exists(scenarioJson) || File.Exists(chunkDefsXml);
|
||||
if (!hasMetadata)
|
||||
{
|
||||
// No PlayGo sidecar: report a fully-installed single chunk. Available must
|
||||
// stay true or scePlayGoOpen fails with NotSupportPlayGo (fatal PS5-component
|
||||
// init failure for UE titles); chunk 0 reports LocalFast and every other id
|
||||
// returns BAD_CHUNK_ID, terminating title-side chunk enumeration.
|
||||
TracePlayGo("metadata_missing; fully-installed single chunk");
|
||||
// Full installs may omit PlayGo sidecar metadata.
|
||||
TracePlayGo("metadata_missing; using fully-installed default chunk");
|
||||
return new PlayGoMetadata(true, [(ushort)0]);
|
||||
}
|
||||
|
||||
var chunkIds = LoadChunkIds(chunkDefsXml);
|
||||
if (chunkIds.Length == 0)
|
||||
{
|
||||
// Unreadable/empty sidecar: fall back to chunk 0, not an empty set
|
||||
// (which IsKnownChunkId would treat as "every id valid").
|
||||
TracePlayGo("metadata_unreadable; fully-installed single chunk");
|
||||
return new PlayGoMetadata(true, [(ushort)0]);
|
||||
}
|
||||
|
||||
return new PlayGoMetadata(true, chunkIds);
|
||||
}
|
||||
|
||||
@@ -757,7 +738,7 @@ public static class PlayGoExports
|
||||
}
|
||||
}
|
||||
|
||||
private static void TracePlayGoLocus(CpuContext ctx, uint entries, ulong chunkIds, ulong outLoci)
|
||||
private static void TracePlayGoLocus(uint entries, ulong chunkIds, ulong outLoci)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PLAYGO"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
@@ -767,10 +748,8 @@ public static class PlayGoExports
|
||||
var count = Interlocked.Increment(ref _locusTraceDiagnostics);
|
||||
if (entries != 1 || count <= 32 || count % 1000 == 0)
|
||||
{
|
||||
_ = ctx.TryReadUInt16(chunkIds, out var firstChunkId);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] playgo.get_locus entries={entries} first_chunk={firstChunkId} " +
|
||||
$"chunk_ids=0x{chunkIds:X16} out=0x{outLoci:X16}");
|
||||
$"[LOADER][TRACE] playgo.get_locus entries={entries} chunk_ids=0x{chunkIds:X16} out=0x{outLoci:X16}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -241,64 +241,6 @@ public static class RtcExports
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcGetCurrentRawNetworkTick(CpuContext ctx) => RtcGetCurrentTick(ctx);
|
||||
|
||||
// Diagnostic: middleware busy-wait loops typically poll sceRtcGetCurrentTick, so the
|
||||
// caller's return address pinpoints the loop. SHARPEMU_RTC_PROBE_RANGE=<start>-<end>
|
||||
// (hex guest addresses) dumps 0x100 bytes of code around the first matching caller,
|
||||
// once, for offline disassembly. Costs nothing when the variable is unset.
|
||||
private static readonly ulong[]? _rtcProbeRange = ParseRtcProbeRange();
|
||||
private static int _rtcProbeDone;
|
||||
|
||||
private static ulong[]? ParseRtcProbeRange()
|
||||
{
|
||||
var value = Environment.GetEnvironmentVariable("SHARPEMU_RTC_PROBE_RANGE");
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var parts = value.Split('-', 2, StringSplitOptions.TrimEntries);
|
||||
return parts.Length == 2 &&
|
||||
TryParseHexAddress(parts[0], out var start) &&
|
||||
TryParseHexAddress(parts[1], out var end) &&
|
||||
start < end
|
||||
? [start, end]
|
||||
: null;
|
||||
}
|
||||
|
||||
private static bool TryParseHexAddress(string value, out ulong address)
|
||||
{
|
||||
if (value.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
value = value[2..];
|
||||
}
|
||||
|
||||
return ulong.TryParse(
|
||||
value,
|
||||
System.Globalization.NumberStyles.HexNumber,
|
||||
null,
|
||||
out address);
|
||||
}
|
||||
|
||||
private static void ProbeRtcCaller(CpuContext ctx)
|
||||
{
|
||||
if (Volatile.Read(ref _rtcProbeDone) != 0 ||
|
||||
!ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out var ret) ||
|
||||
ret < _rtcProbeRange![0] ||
|
||||
ret >= _rtcProbeRange[1] ||
|
||||
Interlocked.CompareExchange(ref _rtcProbeDone, 1, 0) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var start = ret - 0x60;
|
||||
Span<byte> code = stackalloc byte[0x100];
|
||||
if (ctx.Memory.TryRead(start, code))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][DIAG] rtc.caller_code ret=0x{ret:X} @0x{start:X}: {System.Convert.ToHexString(code)}");
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "18B2NS1y9UU",
|
||||
ExportName = "sceRtcGetCurrentTick",
|
||||
@@ -312,11 +254,6 @@ public static class RtcExports
|
||||
return unchecked((int)0x80B50002);
|
||||
}
|
||||
|
||||
if (_rtcProbeRange is not null)
|
||||
{
|
||||
ProbeRtcCaller(ctx);
|
||||
}
|
||||
|
||||
var tickValue = unchecked((ulong)(DateTime.UtcNow.Ticks / DateTimeTicksPerMicrosecond));
|
||||
if (!ctx.TryWriteUInt64(tickAddress, tickValue))
|
||||
{
|
||||
|
||||
@@ -15,62 +15,6 @@ public static class SystemServiceExports
|
||||
private const int DisplaySafeAreaInfoSize = sizeof(float) + 128;
|
||||
private const int HdrToneMapLuminanceSize = sizeof(float) * 3;
|
||||
|
||||
private const int TitleIdFieldSize = 0x10;
|
||||
|
||||
private static string? _mainAppTitleId;
|
||||
|
||||
public static void ConfigureApplicationInfo(string? titleId)
|
||||
{
|
||||
_mainAppTitleId = string.IsNullOrWhiteSpace(titleId) ? null : titleId.Trim();
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "3RQ5aQfnstU",
|
||||
ExportName = "sceSystemServiceGetNoticeScreenSkipFlag",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceSystemService")]
|
||||
public static int SystemServiceGetNoticeScreenSkipFlag(CpuContext ctx)
|
||||
{
|
||||
var flagAddress = ctx[CpuRegister.Rdi];
|
||||
if (flagAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSystemServiceErrorParameter);
|
||||
}
|
||||
|
||||
// No system notice screen to skip in the emulator; report "do not skip".
|
||||
Span<byte> flagBytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(flagBytes, 0);
|
||||
return ctx.Memory.TryWrite(flagAddress, flagBytes)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "4veE0XiIugA",
|
||||
ExportName = "sceSystemServiceGetMainAppTitleId",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceSystemService")]
|
||||
public static int SystemServiceGetMainAppTitleId(CpuContext ctx)
|
||||
{
|
||||
var titleIdAddress = ctx[CpuRegister.Rdi];
|
||||
if (titleIdAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSystemServiceErrorParameter);
|
||||
}
|
||||
|
||||
// Title IDs are a fixed 9-char format written into a 0x10-byte field;
|
||||
// bound the length so a malformed param.json cannot drive an unbounded
|
||||
// stack allocation or overrun the guest buffer.
|
||||
var titleId = _mainAppTitleId ?? "PPSA00000";
|
||||
var length = Math.Min(titleId.Length, TitleIdFieldSize - 1);
|
||||
Span<byte> titleIdBytes = stackalloc byte[TitleIdFieldSize];
|
||||
titleIdBytes.Clear();
|
||||
System.Text.Encoding.ASCII.GetBytes(titleId.AsSpan(0, length), titleIdBytes);
|
||||
return ctx.Memory.TryWrite(titleIdAddress, titleIdBytes[..(length + 1)])
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "fZo48un7LK4",
|
||||
ExportName = "sceSystemServiceParamGetInt",
|
||||
|
||||
@@ -2646,12 +2646,6 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
}
|
||||
|
||||
if (PngSplashLoader.TryLoadIcon(out var iconPixels, out var iconWidth, out var iconHeight))
|
||||
{
|
||||
var icon = new RawImage((int)iconWidth, (int)iconHeight, iconPixels);
|
||||
_window.SetWindowIcon(ref icon);
|
||||
}
|
||||
|
||||
WaitForRenderDocAttachIfRequested();
|
||||
_vk = Vk.GetApi();
|
||||
CreateInstance();
|
||||
@@ -4160,48 +4154,6 @@ internal static unsafe class VulkanVideoPresenter
|
||||
_submitTimeline;
|
||||
}
|
||||
|
||||
private void TransitionNewGuestImageToSampled(Image image, uint mipLevels)
|
||||
{
|
||||
var commandBuffer = AllocateGuestCommandBuffer();
|
||||
var beginInfo = new CommandBufferBeginInfo
|
||||
{
|
||||
SType = StructureType.CommandBufferBeginInfo,
|
||||
Flags = CommandBufferUsageFlags.OneTimeSubmitBit,
|
||||
};
|
||||
Check(
|
||||
_vk.BeginCommandBuffer(commandBuffer, &beginInfo),
|
||||
"vkBeginCommandBuffer(guest image init)");
|
||||
var barrier = new ImageMemoryBarrier
|
||||
{
|
||||
SType = StructureType.ImageMemoryBarrier,
|
||||
SrcAccessMask = 0,
|
||||
DstAccessMask = AccessFlags.ShaderReadBit,
|
||||
OldLayout = ImageLayout.Undefined,
|
||||
NewLayout = ImageLayout.ShaderReadOnlyOptimal,
|
||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
Image = image,
|
||||
SubresourceRange = ColorSubresourceRange(0, mipLevels),
|
||||
};
|
||||
_vk.CmdPipelineBarrier(
|
||||
commandBuffer,
|
||||
PipelineStageFlags.TopOfPipeBit,
|
||||
PipelineStageFlags.AllCommandsBit,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
1,
|
||||
&barrier);
|
||||
Check(
|
||||
_vk.EndCommandBuffer(commandBuffer),
|
||||
"vkEndCommandBuffer(guest image init)");
|
||||
// Same-queue submission order makes the transition visible to any
|
||||
// later use of the image; no CPU-side wait is needed.
|
||||
SubmitGuestCommandBuffer(commandBuffer, [], []);
|
||||
}
|
||||
|
||||
private void EnsureGuestSubmissionCapacity()
|
||||
{
|
||||
CollectCompletedGuestSubmissions(waitForOldest: false);
|
||||
@@ -4262,43 +4214,23 @@ internal static unsafe class VulkanVideoPresenter
|
||||
return;
|
||||
}
|
||||
|
||||
if (result == Result.ErrorDeviceLost)
|
||||
{
|
||||
_deviceLost = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Check(result, $"vkWaitForFences(guest: {oldest.DebugName})");
|
||||
}
|
||||
Check(result, $"vkWaitForFences(guest: {oldest.DebugName})");
|
||||
}
|
||||
|
||||
while (_pendingGuestSubmissions.TryPeek(out var submission))
|
||||
{
|
||||
var status = _vk.GetFenceStatus(_device, submission.Fence);
|
||||
if (status == Result.NotReady && !_deviceLost)
|
||||
if (status == Result.NotReady)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (status == Result.ErrorDeviceLost)
|
||||
{
|
||||
// Pending fences never signal on a lost device; retire the
|
||||
// submission anyway so teardown and back-pressure survive.
|
||||
_deviceLost = true;
|
||||
}
|
||||
else if (status != Result.NotReady)
|
||||
{
|
||||
Check(status, $"vkGetFenceStatus(guest: {submission.DebugName})");
|
||||
}
|
||||
|
||||
Check(status, $"vkGetFenceStatus(guest: {submission.DebugName})");
|
||||
_pendingGuestSubmissions.Dequeue();
|
||||
|
||||
if (!_deviceLost)
|
||||
foreach (var image in submission.TraceImages)
|
||||
{
|
||||
foreach (var image in submission.TraceImages)
|
||||
{
|
||||
TraceGuestImageContents(image);
|
||||
}
|
||||
TraceGuestImageContents(image);
|
||||
}
|
||||
|
||||
foreach (var resources in submission.Resources)
|
||||
@@ -4576,20 +4508,13 @@ internal static unsafe class VulkanVideoPresenter
|
||||
$"queue={_activeGuestQueue.Name} submission={_activeGuestQueue.SubmissionId} " +
|
||||
$"handle={work.VideoOutHandle} index={work.DisplayBufferIndex} " +
|
||||
$"capture_complete={(captured ? 1 : 0)}");
|
||||
// Demon's Souls executes wait-safe markers before their flip capture;
|
||||
// an assert here would fail-fast the process, so warn once instead.
|
||||
// Dedup on a flag, not the (per-frame-unique) version, to bound growth.
|
||||
if (work.Version != 0 && !captured && !_loggedFlipWaitOrderViolation)
|
||||
{
|
||||
_loggedFlipWaitOrderViolation = true;
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] vk.flip_wait_order version={work.Version} " +
|
||||
"executed before its flip capture; continuing.");
|
||||
}
|
||||
#if DEBUG
|
||||
System.Diagnostics.Debug.Assert(
|
||||
work.Version == 0 || captured,
|
||||
"An ordered wait-safe marker must execute after its flip capture.");
|
||||
#endif
|
||||
}
|
||||
|
||||
private bool _loggedFlipWaitOrderViolation;
|
||||
|
||||
private GuestImageResource CreateGuestFlipSnapshot(
|
||||
GuestImageResource source,
|
||||
long version)
|
||||
@@ -8369,7 +8294,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
return (properties.OptimalTilingFeatures & FormatFeatureFlags.ColorAttachmentBit) != 0;
|
||||
}
|
||||
|
||||
internal static Format GetTextureFormat(uint format, uint numberType) =>
|
||||
private static Format GetTextureFormat(uint format, uint numberType) =>
|
||||
(format, numberType) switch
|
||||
{
|
||||
(9, _) => Format.A2B10G10R10UnormPack32,
|
||||
@@ -10239,9 +10164,6 @@ internal static unsafe class VulkanVideoPresenter
|
||||
_vk.AllocateMemory(_device, &allocationInfo, null, out var memory),
|
||||
"vkAllocateMemory(offscreen)");
|
||||
Check(_vk.BindImageMemory(_device, image, memory, 0), "vkBindImageMemory(offscreen)");
|
||||
// Rendering and uploads only define the mips they touch; define the whole
|
||||
// chain once so full-chain sampled binds never read Undefined layout.
|
||||
TransitionNewGuestImageToSampled(image, mipLevels);
|
||||
|
||||
var viewInfo = new ImageViewCreateInfo
|
||||
{
|
||||
@@ -11009,7 +10931,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
return view;
|
||||
}
|
||||
|
||||
internal static bool IsCompatibleViewFormat(Format imageFormat, Format viewFormat)
|
||||
private static bool IsCompatibleViewFormat(Format imageFormat, Format viewFormat)
|
||||
{
|
||||
if (imageFormat == viewFormat)
|
||||
{
|
||||
@@ -11075,23 +10997,6 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
|
||||
private void Render(double _)
|
||||
{
|
||||
try
|
||||
{
|
||||
RenderCore();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// Device loss can strike between any two Vulkan calls in the frame;
|
||||
// keep the window loop pumping instead of tearing the presenter down.
|
||||
if (!TryMarkDeviceLost(exception))
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderCore()
|
||||
{
|
||||
if (Volatile.Read(ref _presenterCloseRequested))
|
||||
{
|
||||
@@ -11105,18 +11010,6 @@ internal static unsafe class VulkanVideoPresenter
|
||||
return;
|
||||
}
|
||||
|
||||
if (_deviceLost)
|
||||
{
|
||||
// Drain queued work so producers aren't back-pressured, then
|
||||
// return without any Vulkan call (fences never signal post-loss).
|
||||
while (TryTakeGuestWork(out var lostWork))
|
||||
{
|
||||
CompleteGuestWork(lostWork);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Reuse of a frame slot waits only on that slot's fence, keeping
|
||||
// up to MaxFramesInFlight frames pipelined between CPU and GPU.
|
||||
var frameSlot = _currentFrameSlot;
|
||||
@@ -13937,38 +13830,20 @@ internal static unsafe class VulkanVideoPresenter
|
||||
return;
|
||||
}
|
||||
|
||||
if (result == Result.ErrorDeviceLost)
|
||||
{
|
||||
throw new VulkanDeviceLostException(operation);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"{operation} failed with {result}.");
|
||||
}
|
||||
|
||||
private static void Check(Result result, string operation)
|
||||
{
|
||||
if (result == Result.ErrorDeviceLost)
|
||||
{
|
||||
throw new VulkanDeviceLostException(operation);
|
||||
}
|
||||
|
||||
if (result != Result.Success)
|
||||
{
|
||||
throw new InvalidOperationException($"{operation} failed with {result}.");
|
||||
}
|
||||
}
|
||||
|
||||
// Typed so the frame-boundary catch can recognize device loss without
|
||||
// depending on the exact wording of the exception message.
|
||||
private sealed class VulkanDeviceLostException(string operation)
|
||||
: InvalidOperationException($"{operation} failed with {Result.ErrorDeviceLost}.");
|
||||
|
||||
private bool TryMarkDeviceLost(Exception exception)
|
||||
{
|
||||
// Prefer the typed signal; fall back to the message for losses that
|
||||
// surface through other layers (e.g. Silk.NET bindings).
|
||||
if (exception is not VulkanDeviceLostException &&
|
||||
!exception.Message.Contains(nameof(Result.ErrorDeviceLost), StringComparison.Ordinal))
|
||||
if (!exception.Message.Contains(nameof(Result.ErrorDeviceLost), StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,36 @@ namespace SharpEmu.ShaderCompiler.Vulkan;
|
||||
|
||||
public static class SpirvFixedShaders
|
||||
{
|
||||
/// <summary>Fragment half of the fixed fullscreen barycentric diagnostic draw.</summary>
|
||||
public static byte[] CreateBarycentricFragment()
|
||||
{
|
||||
var module = new SpirvModuleBuilder();
|
||||
module.AddCapability(SpirvCapability.Shader);
|
||||
var voidType = module.TypeVoid();
|
||||
var floatType = module.TypeFloat(32);
|
||||
var vec4Type = module.TypeVector(floatType, 4);
|
||||
var inputPointer = module.TypePointer(SpirvStorageClass.Input, vec4Type);
|
||||
var outputPointer = module.TypePointer(SpirvStorageClass.Output, vec4Type);
|
||||
var barycentric = module.AddGlobalVariable(inputPointer, SpirvStorageClass.Input);
|
||||
module.AddName(barycentric, "barycentric");
|
||||
module.AddDecoration(barycentric, SpirvDecoration.Location, 0);
|
||||
module.AddDecoration(barycentric, SpirvDecoration.NoPerspective);
|
||||
var output = module.AddGlobalVariable(outputPointer, SpirvStorageClass.Output);
|
||||
module.AddName(output, "outColor");
|
||||
module.AddDecoration(output, SpirvDecoration.Location, 0);
|
||||
var functionType = module.TypeFunction(voidType);
|
||||
var main = module.BeginFunction(voidType, functionType);
|
||||
module.AddName(main, "main");
|
||||
module.AddLabel();
|
||||
var value = module.AddInstruction(SpirvOp.Load, vec4Type, barycentric);
|
||||
module.AddStatement(SpirvOp.Store, output, value);
|
||||
module.AddStatement(SpirvOp.Return);
|
||||
module.EndFunction();
|
||||
module.AddEntryPoint(SpirvExecutionModel.Fragment, main, "main", [barycentric, output]);
|
||||
module.AddExecutionMode(main, SpirvExecutionMode.OriginUpperLeft);
|
||||
return module.Build();
|
||||
}
|
||||
|
||||
public static byte[] CreateFullscreenVertex(uint attributeCount)
|
||||
{
|
||||
var module = new SpirvModuleBuilder();
|
||||
|
||||
@@ -1767,9 +1767,9 @@ public static class Gen5ShaderTranslator
|
||||
destinations = [Gen5Operand.Vector(word & 0xFF)];
|
||||
if (opcode == "VReadlaneB32")
|
||||
{
|
||||
// The scalar destination lives in the low vdst byte (bits 0-7);
|
||||
// bits 8-14 are the VOP3B carry-out sdst, which readlane lacks.
|
||||
destinations = [Gen5Operand.Scalar(word & 0xFF)];
|
||||
// VReadlaneB32 writes to scalar destination (bits 8-14), not vector.
|
||||
// Bits 0-7 are unused for this opcode.
|
||||
destinations = [Gen5Operand.Scalar((word >> 8) & 0x7F)];
|
||||
}
|
||||
var isVop3B = IsVop3BOpcode((word >> 16) & 0x3FF);
|
||||
control = new Gen5Vop3Control(
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Kernel;
|
||||
|
||||
public sealed class KernelMemoryCompatExportsTests
|
||||
{
|
||||
[Fact]
|
||||
public void PosixStat_MissingFileReturnsMinusOne()
|
||||
{
|
||||
const ulong memoryBase = 0x1_0000_0000;
|
||||
const ulong pathAddress = memoryBase + 0x100;
|
||||
const ulong statAddress = memoryBase + 0x400;
|
||||
var memory = new FakeCpuMemory(memoryBase, 0x1000);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
memory.WriteCString(pathAddress, "/__sharpemu_test_missing__/shader.cache");
|
||||
context[CpuRegister.Rdi] = pathAddress;
|
||||
context[CpuRegister.Rsi] = statAddress;
|
||||
|
||||
var result = KernelMemoryCompatExports.PosixStat(context);
|
||||
|
||||
Assert.Equal(-1, result);
|
||||
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sprintf_ReadsVariadicDoubleFromXmmRegister()
|
||||
{
|
||||
const ulong memoryBase = 0x1_0000_0000;
|
||||
const ulong destinationAddress = memoryBase + 0x100;
|
||||
const ulong formatAddress = memoryBase + 0x200;
|
||||
var memory = new FakeCpuMemory(memoryBase, 0x1000);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
memory.WriteCString(formatAddress, "%.4f");
|
||||
context[CpuRegister.Rdi] = destinationAddress;
|
||||
context[CpuRegister.Rsi] = formatAddress;
|
||||
context.SetXmmRegister(
|
||||
0,
|
||||
unchecked((ulong)BitConverter.DoubleToInt64Bits(0.5576)),
|
||||
0);
|
||||
|
||||
var result = KernelMemoryCompatExports.Sprintf(context);
|
||||
|
||||
Assert.Equal(0, result);
|
||||
Assert.Equal(6UL, context[CpuRegister.Rax]);
|
||||
Span<byte> output = stackalloc byte[7];
|
||||
Assert.True(memory.TryRead(destinationAddress, output));
|
||||
Assert.Equal("0.5576\0", Encoding.UTF8.GetString(output));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sprintf_UsesCLocaleForFloatingPoint()
|
||||
{
|
||||
var previousCulture = CultureInfo.CurrentCulture;
|
||||
try
|
||||
{
|
||||
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("tr-TR");
|
||||
|
||||
const ulong memoryBase = 0x1_0000_0000;
|
||||
const ulong destinationAddress = memoryBase + 0x100;
|
||||
const ulong formatAddress = memoryBase + 0x200;
|
||||
var memory = new FakeCpuMemory(memoryBase, 0x1000);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
memory.WriteCString(formatAddress, "%.4f");
|
||||
context[CpuRegister.Rdi] = destinationAddress;
|
||||
context[CpuRegister.Rsi] = formatAddress;
|
||||
context.SetXmmRegister(
|
||||
0,
|
||||
unchecked((ulong)BitConverter.DoubleToInt64Bits(0.5576)),
|
||||
0);
|
||||
|
||||
var result = KernelMemoryCompatExports.Sprintf(context);
|
||||
|
||||
Assert.Equal(0, result);
|
||||
Span<byte> output = stackalloc byte[7];
|
||||
Assert.True(memory.TryRead(destinationAddress, output));
|
||||
Assert.Equal("0.5576\0", Encoding.UTF8.GetString(output));
|
||||
}
|
||||
finally
|
||||
{
|
||||
CultureInfo.CurrentCulture = previousCulture;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user