Compare commits

..

57 Commits

Author SHA1 Message Date
ParantezTech 0666071f79 [Tests] Cover backward red-zone spans, direct memory no-space results and the mount info size 2026-10-09 01:27:52 +03:00
ParantezTech ee9660d81d [Loader] Patch red-zone sites from the previous instruction when every indirect branch is resolved 2026-10-09 01:27:52 +03:00
ParantezTech 28b2f5709d [Kernel] Add sceKernelSync
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-09 01:01:48 +03:00
ParantezTech f7e8f54dbc [Stubs] Add sceTextToSpeech2Cancel and sceTextToSpeech2GetSpeechStatus
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-09 01:01:48 +03:00
ParantezTech afd0dffcf6 [SaveData] Write the mount info structure at its 48-byte guest size 2026-10-09 01:01:48 +03:00
ParantezTech cde859c5e5 [Tests] Cover random devices, POSIX open, UDS initialization, pad class data and exception contexts
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:41 +03:00
ParantezTech 757567bd15 [Pad] Add scePadDeviceClassParseData
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:41 +03:00
ParantezTech 62bbf51bbc [Pad] Write the device class extended information at its guest size
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:40 +03:00
ParantezTech c1a0da742f [NP] Reset Universal Data System state with the runtime
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:40 +03:00
ParantezTech ad0fd8be7b [NP] Create Universal Data System contexts before explicit initialization
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:40 +03:00
ParantezTech 6f2937936a [NP] Keep Universal Data System handles across repeated initialization
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:40 +03:00
ParantezTech 13b77eedcb [Cpu] Expose the guest exception context writer to tests
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:40 +03:00
ParantezTech a7bfef97ac [Cpu] Report the flags and Sony stack alias in guest exception contexts
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:40 +03:00
ParantezTech 10254a3c7e [Cpu] Place the guest exception mcontext at the start of the context
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:40 +03:00
ParantezTech 3d65921eb7 [Kernel] Report random device descriptors in fstat
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:40 +03:00
ParantezTech cd77a68c87 [Kernel] Close random device descriptors
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:39 +03:00
ParantezTech a662295409 [Kernel] Read random device descriptors from the host RNG
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:39 +03:00
ParantezTech 4d8f5a75d5 [Kernel] Open /dev/random and /dev/urandom as host RNG descriptors
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:39 +03:00
ParantezTech d3deab28aa [Kernel] Return POSIX results from _open and keep sceKernelOpen raw
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:39 +03:00
ParantezTech 8c0ca1a2a7 [Kernel] Clear the outputs and report no space when no direct memory span is free
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:39 +03:00
ParantezTech 9d530f63f9 [Tests] Cover controller ports, user exports, trials and open-file renames
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 18:25:29 +03:00
ParantezTech f945215935 [Kernel] Allow renaming and unlinking open guest files
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 18:25:29 +03:00
ParantezTech c37f76f12d [NP] Expose the game trials flag 2026-10-08 18:25:28 +03:00
ParantezTech 73666f529e [UserService] Add user number and termination exports
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 18:25:27 +03:00
ParantezTech 0e55fa924c [Pad] Track independent standard, special and remote port handles
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 18:25:26 +03:00
ParantezTech d3f584a2f1 [Tests] Match red-zone coverage to forward-only patching 2026-10-08 18:25:26 +03:00
ParantezTech dd6a936a12 [Tests] Cover Gen5 tiling, DCC clears and offline authentication
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:32:02 +03:00
ParantezTech c0028d304b [GPU] Materialize DCC slice clears from guest metadata
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:32:01 +03:00
ParantezTech ef10b4a1d8 [GPU] Track DCC metadata coverage for color targets and volume textures
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:32:00 +03:00
ParantezTech 7be8a4b389 [GPU] Decode DCC fixed and register clear values
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:31:58 +03:00
ParantezTech 1984901a31 [GPU] Use Gen5 render-target tile equations on the CPU and GPU
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:31:57 +03:00
ParantezTech 7b30868b03 [SpirvTranslator] Preserve F32 signed zero, infinity and NaN when supported
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:31:37 +03:00
ParantezTech dc3b1d305d [Loader] Report patching progress for large executables 2026-10-08 17:31:36 +03:00
ParantezTech 7866bd4217 [Loader] Preserve switch targets while patching guest red-zone accesses
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:31:35 +03:00
ParantezTech 30429400ed [PlayGo] Add install and optional chunk queries
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:30:10 +03:00
ParantezTech 78792d7a5a [PlayGo] Expose runtime state reset
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:30:09 +03:00
ParantezTech 7c08757173 [PlayGo] Keep chunk metadata parsing in the metadata loader
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:30:07 +03:00
ParantezTech 2a2ea5a35a [PlayGo] Keep unknown chunk lists empty
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:28:29 +03:00
ParantezTech 34e8900035 [Pad] Trace input state changes when requested 2026-10-08 17:28:28 +03:00
ParantezTech 923f75d340 [Pad] Add the n3kSX62fgNo compatibility export 2026-10-08 17:28:27 +03:00
ParantezTech 946bd0a777 [Json] Add the special-float format setter
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:28:26 +03:00
ParantezTech d31138fcca [Json] Track the global special-float handler
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:28:24 +03:00
ParantezTech 077c9a940f [Network] Add Ethernet address string conversion
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:28:23 +03:00
ParantezTech ea1dcbcea1 [Network] Close POSIX socket descriptors through sceNetSocketClose
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:28:22 +03:00
ParantezTech 53f233da9b [Network] Add socket epoll registration and readiness waits
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:28:05 +03:00
ParantezTech b368247d64 [SpirvTranslator] Match guest F32 denormal and trigonometric semantics
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:26:59 +03:00
ParantezTech c566dba5a1 [VideoOut] Add deduplicated volume texture binding traces 2026-10-08 17:26:58 +03:00
ParantezTech 8d884f2692 [GPU] Skip piece hashing for stencil association proxies 2026-10-08 17:26:56 +03:00
ParantezTech 679ea0f355 [Kernel] Warn when APR cannot resolve a resource path 2026-10-08 17:26:55 +03:00
ParantezTech 124dfa8afd [Np] Complete offline authentication requests as signed out 2026-10-08 17:26:54 +03:00
ParantezTech 1b66707daf [SharePlay] Report inactive share-play state
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:26:53 +03:00
ParantezTech 619ac2b367 [Network] Add the RUDP context lifecycle
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:26:52 +03:00
ParantezTech a1c0d7d36d [Videodec2] Decode guest access units into pitched NV12 frames
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:26:51 +03:00
ParantezTech 6cc753e0fc [AppContent] Report missing add-on content entitlements
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:26:50 +03:00
ParantezTech beedb746d9 [Kernel] Expose the process argument count and vector
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:26:49 +03:00
ParantezTech 82c9731f74 [Libc] Add file-lock compatibility exports 2026-10-08 17:26:48 +03:00
ParantezTech d708227541 [Cpu] Prefer LLE for libc FILE-object exports 2026-10-08 17:26:47 +03:00
10 changed files with 51 additions and 540 deletions

1
.gitignore vendored
View File

@ -9,7 +9,6 @@
[Aa]rtifacts/
logs/
.tmp/
src/SharpEmu.Demo/
x86/
x64/

View File

@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SharpEmuVersion>0.0.5-nexus-release.2</SharpEmuVersion>
<SharpEmuVersion>0.0.5-nexus</SharpEmuVersion>
<Version>$(SharpEmuVersion)</Version>
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>

View File

@ -180,71 +180,4 @@ SPDX-License-Identifier: GPL-2.0-or-later
SkipUnchangedFiles="true" />
</Target>
<PropertyGroup>
<SharpEmuDemoTag>e3a6dd3</SharpEmuDemoTag>
<SharpEmuDemoFolderName>demo</SharpEmuDemoFolderName>
<SharpEmuDemoCacheDir>$(BaseIntermediateOutputPath)sharpemu-demo/$(SharpEmuDemoTag)</SharpEmuDemoCacheDir>
<SharpEmuDemoArchive>$(SharpEmuDemoCacheDir)/sharpemu-demo.zip</SharpEmuDemoArchive>
<SharpEmuDemoDir>$([MSBuild]::NormalizeDirectory('$(MSBuildProjectDirectory)', '..', 'SharpEmu.Demo'))</SharpEmuDemoDir>
<SharpEmuDemoStamp>$(SharpEmuDemoDir).tag</SharpEmuDemoStamp>
</PropertyGroup>
<Target Name="FetchSharpEmuDemo" BeforeTargets="Publish;Build">
<ReadLinesFromFile File="$(SharpEmuDemoStamp)" Condition="Exists('$(SharpEmuDemoStamp)')">
<Output TaskParameter="Lines" PropertyName="_SharpEmuDemoExtractedTag" />
</ReadLinesFromFile>
<PropertyGroup>
<_SharpEmuDemoCurrent Condition="'$(SharpEmuDemoTag)' != '' And '$(_SharpEmuDemoExtractedTag)' == '$(SharpEmuDemoTag)'">true</_SharpEmuDemoCurrent>
</PropertyGroup>
<DownloadFile
SourceUrl="https://github.com/sharpemu/sharpemu-demo/releases/download/$(SharpEmuDemoTag)/sharpemu-demo.zip"
DestinationFolder="$(SharpEmuDemoCacheDir)"
Condition="'$(SharpEmuDemoTag)' != '' And '$(_SharpEmuDemoCurrent)' != 'true' And !Exists('$(SharpEmuDemoArchive)')"
ContinueOnError="WarnAndContinue" />
<RemoveDir Directories="$(SharpEmuDemoDir)" Condition="'$(_SharpEmuDemoCurrent)' != 'true'" />
<Unzip
SourceFiles="$(SharpEmuDemoArchive)"
DestinationFolder="$(SharpEmuDemoDir)"
Condition="'$(SharpEmuDemoTag)' != '' And '$(_SharpEmuDemoCurrent)' != 'true' And Exists('$(SharpEmuDemoArchive)')"
ContinueOnError="WarnAndContinue" />
<PropertyGroup>
<SharpEmuDemoComplete Condition="Exists('$(SharpEmuDemoDir)eboot.bin') And Exists('$(SharpEmuDemoDir)sce_sys/param.json') And Exists('$(SharpEmuDemoDir)sce_sys/icon0.png') And Exists('$(SharpEmuDemoDir)sce_sys/pic0.png')">true</SharpEmuDemoComplete>
</PropertyGroup>
<WriteLinesToFile
File="$(SharpEmuDemoStamp)"
Lines="$(SharpEmuDemoTag)"
Overwrite="true"
Condition="'$(SharpEmuDemoComplete)' == 'true' And '$(_SharpEmuDemoCurrent)' != 'true'" />
<RemoveDir Directories="$(SharpEmuDemoDir)" Condition="'$(SharpEmuDemoComplete)' != 'true'" />
<Delete Files="$(SharpEmuDemoArchive)" Condition="'$(SharpEmuDemoComplete)' != 'true' And Exists('$(SharpEmuDemoArchive)')" />
</Target>
<Target Name="BuildSharpEmuDemo" AfterTargets="Build" DependsOnTargets="FetchSharpEmuDemo">
<ItemGroup>
<_SharpEmuDemoFiles Include="$(SharpEmuDemoDir)**/*" Exclude="$(SharpEmuDemoStamp)" Condition="'$(SharpEmuDemoComplete)' == 'true'" />
</ItemGroup>
<RemoveDir
Directories="$(OutDir)$(SharpEmuDemoFolderName)"
Condition="'$(SharpEmuDemoComplete)' != 'true'"
ContinueOnError="WarnAndContinue" />
<Copy
SourceFiles="@(_SharpEmuDemoFiles)"
DestinationFiles="@(_SharpEmuDemoFiles->'$(OutDir)$(SharpEmuDemoFolderName)/%(RecursiveDir)%(Filename)%(Extension)')"
SkipUnchangedFiles="true" />
</Target>
<Target Name="PublishSharpEmuDemo" AfterTargets="Publish" DependsOnTargets="FetchSharpEmuDemo">
<ItemGroup>
<_SharpEmuDemoFiles Include="$(SharpEmuDemoDir)**/*" Exclude="$(SharpEmuDemoStamp)" Condition="'$(SharpEmuDemoComplete)' == 'true'" />
</ItemGroup>
<RemoveDir
Directories="$(PublishDir)$(SharpEmuDemoFolderName)"
Condition="'$(SharpEmuDemoComplete)' != 'true'"
ContinueOnError="WarnAndContinue" />
<Copy
SourceFiles="@(_SharpEmuDemoFiles)"
DestinationFiles="@(_SharpEmuDemoFiles->'$(PublishDir)$(SharpEmuDemoFolderName)/%(RecursiveDir)%(Filename)%(Extension)')"
SkipUnchangedFiles="true" />
</Target>
</Project>

View File

@ -1,34 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.IO;
namespace SharpEmu.GUI;
internal static class BundledDemo
{
internal const string FolderName = "demo";
private static readonly string[] RequiredFiles =
[
"eboot.bin",
Path.Combine("sce_sys", "param.json"),
Path.Combine("sce_sys", "icon0.png"),
Path.Combine("sce_sys", "pic0.png"),
Path.Combine("sce_sys", "snd0.at9"),
];
internal static string? FindEboot(string baseDirectory)
{
var root = Path.Combine(baseDirectory, FolderName);
foreach (var file in RequiredFiles)
{
if (!File.Exists(Path.Combine(root, file)))
{
return null;
}
}
return Path.GetFullPath(Path.Combine(root, "eboot.bin"));
}
}

View File

@ -15,7 +15,6 @@ namespace SharpEmu.GUI;
public sealed class LibraryTileCollection : IReadOnlyList<LibraryTile>, IList, INotifyCollectionChanged
{
private readonly ObservableCollection<GameEntry> _games;
private bool _includeAddFolderTile = true;
public LibraryTileCollection(ObservableCollection<GameEntry> games)
{
@ -25,22 +24,7 @@ public sealed class LibraryTileCollection : IReadOnlyList<LibraryTile>, IList, I
public event NotifyCollectionChangedEventHandler? CollectionChanged;
public bool IncludeAddFolderTile
{
get => _includeAddFolderTile;
set
{
if (_includeAddFolderTile == value)
{
return;
}
_includeAddFolderTile = value;
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
public int Count => _games.Count + (_includeAddFolderTile ? 1 : 0);
public int Count => _games.Count + 1;
bool IList.IsFixedSize => false;
@ -59,7 +43,7 @@ public sealed class LibraryTileCollection : IReadOnlyList<LibraryTile>, IList, I
return _games[index];
}
if (_includeAddFolderTile && index == _games.Count)
if (index == _games.Count)
{
return AddFolderTile.Instance;
}
@ -81,10 +65,7 @@ public sealed class LibraryTileCollection : IReadOnlyList<LibraryTile>, IList, I
yield return game;
}
if (_includeAddFolderTile)
{
yield return AddFolderTile.Instance;
}
yield return AddFolderTile.Instance;
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
@ -98,7 +79,7 @@ public sealed class LibraryTileCollection : IReadOnlyList<LibraryTile>, IList, I
int IList.IndexOf(object? value) => value switch
{
GameEntry game => _games.IndexOf(game),
AddFolderTile when _includeAddFolderTile => _games.Count,
AddFolderTile => _games.Count,
_ => -1,
};

View File

@ -299,21 +299,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
</DataTemplate>
</ListBox.DataTemplates>
</ListBox>
<Button x:Name="CarouselAddFolderButton"
Classes="ghost"
HorizontalAlignment="Left"
VerticalAlignment="Top"
IsVisible="False"
AutomationProperties.Name="{Binding [Library.AddFolder], Source={x:Static local:Localization.Instance}}">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="+"
FontSize="16"
FontWeight="Light"
VerticalAlignment="Center" />
<TextBlock Text="{Binding [Library.AddFolder], Source={x:Static local:Localization.Instance}}"
VerticalAlignment="Center" />
</StackPanel>
</Button>
</Panel>
<!-- Selected-game identity and actions share one detail surface so

View File

@ -178,19 +178,6 @@ public partial class MainWindow : Window
private GameEntry? _lastSelectedGame;
private double _libraryRailRowHeight = 188;
private double _libraryGridRowHeight = 216;
private const double CarouselLeadingPadding = 20;
private const double CarouselItemMargin = 12;
private bool _carouselAnimating;
private readonly TranslateTransform _carouselTransform = new();
private double _carouselOffset;
private double _carouselScrollFrom;
private double _carouselScrollTo;
private long _carouselScrollStartedAt;
private bool _carouselPointerDown;
private bool _carouselDragging;
private Point _carouselDragStart;
private double _carouselDragStartOffset;
private double _carouselWheelAccumulator;
// Bundled key art shown whenever no game-specific backdrop applies; the
// plain window color remains the fallback when the asset fails to load.
@ -259,27 +246,6 @@ public partial class MainWindow : Window
_libraryLayoutTimer.Stop();
UpdateLibraryLayoutMetrics();
};
CarouselAddFolderButton.Click += (_, _) => StartAddFolderFromTile();
GameList.AddHandler(PointerPressedEvent, OnCarouselPointerPressed, RoutingStrategies.Tunnel, handledEventsToo: true);
GameList.AddHandler(PointerMovedEvent, OnCarouselPointerMoved, RoutingStrategies.Tunnel, handledEventsToo: true);
GameList.AddHandler(PointerReleasedEvent, OnCarouselPointerReleased, RoutingStrategies.Tunnel, handledEventsToo: true);
GameList.AddHandler(
PointerCaptureLostEvent,
(_, e) =>
{
if (ReferenceEquals(e.Source, GameList))
{
EndCarouselDrag(select: false);
}
},
RoutingStrategies.Tunnel | RoutingStrategies.Bubble,
handledEventsToo: true);
LibraryPage.AddHandler(PointerWheelChangedEvent, OnCarouselPointerWheel, RoutingStrategies.Tunnel, handledEventsToo: true);
GameList.SizeChanged += (_, _) =>
{
UpdateCarouselPadding();
Dispatcher.UIThread.Post(() => AnchorCarouselSelection(animate: false), DispatcherPriority.Loaded);
};
TitleBar.PointerPressed += OnTitleBarPointerPressed;
TitleBar.DoubleTapped += OnTitleBarDoubleTapped;
@ -410,7 +376,13 @@ public partial class MainWindow : Window
Closing += (_, _) => BeginWindowClosing();
Closed += (_, _) => CompleteWindowClosing();
SdlLauncherGamepad.EnsureStarted();
_gamepadTimer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(50),
};
EnvRenderDocToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle(
"SHARPEMU_RENDERDOC",
EnvRenderDocToggle.IsChecked == true);
@ -418,6 +390,24 @@ public partial class MainWindow : Window
SetEnvironmentToggle(
"SHARPEMU_VK_DISABLE_IMPLICITS",
EnvDisableVkImplicitLayersToggle.IsChecked == true);
DefaultProfileBox.TextChanged += (_, _) =>
_settings.DefaultProfile = GuiSettings.NormalizeDefaultProfile(DefaultProfileBox.Text);
LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel);
AddHandler(KeyDownEvent, OnPreviewKeyDown, RoutingStrategies.Tunnel);
CtxLaunch.Click += (_, _) => LaunchSelected();
CtxOpenFolder.Click += (_, _) => OpenSelectedGameFolder();
CtxCopyPath.Click += async (_, _) =>
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.Path);
CtxCopyTitleId.Click += async (_, _) =>
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.TitleId);
CtxGameSettings.Click += (_, _) => OpenSelectedGameSettings();
CtxRemove.Click += (_, _) => RemoveSelectedFromLibrary();
Opened += async (_, _) => await OnOpenedAsync();
Closing += (_, _) => BeginWindowClosing();
Closed += (_, _) => CompleteWindowClosing();
SdlLauncherGamepad.EnsureStarted();
_gamepadTimer = new DispatcherTimer
@ -502,25 +492,12 @@ public partial class MainWindow : Window
() => SetOptionsNavigationIndicator(_optionsSectionIndex, animate: false),
DispatcherPriority.Loaded);
}
else
{
FocusLibrarySelection();
}
}
private void SetLibraryLayout(bool grid)
{
_isLibraryGridLayout = grid;
var selectedTile = GameList.SelectedItem;
_libraryTiles.IncludeAddFolderTile = grid;
if (selectedTile is GameEntry && !ReferenceEquals(GameList.SelectedItem, selectedTile))
{
GameList.SelectedItem = selectedTile;
}
CarouselAddFolderButton.IsVisible = !grid;
SetClass(GameList, "gridLayout", grid);
UpdateCarouselPadding();
SetClass(LibrarySelectedDetails, "gridLayout", grid);
LibraryPage.Margin = new Thickness(0, 6, 0, 0);
UpdateLibraryLayoutMetrics();
@ -529,11 +506,7 @@ public partial class MainWindow : Window
if (GameList.SelectedItem is { } selected)
{
Dispatcher.UIThread.Post(
() =>
{
GameList.ScrollIntoView(selected);
AnchorCarouselSelection(animate: false);
},
() => GameList.ScrollIntoView(selected),
DispatcherPriority.Loaded);
}
}
@ -545,17 +518,8 @@ public partial class MainWindow : Window
Resources["LibraryTileWidth"] = metrics.ItemWidth;
Resources["LibraryRailItemHeight"] = metrics.RailItemHeight;
Resources["LibraryGridItemHeight"] = metrics.GridItemHeight;
_libraryRailRowHeight = metrics.RailItemHeight + 16 + 48;
CarouselAddFolderButton.Margin = new Thickness(0, metrics.RailItemHeight + 24, 0, 0);
_libraryRailRowHeight = metrics.RailItemHeight + 16;
_libraryGridRowHeight = metrics.GridItemHeight + 16;
UpdateCarouselPadding();
Dispatcher.UIThread.Post(
() =>
{
UpdateCarouselPadding();
AnchorCarouselSelection(animate: false);
},
DispatcherPriority.Loaded);
GameList.MaxHeight = double.PositiveInfinity;
UpdateLibraryMinimumHeight();
@ -886,28 +850,11 @@ public partial class MainWindow : Window
if (ShouldNavigate(left, ref _navLeftNextAt, now))
{
MoveSelection(-1);
ReturnFocusFromCarouselAddFolder();
}
if (ShouldNavigate(right, ref _navRightNextAt, now))
{
MoveSelection(1);
ReturnFocusFromCarouselAddFolder();
}
if (!_isLibraryGridLayout)
{
var up = (pad.Buttons & HostGamepadButtons.Up) != 0 || pad.LeftY < 64;
var down = (pad.Buttons & HostGamepadButtons.Down) != 0 || pad.LeftY > 192;
if (ShouldNavigate(down, ref _navDownNextAt, now))
{
CarouselAddFolderButton.Focus(NavigationMethod.Directional);
}
if (ShouldNavigate(up, ref _navUpNextAt, now))
{
ReturnFocusFromCarouselAddFolder();
}
}
if (_isLibraryGridLayout)
@ -930,14 +877,7 @@ public partial class MainWindow : Window
var pressed = pad.Buttons & ~_previousPadButtons;
if ((pressed & HostGamepadButtons.Cross) != 0)
{
if (!_isLibraryGridLayout && CarouselAddFolderButton.IsFocused)
{
StartAddFolderFromTile();
}
else
{
LaunchSelected();
}
LaunchSelected();
}
_previousPadButtons = pad.Buttons;
@ -1000,10 +940,6 @@ public partial class MainWindow : Window
: Math.Clamp(GameList.SelectedIndex + delta, 0, _libraryTiles.Count - 1);
GameList.SelectedIndex = index;
GameList.ScrollIntoView(index);
if (GameList.IsKeyboardFocusWithin && GameList.ContainerFromIndex(index) is { } container)
{
container.Focus(NavigationMethod.Unspecified);
}
}
private async Task OnOpenedAsync()
@ -1017,7 +953,6 @@ public partial class MainWindow : Window
: $" · UNOFFICIAL {BuildInfo.CommitSha}";
VersionText.Text = display;
Title = $"SharpEmu {display}";
ReportGuiRenderer();
ToolTip.SetTip(VersionText, BuildInfo.Banner);
_settings = GuiSettings.Load();
@ -1035,66 +970,7 @@ public partial class MainWindow : Window
}
SeedLibraryFromCache();
FocusLibrarySelection();
await RescanLibraryAsync();
FocusLibrarySelection();
}
private void ReturnFocusFromCarouselAddFolder()
{
if (CarouselAddFolderButton.IsFocused)
{
FocusLibrarySelection();
}
}
private void ReportGuiRenderer()
{
if (!OperatingSystem.IsWindows())
{
return;
}
try
{
var angle = Process.GetCurrentProcess().Modules
.Cast<ProcessModule>()
.Any(static module => string.Equals(module.ModuleName, "av_libglesv2.dll", StringComparison.OrdinalIgnoreCase));
AppendConsoleLine(
angle ? "[GUI] Renderer: GPU (ANGLE/Direct3D)" : "[GUI] Renderer: software (ANGLE not loaded)",
angle ? DimLineBrush : WarningLineBrush);
}
catch (Exception)
{
}
}
private void FocusLibrarySelection()
{
Dispatcher.UIThread.Post(
() =>
{
if (_activePageIndex != 0 || _isGameSettingsOpen)
{
return;
}
if (GameList.SelectedIndex < 0 && _visibleGames.Count > 0)
{
GameList.SelectedIndex = 0;
}
AnchorCarouselSelection(animate: false);
if (GameList.SelectedIndex >= 0 && GameList.ContainerFromIndex(GameList.SelectedIndex) is { } container)
{
container.Focus(NavigationMethod.Unspecified);
}
else
{
GameList.Focus(NavigationMethod.Unspecified);
}
},
DispatcherPriority.Loaded);
}
private void PopulateLanguageBox()
@ -2008,7 +1884,21 @@ public partial class MainWindow : Window
continue;
}
games.Add(CreateGameEntry(fullPath));
long size = 0;
try
{
size = new FileInfo(fullPath).Length;
}
catch (IOException exception)
{
Console.Error.WriteLine(
$"[GUI][WARN] Could not inspect executable '{fullPath}': {exception.Message}");
}
var (title, titleId, version) = TryReadParamJson(fullPath);
games.Add(new GameEntry(
title ?? GameNameFor(fullPath), titleId, version, fullPath, size,
FindCoverFor(fullPath), FindBackgroundFor(fullPath)));
}
}
catch (Exception exception)
@ -2019,41 +1909,9 @@ public partial class MainWindow : Window
}
games.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase));
AddBundledDemo(games, excludedPaths);
return games;
}
private static void AddBundledDemo(List<GameEntry> games, IReadOnlySet<string> excludedPaths)
{
var demo = BundledDemo.FindEboot(AppContext.BaseDirectory);
if (demo is null || excludedPaths.Contains(demo))
{
return;
}
games.RemoveAll(game => string.Equals(game.Path, demo, GameLibraryPath.Comparison));
games.Insert(0, CreateGameEntry(demo));
}
private static GameEntry CreateGameEntry(string fullPath)
{
long size = 0;
try
{
size = new FileInfo(fullPath).Length;
}
catch (IOException exception)
{
Console.Error.WriteLine(
$"[GUI][WARN] Could not inspect executable '{fullPath}': {exception.Message}");
}
var (title, titleId, version) = TryReadParamJson(fullPath);
return new GameEntry(
title ?? GameNameFor(fullPath), titleId, version, fullPath, size,
FindCoverFor(fullPath), FindBackgroundFor(fullPath));
}
/// <summary>
/// Reads the game title, title id and content version from
/// sce_sys/param.json next to the executable, when present.
@ -2225,195 +2083,6 @@ public partial class MainWindow : Window
_lastSelectedGame = GameList.SelectedItem as GameEntry;
UpdateSelectedGame();
Dispatcher.UIThread.Post(() => AnchorCarouselSelection(animate: true), DispatcherPriority.Loaded);
}
private double CarouselItemStride() =>
(Resources.TryGetValue("LibraryTileWidth", out var width) && width is double tileWidth ? tileWidth : 0) +
CarouselItemMargin;
private void UpdateCarouselPadding()
{
if (GameList.ItemsPanelRoot is not { } panel)
{
return;
}
if (_isLibraryGridLayout)
{
panel.ClearValue(MarginProperty);
if (ReferenceEquals(panel.RenderTransform, _carouselTransform))
{
panel.RenderTransform = null;
}
return;
}
panel.Margin = new Thickness(CarouselLeadingPadding, 0, 0, 0);
if (!ReferenceEquals(panel.RenderTransform, _carouselTransform))
{
panel.RenderTransform = _carouselTransform;
}
SetCarouselOffset(_carouselOffset);
}
private double MaximumCarouselOffset() =>
Math.Max(0, (_visibleGames.Count - 1) * CarouselItemStride());
private void SetCarouselOffset(double offset)
{
_carouselOffset = Math.Clamp(offset, 0, MaximumCarouselOffset());
_carouselTransform.X = -_carouselOffset;
}
private void AnchorCarouselSelection(bool animate)
{
if (_isLibraryGridLayout || GameList.SelectedIndex < 0 || _carouselPointerDown)
{
return;
}
var target = Math.Clamp(GameList.SelectedIndex * CarouselItemStride(), 0, MaximumCarouselOffset());
if (!animate)
{
_carouselAnimating = false;
SetCarouselOffset(target);
return;
}
_carouselScrollFrom = _carouselOffset;
_carouselScrollTo = target;
_carouselScrollStartedAt = Stopwatch.GetTimestamp();
if (!_carouselAnimating)
{
_carouselAnimating = true;
RequestAnimationFrame(OnCarouselAnimationFrame);
}
}
private void OnCarouselPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (_isLibraryGridLayout || !e.GetCurrentPoint(GameList).Properties.IsLeftButtonPressed)
{
return;
}
_carouselPointerDown = true;
_carouselDragging = false;
_carouselDragStart = e.GetPosition(GameList);
_carouselDragStartOffset = _carouselOffset;
}
private void OnCarouselPointerMoved(object? sender, PointerEventArgs e)
{
if (!_carouselPointerDown)
{
return;
}
var deltaX = e.GetPosition(GameList).X - _carouselDragStart.X;
if (!_carouselDragging)
{
if (Math.Abs(deltaX) < 8)
{
return;
}
_carouselDragging = true;
_carouselAnimating = false;
_carouselDragStartOffset = _carouselOffset;
_carouselDragStart = e.GetPosition(GameList);
deltaX = 0;
e.Pointer.Capture(GameList);
}
SetCarouselOffset(_carouselDragStartOffset - deltaX);
e.Handled = true;
}
private void OnCarouselPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (!_carouselPointerDown)
{
return;
}
var dragged = _carouselDragging;
EndCarouselDrag(select: true);
if (dragged)
{
e.Pointer.Capture(null);
e.Handled = true;
}
}
private void EndCarouselDrag(bool select)
{
var dragged = _carouselDragging;
_carouselPointerDown = false;
_carouselDragging = false;
if (!dragged || !select || _visibleGames.Count == 0)
{
if (!dragged)
{
AnchorCarouselSelection(animate: true);
}
return;
}
var stride = CarouselItemStride();
var index = stride <= 0 ? 0 : (int)Math.Round(_carouselOffset / stride);
index = Math.Clamp(index, 0, _visibleGames.Count - 1);
if (GameList.SelectedIndex == index)
{
AnchorCarouselSelection(animate: true);
}
else
{
GameList.SelectedIndex = index;
}
}
private void OnCarouselPointerWheel(object? sender, PointerWheelEventArgs e)
{
if (_isLibraryGridLayout || _isGameSettingsOpen || _activePageIndex != 0 || _visibleGames.Count == 0)
{
return;
}
var delta = Math.Abs(e.Delta.X) > Math.Abs(e.Delta.Y) ? -e.Delta.X : e.Delta.Y;
_carouselWheelAccumulator += delta;
while (Math.Abs(_carouselWheelAccumulator) >= 1)
{
var step = _carouselWheelAccumulator > 0 ? -1 : 1;
_carouselWheelAccumulator += step;
MoveSelection(step);
}
e.Handled = true;
}
private void OnCarouselAnimationFrame(TimeSpan frameTime)
{
if (!_carouselAnimating || _carouselDragging)
{
_carouselAnimating = false;
return;
}
var progress = Math.Min(1.0, Stopwatch.GetElapsedTime(_carouselScrollStartedAt).TotalMilliseconds / 260.0);
var eased = 1 - Math.Pow(1 - progress, 3);
SetCarouselOffset(_carouselScrollFrom + (_carouselScrollTo - _carouselScrollFrom) * eased);
if (progress >= 1)
{
_carouselAnimating = false;
return;
}
RequestAnimationFrame(OnCarouselAnimationFrame);
}
private void OnGameListDoubleTapped(object? sender, TappedEventArgs e)

View File

@ -9,21 +9,17 @@ Cover-art library item states and motion.
<Style Selector="ListBox.tileGrid">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="Margin" Value="-32,0,-32,0" />
<Setter Property="Padding" Value="0" />
<Setter Property="Padding" Value="4,0,8,0" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Disabled" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Visible" />
<Setter Property="ItemsPanel">
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
<WrapPanel />
</ItemsPanelTemplate>
</Setter>
</Style>
<Style Selector="ListBox.tileGrid.gridLayout">
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="Margin" Value="0" />
<Setter Property="Padding" Value="4,0,8,0" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Visible" />
@ -34,7 +30,6 @@ Cover-art library item states and motion.
</Setter>
</Style>
<Style Selector="ListBox.tileGrid ListBoxItem">
<Setter Property="FocusAdorner" Value="{x:Null}" />
<Setter Property="Width" Value="{DynamicResource LibraryTileWidth}" />
<Setter Property="Height" Value="{DynamicResource LibraryRailItemHeight}" />
<Setter Property="Padding" Value="4" />

View File

@ -230,9 +230,7 @@ public static partial class ImageRequestBuilders
}
else
{
// A linear color buffer uses the same 256-byte row alignment as a linear texture of the same memory;
// an unaligned pitch made the target and a later texture view of it two different cached images.
pitch = TileGeometry.TexturePitch(transferFormat, width, GuestTileMode.Linear);
pitch = width;
}
var mipSpans = new TileLevelSpan[TiledSurfaceLayout.MaxLevels];

View File

@ -240,21 +240,6 @@ public sealed class ImageRequestBuildersTests : IClassFixture<HeadlessVulkanFixt
Assert.Equal(0u, ImageRequestBuilders.SampleCount(4));
}
[Fact]
public void ColorTarget_LinearTargetPitchMatchesTheLinearTexturePitch()
{
var target = ImageRequestBuilders.ColorTarget(RegisterWords.Color(Base, 816, 604), 0xF, 0, false);
var texture = ImageRequestBuilders.Texture(RegisterWords.Texture(Base, GuestPixelFormat.Bits8_8_8_8UNorm, 816, 604), Sampled2D);
Assert.NotNull(target);
var description = target.Value.Request.Description;
Assert.Equal(new Extent3D(816, 604, 1), description.Extent);
Assert.Equal(832u, description.Pitch);
Assert.Equal(832UL * 604 * 4, description.Data.Size);
Assert.Equal(texture.Request.Description.Pitch, description.Pitch);
Assert.Equal(texture.Request.Description.Data.Size, description.Data.Size);
}
[Fact]
public void Texture_LinearTwoDimensionalFromDescriptor()
{