Compare commits

..

1 Commits

Author SHA1 Message Date
ParantezTech 8962850d2a [CI] Commits are now automatically added to the release notes 2026-07-29 01:42:48 +03:00
46 changed files with 730 additions and 2639 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<SharpEmuVersion>0.0.3-hotfix-2</SharpEmuVersion> <SharpEmuVersion>0.0.3-hotfix-1</SharpEmuVersion>
<Version>$(SharpEmuVersion)</Version> <Version>$(SharpEmuVersion)</Version>
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot> <RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
-202
View File
@@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-6
View File
@@ -19,12 +19,6 @@ precedence = "aggregate"
SPDX-FileCopyrightText = "SharpEmu Emulator Project" SPDX-FileCopyrightText = "SharpEmu Emulator Project"
SPDX-License-Identifier = "GPL-2.0-or-later" SPDX-License-Identifier = "GPL-2.0-or-later"
[[annotations]]
path = "src/SharpEmu.GUI/Assets/Fonts/MaterialSymbolsRounded-*.ttf"
precedence = "aggregate"
SPDX-FileCopyrightText = "2026 Google LLC"
SPDX-License-Identifier = "Apache-2.0"
[[annotations]] [[annotations]]
path = "src/SharpEmu.LibAtrac9/**" path = "src/SharpEmu.LibAtrac9/**"
precedence = "aggregate" precedence = "aggregate"
-16
View File
@@ -1,16 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.GUI;
/// <summary>
/// Stateless trailing action that opens the library folder picker.
/// </summary>
public sealed class AddFolderTile : LibraryTile
{
public static AddFolderTile Instance { get; } = new();
private AddFolderTile()
{
}
}
-3
View File
@@ -15,8 +15,6 @@ cascade order so individual launcher views do not redefine global visuals.
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="avares://SharpEmu.GUI/Themes/Tokens.axaml" /> <ResourceInclude Source="avares://SharpEmu.GUI/Themes/Tokens.axaml" />
<ResourceInclude Source="avares://SharpEmu.GUI/Themes/Templates/SettingRow.axaml" /> <ResourceInclude Source="avares://SharpEmu.GUI/Themes/Templates/SettingRow.axaml" />
<ResourceInclude Source="avares://SharpEmu.GUI/Themes/Templates/OptionsControls.axaml" />
<ResourceInclude Source="avares://SharpEmu.GUI/Themes/Templates/SplitNumericUpDown.axaml" />
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
</ResourceDictionary> </ResourceDictionary>
</Application.Resources> </Application.Resources>
@@ -31,6 +29,5 @@ cascade order so individual launcher views do not redefine global visuals.
<StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Inputs.axaml" /> <StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Inputs.axaml" />
<StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Console.axaml" /> <StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Console.axaml" />
<StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Library.axaml" /> <StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Library.axaml" />
<StyleInclude Source="avares://SharpEmu.GUI/Themes/Styles/Options.axaml" />
</Application.Styles> </Application.Styles>
</Application> </Application>
-1
View File
@@ -18,7 +18,6 @@ public partial class App : Application
{ {
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{ {
desktop.ShutdownMode = Avalonia.Controls.ShutdownMode.OnMainWindowClose;
desktop.MainWindow = new MainWindow(); desktop.MainWindow = new MainWindow();
} }
@@ -1,11 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using Avalonia.Controls;
namespace SharpEmu.GUI.Controls.Settings;
/// <summary>
/// Numeric input whose native spinner is presented as a separate split button
/// </summary>
public sealed class SplitNumericUpDown : NumericUpDown;
+1 -1
View File
@@ -17,7 +17,7 @@ internal enum GameEntryChanges
Background = 4, Background = 4,
} }
public sealed class GameEntry : LibraryTile, INotifyPropertyChanged public sealed class GameEntry : INotifyPropertyChanged
{ {
// Placeholder gradients for games without cover art, picked // Placeholder gradients for games without cover art, picked
// deterministically from the game name so a game keeps its color. // deterministically from the game name so a game keeps its color.
-21
View File
@@ -45,9 +45,6 @@ public sealed class GuiSettings
/// <summary>UI language, matching a file code under Languages/ (e.g. "en", "tr").</summary> /// <summary>UI language, matching a file code under Languages/ (e.g. "en", "tr").</summary>
public string Language { get; set; } = "en"; public string Language { get; set; } = "en";
/// <summary>Default text-entry profile exposed to games.</summary>
public string DefaultProfile { get; set; } = "Sharp";
/// <summary>Publish launcher/game status to Discord Rich Presence.</summary> /// <summary>Publish launcher/game status to Discord Rich Presence.</summary>
public bool DiscordRichPresence { get; set; } = true; public bool DiscordRichPresence { get; set; } = true;
@@ -115,18 +112,6 @@ public sealed class GuiSettings
settings.EnvironmentToggles = FilterNullOrEmpty(settings.EnvironmentToggles); settings.EnvironmentToggles = FilterNullOrEmpty(settings.EnvironmentToggles);
settings.LogLevel ??= "Info"; settings.LogLevel ??= "Info";
settings.Language ??= "en"; settings.Language ??= "en";
var legacyProfile = settings.EnvironmentToggles
.Select(entry => entry.Split('=', 2, StringSplitOptions.TrimEntries))
.FirstOrDefault(parts =>
parts.Length == 2 &&
string.Equals(parts[0], "SHARPEMU_DEFAULT_PROFILE", StringComparison.OrdinalIgnoreCase));
settings.EnvironmentToggles.RemoveAll(entry =>
string.Equals(
entry.Split('=', 2, StringSplitOptions.TrimEntries)[0],
"SHARPEMU_DEFAULT_PROFILE",
StringComparison.OrdinalIgnoreCase));
settings.DefaultProfile = NormalizeDefaultProfile(
legacyProfile is { Length: 2 } ? legacyProfile[1] : settings.DefaultProfile);
settings.DiscordClientId ??= "1525606762248540221"; settings.DiscordClientId ??= "1525606762248540221";
if (settings.RenderResolutionScale <= 0 || settings.RenderResolutionScale > 2.0) if (settings.RenderResolutionScale <= 0 || settings.RenderResolutionScale > 2.0)
{ {
@@ -167,12 +152,6 @@ public sealed class GuiSettings
return $"{width}x{height}"; return $"{width}x{height}";
} }
internal static string NormalizeDefaultProfile(string? value)
{
var trimmed = value?.Trim();
return string.IsNullOrEmpty(trimmed) ? "Sharp" : trimmed;
}
public void Save() public void Save()
{ {
try try
+2 -4
View File
@@ -7,7 +7,7 @@
"Page.GameCount.Other": "{0} لعبة", "Page.GameCount.Other": "{0} لعبة",
"Library.SearchWatermark": "ابحث في المكتبة...", "Library.SearchWatermark": "ابحث في المكتبة...",
"Library.AddFolder": "إضافة مجلد", "Library.AddFolder": " إضافة مجلد",
"Library.OpenFile": "فتح ملف...", "Library.OpenFile": "فتح ملف...",
"Library.Context.Launch": "تشغيل", "Library.Context.Launch": "تشغيل",
@@ -66,8 +66,6 @@
"Options.Language.Label": "لغة المحاكي", "Options.Language.Label": "لغة المحاكي",
"Options.Language.Desc": "اللغة المستخدمة في جميع أنحاء المشغل. تُطبق فوراً.", "Options.Language.Desc": "اللغة المستخدمة في جميع أنحاء المشغل. تُطبق فوراً.",
"Options.DefaultProfile.Label": "اسم الملف الشخصي الافتراضي",
"Options.DefaultProfile.Desc": "الاسم المستخدم عندما تطلب لعبة إدخال نص. القيمة الافتراضية هي Sharp.",
"Common.On": "تشغيل", "Common.On": "تشغيل",
"Common.Off": "إيقاف", "Common.Off": "إيقاف",
@@ -155,7 +153,7 @@
"About.Discord.Label": "دسكورد", "About.Discord.Label": "دسكورد",
"About.Discord.Desc": "انضم إلى المجتمع واحصل على الدعم وتابع التطوير.", "About.Discord.Desc": "انضم إلى المجتمع واحصل على الدعم وتابع التطوير.",
"About.GithubButton": "ساهم على GitHub!", "About.GithubButton": "ساهم على GitHub!",
"About.DiscordComingSoon": "قريبًا", "About.DiscordButton": "انضم إلى دسكوردنا!",
"Updater.Auto.Label": "التحقق من التحديثات عند بدء التشغيل", "Updater.Auto.Label": "التحقق من التحديثات عند بدء التشغيل",
"Updater.Auto.Desc": "يتحقق من GitHub دون تأخير بدء التشغيل.", "Updater.Auto.Desc": "يتحقق من GitHub دون تأخير بدء التشغيل.",
"Updater.Label": "التحديثات", "Updater.Label": "التحديثات",
+2 -4
View File
@@ -7,7 +7,7 @@
"Page.GameCount.Other": "{0} jogos", "Page.GameCount.Other": "{0} jogos",
"Library.SearchWatermark": "Pesquisar na biblioteca…", "Library.SearchWatermark": "Pesquisar na biblioteca…",
"Library.AddFolder": "Adicionar pasta", "Library.AddFolder": " Adicionar pasta",
"Library.OpenFile": "Abrir arquivo…", "Library.OpenFile": "Abrir arquivo…",
"Library.Context.Launch": "Jogar", "Library.Context.Launch": "Jogar",
@@ -76,8 +76,6 @@
"Options.Language.Label": "Idioma do emulador", "Options.Language.Label": "Idioma do emulador",
"Options.Language.Desc": "Idioma usado em toda a interface do emulador. A alteração é aplicada imediatamente.", "Options.Language.Desc": "Idioma usado em toda a interface do emulador. A alteração é aplicada imediatamente.",
"Options.DefaultProfile.Label": "Nome de perfil padrão",
"Options.DefaultProfile.Desc": "Nome usado quando um jogo solicita entrada de texto. O padrão é Sharp.",
"Common.On": "Ativado", "Common.On": "Ativado",
"Common.Off": "Desativado", "Common.Off": "Desativado",
@@ -144,7 +142,7 @@
"About.Discord.Label": "Discord", "About.Discord.Label": "Discord",
"About.Discord.Desc": "Participe da comunidade, obtenha suporte e acompanhe o desenvolvimento.", "About.Discord.Desc": "Participe da comunidade, obtenha suporte e acompanhe o desenvolvimento.",
"About.GithubButton": "Contribua no GitHub!", "About.GithubButton": "Contribua no GitHub!",
"About.DiscordComingSoon": "Em breve", "About.DiscordButton": "Entre no nosso Discord!",
"Library.Context.GameSettings": "Configurações do jogo…", "Library.Context.GameSettings": "Configurações do jogo…",
"Options.Env.WritableApp0.Desc": "Permite que os jogos criem e gravem arquivos dentro da própria pasta de instalação.\nNecessário para dumps não empacotados que gravam seus saves ou configurações em /app0.", "Options.Env.WritableApp0.Desc": "Permite que os jogos criem e gravem arquivos dentro da própria pasta de instalação.\nNecessário para dumps não empacotados que gravam seus saves ou configurações em /app0.",
+2 -4
View File
@@ -7,7 +7,7 @@
"Page.GameCount.Other": "{0} Spiele", "Page.GameCount.Other": "{0} Spiele",
"Library.SearchWatermark": "Bibliothek durchsuchen…", "Library.SearchWatermark": "Bibliothek durchsuchen…",
"Library.AddFolder": "Spielordner hinzufügen", "Library.AddFolder": " Spielordner hinzufügen",
"Library.OpenFile": "Datei öffnen…", "Library.OpenFile": "Datei öffnen…",
"Library.Context.Launch": "Starten", "Library.Context.Launch": "Starten",
@@ -66,8 +66,6 @@
"Options.Language.Label": "Emulator-Sprache", "Options.Language.Label": "Emulator-Sprache",
"Options.Language.Desc": "Sprache der Benutzeroberfläche. Wird sofort angewendet.", "Options.Language.Desc": "Sprache der Benutzeroberfläche. Wird sofort angewendet.",
"Options.DefaultProfile.Label": "Standardprofilname",
"Options.DefaultProfile.Desc": "Name für Texteingaben in Spielen. Der Standardwert ist Sharp.",
"Common.On": "An", "Common.On": "An",
"Common.Off": "Aus", "Common.Off": "Aus",
@@ -155,7 +153,7 @@
"About.Discord.Label": "Discord", "About.Discord.Label": "Discord",
"About.Discord.Desc": "Tritt der Community bei, erhalte Support und verfolge die Entwicklung.", "About.Discord.Desc": "Tritt der Community bei, erhalte Support und verfolge die Entwicklung.",
"About.GithubButton": "Auf GitHub mitwirken!", "About.GithubButton": "Auf GitHub mitwirken!",
"About.DiscordComingSoon": "Demnächst verfügbar", "About.DiscordButton": "Tritt unserem Discord bei!",
"Updater.Auto.Label": "Beim Start nach Updates suchen", "Updater.Auto.Label": "Beim Start nach Updates suchen",
"Updater.Auto.Desc": "Fragt GitHub ab, ohne den Start zu verzögern.", "Updater.Auto.Desc": "Fragt GitHub ab, ohne den Start zu verzögern.",
"Updater.Label": "Updates", "Updater.Label": "Updates",
+2 -4
View File
@@ -7,7 +7,7 @@
"Page.GameCount.Other": "{0} spil", "Page.GameCount.Other": "{0} spil",
"Library.SearchWatermark": "Søg i biblioteket…", "Library.SearchWatermark": "Søg i biblioteket…",
"Library.AddFolder": "Tilføj mappe", "Library.AddFolder": " Tilføj mappe",
"Library.OpenFile": "Åbn fil…", "Library.OpenFile": "Åbn fil…",
"Library.Context.Launch": "Start", "Library.Context.Launch": "Start",
@@ -66,8 +66,6 @@
"Options.Language.Label": "Emulatorsprog", "Options.Language.Label": "Emulatorsprog",
"Options.Language.Desc": "Sprog der bruges i hele launcheren. Anvendes med det samme.", "Options.Language.Desc": "Sprog der bruges i hele launcheren. Anvendes med det samme.",
"Options.DefaultProfile.Label": "Standardprofilnavn",
"Options.DefaultProfile.Desc": "Navn der bruges, når et spil beder om tekstinput. Standardværdien er Sharp.",
"Common.On": "Til", "Common.On": "Til",
"Common.Off": "Fra", "Common.Off": "Fra",
@@ -155,7 +153,7 @@
"About.Discord.Label": "Discord", "About.Discord.Label": "Discord",
"About.Discord.Desc": "Bliv en del af fællesskabet, få hjælp og følg udviklingen.", "About.Discord.Desc": "Bliv en del af fællesskabet, få hjælp og følg udviklingen.",
"About.GithubButton": "Bidrag på GitHub!", "About.GithubButton": "Bidrag på GitHub!",
"About.DiscordComingSoon": "Kommer snart", "About.DiscordButton": "Bliv medlem af vores Discord!",
"Updater.Auto.Label": "Søg efter opdateringer ved start", "Updater.Auto.Label": "Søg efter opdateringer ved start",
"Updater.Auto.Desc": "Tjekker GitHub uden at forsinke opstarten.", "Updater.Auto.Desc": "Tjekker GitHub uden at forsinke opstarten.",
"Updater.Label": "Opdateringer", "Updater.Label": "Opdateringer",
+2 -4
View File
@@ -7,7 +7,7 @@
"Page.GameCount.Other": "{0} games", "Page.GameCount.Other": "{0} games",
"Library.SearchWatermark": "Search library…", "Library.SearchWatermark": "Search library…",
"Library.AddFolder": "Add folder", "Library.AddFolder": " Add folder",
"Library.OpenFile": "Open file…", "Library.OpenFile": "Open file…",
"Library.Context.Launch": "Launch", "Library.Context.Launch": "Launch",
@@ -38,8 +38,6 @@
"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.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.Env.LogNp.Desc": "Log NP (PlayStation Network) library calls to the console.",
"Options.Env.GuestImageCpuSync.Desc": "Re-upload guest surfaces the game's own CPU code rewrites.\nLeave off normally. Turn on for titles whose CPU-drawn surfaces never reach the screen.\nCosts performance and regresses some titles, such as GTA V.", "Options.Env.GuestImageCpuSync.Desc": "Re-upload guest surfaces the game's own CPU code rewrites.\nLeave off normally. Turn on for titles whose CPU-drawn surfaces never reach the screen.\nCosts performance and regresses some titles, such as GTA V.",
"Options.DefaultProfile.Label": "Default profile name",
"Options.DefaultProfile.Desc": "Name used when a game asks for text input. Defaults to Sharp.",
"Options.Section.Emulation": "EMULATION", "Options.Section.Emulation": "EMULATION",
"Options.Section.Logging": "LOGGING", "Options.Section.Logging": "LOGGING",
"Options.Section.Launcher": "LAUNCHER", "Options.Section.Launcher": "LAUNCHER",
@@ -188,7 +186,7 @@
"About.Discord.Label": "Discord", "About.Discord.Label": "Discord",
"About.Discord.Desc": "Join the community, get support and follow development.", "About.Discord.Desc": "Join the community, get support and follow development.",
"About.GithubButton": "Contribute in GitHub!", "About.GithubButton": "Contribute in GitHub!",
"About.DiscordComingSoon": "Coming soon", "About.DiscordButton": "Join our Discord!",
"Updater.Auto.Label": "Check for updates on startup", "Updater.Auto.Label": "Check for updates on startup",
"Updater.Auto.Desc": "Checks GitHub without delaying startup.", "Updater.Auto.Desc": "Checks GitHub without delaying startup.",
+2 -4
View File
@@ -7,7 +7,7 @@
"Page.GameCount.Other": "{0} juegos", "Page.GameCount.Other": "{0} juegos",
"Library.SearchWatermark": "Buscar en la biblioteca…", "Library.SearchWatermark": "Buscar en la biblioteca…",
"Library.AddFolder": "Añadir carpeta", "Library.AddFolder": " Añadir carpeta",
"Library.OpenFile": "Abrir archivo…", "Library.OpenFile": "Abrir archivo…",
"Library.Context.Launch": "Iniciar", "Library.Context.Launch": "Iniciar",
@@ -66,8 +66,6 @@
"Options.Language.Label": "Idioma del emulador", "Options.Language.Label": "Idioma del emulador",
"Options.Language.Desc": "Idioma utilizado en todo el launcher. Se aplica inmediatamente.", "Options.Language.Desc": "Idioma utilizado en todo el launcher. Se aplica inmediatamente.",
"Options.DefaultProfile.Label": "Nombre de perfil predeterminado",
"Options.DefaultProfile.Desc": "Nombre utilizado cuando un juego solicita introducir texto. El valor predeterminado es Sharp.",
"Common.On": "Encendido", "Common.On": "Encendido",
"Common.Off": "Apagado", "Common.Off": "Apagado",
@@ -136,7 +134,7 @@
"About.Github.LatestCommitDescription": "Último commit en la rama main", "About.Github.LatestCommitDescription": "Último commit en la rama main",
"About.Discord.Desc": "Únete a la comunidad, recibe soporte y sigue el desarrollo.", "About.Discord.Desc": "Únete a la comunidad, recibe soporte y sigue el desarrollo.",
"About.GithubButton": "Contribuye en GitHub!", "About.GithubButton": "Contribuye en GitHub!",
"About.DiscordComingSoon": "Próximamente", "About.DiscordButton": "Únete a nuestro Discord!",
"Library.Context.GameSettings": "Ajustes del juego…", "Library.Context.GameSettings": "Ajustes del juego…",
"Options.Env.Tab": "Entorno", "Options.Env.Tab": "Entorno",
+2 -4
View File
@@ -7,7 +7,7 @@
"Page.GameCount.Other": "{0} jeux", "Page.GameCount.Other": "{0} jeux",
"Library.SearchWatermark": "Rechercher dans la bibliothèque…", "Library.SearchWatermark": "Rechercher dans la bibliothèque…",
"Library.AddFolder": "Ajouter un dossier", "Library.AddFolder": " Ajouter un dossier",
"Library.OpenFile": "Ouvrir un fichier…", "Library.OpenFile": "Ouvrir un fichier…",
"Library.Context.Launch": "Lancer", "Library.Context.Launch": "Lancer",
@@ -76,8 +76,6 @@
"Options.Language.Label": "Langue de l’émulateur", "Options.Language.Label": "Langue de l’émulateur",
"Options.Language.Desc": "Langue utilisée dans lensemble du lanceur. Le changement est immédiat.", "Options.Language.Desc": "Langue utilisée dans lensemble du lanceur. Le changement est immédiat.",
"Options.DefaultProfile.Label": "Nom de profil par défaut",
"Options.DefaultProfile.Desc": "Nom utilisé lorsquun jeu demande une saisie de texte. La valeur par défaut est Sharp.",
"Common.On": "Activé", "Common.On": "Activé",
"Common.Off": "Désactivé", "Common.Off": "Désactivé",
@@ -144,7 +142,7 @@
"About.Discord.Label": "Discord", "About.Discord.Label": "Discord",
"About.Discord.Desc": "Rejoignez la communauté, obtenez de laide et suivez le développement.", "About.Discord.Desc": "Rejoignez la communauté, obtenez de laide et suivez le développement.",
"About.GithubButton": "Contribuer sur GitHub !", "About.GithubButton": "Contribuer sur GitHub !",
"About.DiscordComingSoon": "Bientôt disponible", "About.DiscordButton": "Rejoindre notre Discord !",
"Library.Context.GameSettings": "Paramètres du jeu…", "Library.Context.GameSettings": "Paramètres du jeu…",
"Options.Env.WritableApp0.Desc": "Autoriser les jeux à créer et écrire des fichiers dans leur dossier dinstallation.\nNécessaire pour les dumps non empaquetés qui écrivent leurs sauvegardes ou leur configuration sous /app0.", "Options.Env.WritableApp0.Desc": "Autoriser les jeux à créer et écrire des fichiers dans leur dossier dinstallation.\nNécessaire pour les dumps non empaquetés qui écrivent leurs sauvegardes ou leur configuration sous /app0.",
+2 -4
View File
@@ -7,7 +7,7 @@
"Page.GameCount.Other": "{0} játékok", "Page.GameCount.Other": "{0} játékok",
"Library.SearchWatermark": "Keresés a könyvtárban", "Library.SearchWatermark": "Keresés a könyvtárban",
"Library.AddFolder": "Mappa hozzáadása", "Library.AddFolder": " Mappa hozzáadása",
"Library.OpenFile": "Fájl megnyitása…", "Library.OpenFile": "Fájl megnyitása…",
"Library.Context.Launch": "Inditás", "Library.Context.Launch": "Inditás",
@@ -76,8 +76,6 @@
"Options.Language.Label": "Emulátor nyelve", "Options.Language.Label": "Emulátor nyelve",
"Options.Language.Desc": "Az indítóban használt nyelv. Azonnal érvénybe lép.", "Options.Language.Desc": "Az indítóban használt nyelv. Azonnal érvénybe lép.",
"Options.DefaultProfile.Label": "Alapértelmezett profilnév",
"Options.DefaultProfile.Desc": "A játékok szövegbeviteli kéréseinél használt név. Az alapértelmezett érték Sharp.",
"Common.On": "Be", "Common.On": "Be",
"Common.Off": "Ki", "Common.Off": "Ki",
@@ -144,7 +142,7 @@
"About.Discord.Label": "Discord", "About.Discord.Label": "Discord",
"About.Discord.Desc": "Csatlakozz a közösséghe, kérj segítéget és kövesd nyomon a fejlesztést.", "About.Discord.Desc": "Csatlakozz a közösséghe, kérj segítéget és kövesd nyomon a fejlesztést.",
"About.GithubButton": "Járulj hozzá GitHubon!", "About.GithubButton": "Járulj hozzá GitHubon!",
"About.DiscordComingSoon": "Hamarosan", "About.DiscordButton": "Csatlakozz a Discordunhoz!",
"Library.Context.GameSettings": "Játékbeállítások…", "Library.Context.GameSettings": "Játékbeállítások…",
"Options.Env.WritableApp0.Desc": "Engedélyezi, hogy a játékok fájlokat hozzanak létre és írjanak a telepítési mappájukban.\nA kicsomagolt dumpokhoz szükséges, amelyek a mentéseiket vagy beállításaikat az /app0 alá írják.", "Options.Env.WritableApp0.Desc": "Engedélyezi, hogy a játékok fájlokat hozzanak létre és írjanak a telepítési mappájukban.\nA kicsomagolt dumpokhoz szükséges, amelyek a mentéseiket vagy beállításaikat az /app0 alá írják.",
+2 -4
View File
@@ -7,7 +7,7 @@
"Page.GameCount.Other": "{0} giochi", "Page.GameCount.Other": "{0} giochi",
"Library.SearchWatermark": "Cerca nella libreria…", "Library.SearchWatermark": "Cerca nella libreria…",
"Library.AddFolder": "Aggiungi cartella", "Library.AddFolder": " Aggiungi cartella",
"Library.OpenFile": "Apri file…", "Library.OpenFile": "Apri file…",
"Library.Context.Launch": "Avvia", "Library.Context.Launch": "Avvia",
@@ -69,8 +69,6 @@
"Options.Language.Label": "Lingua dell'emulatore", "Options.Language.Label": "Lingua dell'emulatore",
"Options.Language.Desc": "Lingua utilizzata in tutto il launcher. Viene applicata immediatamente.", "Options.Language.Desc": "Lingua utilizzata in tutto il launcher. Viene applicata immediatamente.",
"Options.DefaultProfile.Label": "Nome profilo predefinito",
"Options.DefaultProfile.Desc": "Nome usato quando un gioco richiede l'inserimento di testo. Il valore predefinito è Sharp.",
"Common.On": "On", "Common.On": "On",
"Common.Off": "Off", "Common.Off": "Off",
@@ -160,7 +158,7 @@
"About.Discord.Label": "Discord", "About.Discord.Label": "Discord",
"About.Discord.Desc": "Unisciti alla community, ricevi supporto e segui lo sviluppo.", "About.Discord.Desc": "Unisciti alla community, ricevi supporto e segui lo sviluppo.",
"About.GithubButton": "Contribuisci su GitHub!", "About.GithubButton": "Contribuisci su GitHub!",
"About.DiscordComingSoon": "Prossimamente", "About.DiscordButton": "Unisciti al nostro Discord!",
"Updater.Auto.Label": "Controlla aggiornamenti all'avvio", "Updater.Auto.Label": "Controlla aggiornamenti all'avvio",
"Updater.Auto.Desc": "Interroga GitHub senza rallentare l'avvio.", "Updater.Auto.Desc": "Interroga GitHub senza rallentare l'avvio.",
"Updater.Label": "Aggiornamenti", "Updater.Label": "Aggiornamenti",
+2 -4
View File
@@ -7,7 +7,7 @@
"Page.GameCount.Other": "ゲーム {0}本", "Page.GameCount.Other": "ゲーム {0}本",
"Library.SearchWatermark": "ライブラリを検索…", "Library.SearchWatermark": "ライブラリを検索…",
"Library.AddFolder": "フォルダーを追加", "Library.AddFolder": " フォルダーを追加",
"Library.OpenFile": "ファイルを開く…", "Library.OpenFile": "ファイルを開く…",
"Library.Context.Launch": "起動", "Library.Context.Launch": "起動",
@@ -66,8 +66,6 @@
"Options.Language.Label": "エミュレータの言語", "Options.Language.Label": "エミュレータの言語",
"Options.Language.Desc": "ランチャー全体で使用される言語。変更はすぐに適用されます。", "Options.Language.Desc": "ランチャー全体で使用される言語。変更はすぐに適用されます。",
"Options.DefaultProfile.Label": "既定のプロフィール名",
"Options.DefaultProfile.Desc": "ゲームがテキスト入力を要求したときに使用する名前です。既定値は Sharp です。",
"Common.On": "オン", "Common.On": "オン",
"Common.Off": "オフ", "Common.Off": "オフ",
@@ -155,7 +153,7 @@
"About.Discord.Label": "Discord", "About.Discord.Label": "Discord",
"About.Discord.Desc": "コミュニティに参加して、サポートを受けたり開発を追いかけたりしましょう。", "About.Discord.Desc": "コミュニティに参加して、サポートを受けたり開発を追いかけたりしましょう。",
"About.GithubButton": "GitHubで貢献しよう!", "About.GithubButton": "GitHubで貢献しよう!",
"About.DiscordComingSoon": "近日公開", "About.DiscordButton": "Discordに参加しよう!",
"Updater.Auto.Label": "起動時にアップデートを確認", "Updater.Auto.Label": "起動時にアップデートを確認",
"Updater.Auto.Desc": "起動を遅らせずにGitHubへ確認します。", "Updater.Auto.Desc": "起動を遅らせずにGitHubへ確認します。",
"Updater.Label": "アップデート", "Updater.Label": "アップデート",
+2 -4
View File
@@ -7,7 +7,7 @@
"Page.GameCount.Other": "게임 {0}개", "Page.GameCount.Other": "게임 {0}개",
"Library.SearchWatermark": "라이브러리 검색…", "Library.SearchWatermark": "라이브러리 검색…",
"Library.AddFolder": "폴더 추가", "Library.AddFolder": " 폴더 추가",
"Library.OpenFile": "파일 열기…", "Library.OpenFile": "파일 열기…",
"Library.Context.Launch": "실행", "Library.Context.Launch": "실행",
@@ -66,8 +66,6 @@
"Options.Language.Label": "에뮬레이터 언어", "Options.Language.Label": "에뮬레이터 언어",
"Options.Language.Desc": "런처 전체에 사용되는 언어입니다. 변경 사항은 즉시 적용됩니다.", "Options.Language.Desc": "런처 전체에 사용되는 언어입니다. 변경 사항은 즉시 적용됩니다.",
"Options.DefaultProfile.Label": "기본 프로필 이름",
"Options.DefaultProfile.Desc": "게임에서 텍스트 입력을 요청할 때 사용할 이름입니다. 기본값은 Sharp입니다.",
"Common.On": "켬", "Common.On": "켬",
"Common.Off": "끔", "Common.Off": "끔",
@@ -155,7 +153,7 @@
"About.Discord.Label": "디스코드", "About.Discord.Label": "디스코드",
"About.Discord.Desc": "커뮤니티에 참여해 지원을 받고 개발 소식을 확인하세요.", "About.Discord.Desc": "커뮤니티에 참여해 지원을 받고 개발 소식을 확인하세요.",
"About.GithubButton": "GitHub에서 기여하기!", "About.GithubButton": "GitHub에서 기여하기!",
"About.DiscordComingSoon": "곧 공개", "About.DiscordButton": "디스코드 참여하기!",
"Updater.Auto.Label": "시작 시 업데이트 확인", "Updater.Auto.Label": "시작 시 업데이트 확인",
"Updater.Auto.Desc": "시작을 지연시키지 않고 GitHub를 확인합니다.", "Updater.Auto.Desc": "시작을 지연시키지 않고 GitHub를 확인합니다.",
"Updater.Label": "업데이트", "Updater.Label": "업데이트",
+2 -4
View File
@@ -7,7 +7,7 @@
"Page.GameCount.Other": "{0} games", "Page.GameCount.Other": "{0} games",
"Library.SearchWatermark": "Zoeken in bibliotheek…", "Library.SearchWatermark": "Zoeken in bibliotheek…",
"Library.AddFolder": "Map toevoegen", "Library.AddFolder": " Map toevoegen",
"Library.OpenFile": "Bestand openen…", "Library.OpenFile": "Bestand openen…",
"Library.Context.Launch": "Starten", "Library.Context.Launch": "Starten",
@@ -66,8 +66,6 @@
"Options.Language.Label": "Taal van de emulator", "Options.Language.Label": "Taal van de emulator",
"Options.Language.Desc": "Taal die in de hele launcher wordt gebruikt. Wordt direct toegepast.", "Options.Language.Desc": "Taal die in de hele launcher wordt gebruikt. Wordt direct toegepast.",
"Options.DefaultProfile.Label": "Standaardprofielnaam",
"Options.DefaultProfile.Desc": "Naam die wordt gebruikt wanneer een game om tekstinvoer vraagt. De standaardwaarde is Sharp.",
"Common.On": "Aan", "Common.On": "Aan",
"Common.Off": "Uit", "Common.Off": "Uit",
@@ -155,7 +153,7 @@
"About.Discord.Label": "Discord", "About.Discord.Label": "Discord",
"About.Discord.Desc": "Word lid van de community, krijg ondersteuning en volg de ontwikkeling.", "About.Discord.Desc": "Word lid van de community, krijg ondersteuning en volg de ontwikkeling.",
"About.GithubButton": "Draag bij op GitHub!", "About.GithubButton": "Draag bij op GitHub!",
"About.DiscordComingSoon": "Binnenkort", "About.DiscordButton": "Word lid van onze Discord!",
"Updater.Auto.Label": "Bij het opstarten controleren op updates", "Updater.Auto.Label": "Bij het opstarten controleren op updates",
"Updater.Auto.Desc": "Controleert GitHub zonder het opstarten te vertragen.", "Updater.Auto.Desc": "Controleert GitHub zonder het opstarten te vertragen.",
"Updater.Label": "Updates", "Updater.Label": "Updates",
+2 -4
View File
@@ -7,7 +7,7 @@
"Page.GameCount.Other": "{0} jogos", "Page.GameCount.Other": "{0} jogos",
"Library.SearchWatermark": "Pesquisar biblioteca…", "Library.SearchWatermark": "Pesquisar biblioteca…",
"Library.AddFolder": "Adicionar pasta", "Library.AddFolder": " Adicionar pasta",
"Library.OpenFile": "Abrir ficheiro…", "Library.OpenFile": "Abrir ficheiro…",
"Library.Context.Launch": "Iniciar", "Library.Context.Launch": "Iniciar",
@@ -76,8 +76,6 @@
"Options.Language.Label": "Idioma do emulador", "Options.Language.Label": "Idioma do emulador",
"Options.Language.Desc": "Idioma utilizado em todo o lançador. Aplica-se de imediato.", "Options.Language.Desc": "Idioma utilizado em todo o lançador. Aplica-se de imediato.",
"Options.DefaultProfile.Label": "Nome de perfil predefinido",
"Options.DefaultProfile.Desc": "Nome utilizado quando um jogo solicita a introdução de texto. O valor predefinido é Sharp.",
"Common.On": "Ativado", "Common.On": "Ativado",
"Common.Off": "Desativado", "Common.Off": "Desativado",
@@ -144,7 +142,7 @@
"About.Discord.Label": "Discord", "About.Discord.Label": "Discord",
"About.Discord.Desc": "Junte-se à comunidade, obtenha suporte e acompanhe o desenvolvimento.", "About.Discord.Desc": "Junte-se à comunidade, obtenha suporte e acompanhe o desenvolvimento.",
"About.GithubButton": "Contribua no GitHub!", "About.GithubButton": "Contribua no GitHub!",
"About.DiscordComingSoon": "Em breve", "About.DiscordButton": "Junte-se ao nosso Discord!",
"Library.Context.GameSettings": "Definições do jogo…", "Library.Context.GameSettings": "Definições do jogo…",
"Options.Env.WritableApp0.Desc": "Permitir que os jogos criem e escrevam ficheiros dentro da sua pasta de instalação.\nNecessário para dumps não empacotados que escrevem os seus dados guardados ou configurações em /app0.", "Options.Env.WritableApp0.Desc": "Permitir que os jogos criem e escrevam ficheiros dentro da sua pasta de instalação.\nNecessário para dumps não empacotados que escrevem os seus dados guardados ou configurações em /app0.",
+2 -4
View File
@@ -7,7 +7,7 @@
"Page.GameCount.Other": "Игр: {0}", "Page.GameCount.Other": "Игр: {0}",
"Library.SearchWatermark": "Поиск…", "Library.SearchWatermark": "Поиск…",
"Library.AddFolder": "Добавить папку", "Library.AddFolder": " Добавить папку",
"Library.OpenFile": "Открыть файл…", "Library.OpenFile": "Открыть файл…",
"Library.Context.Launch": "Запустить", "Library.Context.Launch": "Запустить",
@@ -109,8 +109,6 @@
"Options.Language.Label": "Язык эмулятора", "Options.Language.Label": "Язык эмулятора",
"Options.Language.Desc": "Язык интерфейса лаунчера. Изменение применяется сразу.", "Options.Language.Desc": "Язык интерфейса лаунчера. Изменение применяется сразу.",
"Options.DefaultProfile.Label": "Имя профиля по умолчанию",
"Options.DefaultProfile.Desc": "Имя, используемое, когда игра запрашивает ввод текста. Значение по умолчанию — Sharp.",
"Common.On": "Включено", "Common.On": "Включено",
"Common.Off": "Выключено", "Common.Off": "Выключено",
@@ -188,7 +186,7 @@
"About.Discord.Label": "Discord", "About.Discord.Label": "Discord",
"About.Discord.Desc": "Присоединяйтесь к сообществу, получайте поддержку и следите за разработкой.", "About.Discord.Desc": "Присоединяйтесь к сообществу, получайте поддержку и следите за разработкой.",
"About.GithubButton": "Участвовать в разработке на GitHub!", "About.GithubButton": "Участвовать в разработке на GitHub!",
"About.DiscordComingSoon": "Скоро", "About.DiscordButton": "Присоединиться к нашему Discord!",
"Updater.Auto.Label": "Проверять обновления при запуске", "Updater.Auto.Label": "Проверять обновления при запуске",
"Updater.Auto.Desc": "Проверяет GitHub без задержки запуска.", "Updater.Auto.Desc": "Проверяет GitHub без задержки запуска.",
+2 -4
View File
@@ -7,7 +7,7 @@
"Page.GameCount.Other": "{0} oyun", "Page.GameCount.Other": "{0} oyun",
"Library.SearchWatermark": "Kütüphanede ara…", "Library.SearchWatermark": "Kütüphanede ara…",
"Library.AddFolder": "Klasör ekle", "Library.AddFolder": " Klasör ekle",
"Library.OpenFile": "Dosya aç…", "Library.OpenFile": "Dosya aç…",
"Library.Context.Launch": "Başlat", "Library.Context.Launch": "Başlat",
@@ -173,8 +173,6 @@
"Options.Env.LogIo.Desc": "Dosya açma, okuma ve yol çözümleme etkinliğini konsola günlükle.\nBir oyun açılışta veri dosyalarını bulamıyorsa kullanın.", "Options.Env.LogIo.Desc": "Dosya açma, okuma ve yol çözümleme etkinliğini konsola günlükle.\nBir oyun açılışta veri dosyalarını bulamıyorsa kullanın.",
"Options.Env.LogNp.Desc": "NP (PlayStation Network) kütüphane çağrılarını konsola günlükle.", "Options.Env.LogNp.Desc": "NP (PlayStation Network) kütüphane çağrılarını konsola günlükle.",
"Options.Env.GuestImageCpuSync.Desc": "Oyunun kendi CPU kodunun yeniden yazdığı misafir yüzeyleri tekrar yükler.\nNormalde kapalı bırakın. CPU ile çizilen yüzeyleri ekrana ulaşmayan oyunlarda açın.\nPerformansa mal olur ve GTA V gibi bazı oyunlarda soruna yol açar.", "Options.Env.GuestImageCpuSync.Desc": "Oyunun kendi CPU kodunun yeniden yazdığı misafir yüzeyleri tekrar yükler.\nNormalde kapalı bırakın. CPU ile çizilen yüzeyleri ekrana ulaşmayan oyunlarda açın.\nPerformansa mal olur ve GTA V gibi bazı oyunlarda soruna yol açar.",
"Options.DefaultProfile.Label": "Varsayilan profil adi",
"Options.DefaultProfile.Desc": "Oyun metin girisi istediginde kullanilacak ad. Varsayilan deger Sharp'tir.",
"Common.Save": "Kaydet", "Common.Save": "Kaydet",
"Common.Cancel": "İptal", "Common.Cancel": "İptal",
"PerGame.Title": "Oyuna özel ayarlar — {0} ({1})", "PerGame.Title": "Oyuna özel ayarlar — {0} ({1})",
@@ -191,7 +189,7 @@
"About.Discord.Label": "Discord", "About.Discord.Label": "Discord",
"About.Discord.Desc": "Topluluğa katılın, destek alın ve geliştirmeyi takip edin.", "About.Discord.Desc": "Topluluğa katılın, destek alın ve geliştirmeyi takip edin.",
"About.GithubButton": "GitHub'da katkıda bulun!", "About.GithubButton": "GitHub'da katkıda bulun!",
"About.DiscordComingSoon": "Yakında", "About.DiscordButton": "Discord'umuza katıl!",
"Options.Section.Rendering": "GÖRÜNTÜ İŞLEME", "Options.Section.Rendering": "GÖRÜNTÜ İŞLEME",
"Options.RenderResolution.Label": "Dahili çözünürlük", "Options.RenderResolution.Label": "Dahili çözünürlük",
"Options.RenderResolution.Desc": "Ekran dışı hedefleri doğal çözünürlüğün altında işle ve sunum sırasında ölçeklendir. Daha düşük değerler GPU payı karşılığında görüntü kalitesini azaltır; bir sonraki başlatmada etkili olur.", "Options.RenderResolution.Desc": "Ekran dışı hedefleri doğal çözünürlüğün altında işle ve sunum sırasında ölçeklendir. Daha düşük değerler GPU payı karşılığında görüntü kalitesini azaltır; bir sonraki başlatmada etkili olur.",
-9
View File
@@ -1,9 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.GUI;
/// <summary>
/// Base type for game cards and actions shown in the library rail.
/// </summary>
public abstract class LibraryTile;
-103
View File
@@ -1,103 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
namespace SharpEmu.GUI;
/// <summary>
/// Read-only view of the visible games with one persistent action tile at
/// the end. Game collection changes keep their original indices, so Avalonia
/// can update and virtualize the rail without rebuilding every item.
/// </summary>
public sealed class LibraryTileCollection : IReadOnlyList<LibraryTile>, IList, INotifyCollectionChanged
{
private readonly ObservableCollection<GameEntry> _games;
public LibraryTileCollection(ObservableCollection<GameEntry> games)
{
_games = games;
_games.CollectionChanged += OnGamesCollectionChanged;
}
public event NotifyCollectionChangedEventHandler? CollectionChanged;
public int Count => _games.Count + 1;
bool IList.IsFixedSize => false;
bool IList.IsReadOnly => true;
bool ICollection.IsSynchronized => false;
object ICollection.SyncRoot => this;
public LibraryTile this[int index]
{
get
{
if ((uint)index < (uint)_games.Count)
{
return _games[index];
}
if (index == _games.Count)
{
return AddFolderTile.Instance;
}
throw new ArgumentOutOfRangeException(nameof(index));
}
}
object? IList.this[int index]
{
get => this[index];
set => throw new NotSupportedException();
}
public IEnumerator<LibraryTile> GetEnumerator()
{
foreach (var game in _games)
{
yield return game;
}
yield return AddFolderTile.Instance;
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
int IList.Add(object? value) => throw new NotSupportedException();
void IList.Clear() => throw new NotSupportedException();
bool IList.Contains(object? value) => ((IList)this).IndexOf(value) >= 0;
int IList.IndexOf(object? value) => value switch
{
GameEntry game => _games.IndexOf(game),
AddFolderTile => _games.Count,
_ => -1,
};
void IList.Insert(int index, object? value) => throw new NotSupportedException();
void IList.Remove(object? value) => throw new NotSupportedException();
void IList.RemoveAt(int index) => throw new NotSupportedException();
void ICollection.CopyTo(Array array, int index)
{
ArgumentNullException.ThrowIfNull(array);
foreach (var tile in this)
{
array.SetValue(tile, index++);
}
}
private void OnGamesCollectionChanged(object? sender, NotifyCollectionChangedEventArgs args) =>
CollectionChanged?.Invoke(this, args);
}
File diff suppressed because it is too large Load Diff
+277 -309
View File
@@ -33,6 +33,8 @@ public partial class MainWindow : Window
{ {
private const int MaxConsoleLines = 4000; private const int MaxConsoleLines = 4000;
private const int MaxConsoleLinesPerFlush = 500; private const int MaxConsoleLinesPerFlush = 500;
private const double LaunchBlurRadius = 12;
private const double BlurTransitionSeconds = 0.24;
private static readonly IBrush DefaultLineBrush = new SolidColorBrush(Color.Parse("#C7CFDE")); private static readonly IBrush DefaultLineBrush = new SolidColorBrush(Color.Parse("#C7CFDE"));
private static readonly IBrush DimLineBrush = new SolidColorBrush(Color.Parse("#6B7488")); private static readonly IBrush DimLineBrush = new SolidColorBrush(Color.Parse("#6B7488"));
@@ -81,12 +83,17 @@ public partial class MainWindow : Window
]; ];
private readonly List<GameEntry> _allGames = new(); private readonly List<GameEntry> _allGames = new();
private readonly ObservableCollection<GameEntry> _visibleGames = new(); private readonly ObservableCollection<GameEntry> _visibleGames = new();
private readonly LibraryTileCollection _libraryTiles;
private readonly GameLibraryWatcher _libraryWatcher = new(); private readonly GameLibraryWatcher _libraryWatcher = new();
private readonly AvaloniaList<LogLine> _consoleLines = new(); private readonly AvaloniaList<LogLine> _consoleLines = new();
private readonly List<LogLine> _allConsoleLines = new(); private readonly List<LogLine> _allConsoleLines = new();
private readonly ConcurrentQueue<(string Line, bool IsError)> _pendingLines = new(); private readonly ConcurrentQueue<(string Line, bool IsError)> _pendingLines = new();
private readonly DispatcherTimer _consoleFlushTimer; private readonly DispatcherTimer _consoleFlushTimer;
private readonly DispatcherTimer _libraryBlurTimer;
private BlurEffect? _libraryBlur;
private double _libraryBlurStartRadius;
private double _libraryBlurTargetRadius;
private long _libraryBlurStartedAt;
private bool _clearLibraryBlurWhenComplete;
private GuiSettings _settings = new(); private GuiSettings _settings = new();
private IReadOnlyList<HostDisplayOption> _hostDisplays = []; private IReadOnlyList<HostDisplayOption> _hostDisplays = [];
@@ -102,7 +109,6 @@ public partial class MainWindow : Window
private bool _isStopping; private bool _isStopping;
private int _autoScrollTicks; private int _autoScrollTicks;
private int _activePageIndex; private int _activePageIndex;
private int _optionsSectionIndex;
private Updater.UpdateInfo? _availableUpdate; private Updater.UpdateInfo? _availableUpdate;
private string _updateStatusKey = "Updater.Status.Ready"; private string _updateStatusKey = "Updater.Status.Ready";
private object?[] _updateStatusArgs = [BuildInfo.CommitSha ?? "dev"]; private object?[] _updateStatusArgs = [BuildInfo.CommitSha ?? "dev"];
@@ -117,19 +123,23 @@ public partial class MainWindow : Window
private int _detailLoadGeneration; private int _detailLoadGeneration;
private int _backdropGeneration; private int _backdropGeneration;
private bool _isClosing; private bool _isClosing;
private bool _restoringGameSelection;
private bool _addFolderInProgress;
private GameEntry? _lastSelectedGame;
// Bundled key art shown whenever no game-specific backdrop applies; the // Bundled key art shown whenever no game-specific backdrop applies; the
// plain window color remains the fallback when the asset fails to load. // plain window color remains the fallback when the asset fails to load.
private Bitmap? _defaultBackdrop; private Bitmap? _defaultBackdrop;
// Whether the native loading/closing popup should be showing; it is a
// desktop-topmost popup, so it closes while the launcher is in the
// background or minimized and reopens from this flag on activation.
private bool _sessionLoadingActive;
// Controller navigation state. // Controller navigation state.
private readonly DispatcherTimer _gamepadTimer; private readonly DispatcherTimer _gamepadTimer;
private HostGamepadButtons _previousPadButtons; private HostGamepadButtons _previousPadButtons;
private long _navLeftNextAt; private long _navLeftNextAt;
private long _navRightNextAt; private long _navRightNextAt;
private long _navUpNextAt;
private long _navDownNextAt;
//Github http client for latest commit //Github http client for latest commit
private static readonly HttpClient GithubHttpClient = CreateGithubHttpClient(); private static readonly HttpClient GithubHttpClient = CreateGithubHttpClient();
@@ -146,7 +156,6 @@ public partial class MainWindow : Window
{ {
InitializeComponent(); InitializeComponent();
InitializeLocalizedChoiceBoxes(); InitializeLocalizedChoiceBoxes();
_libraryTiles = new LibraryTileCollection(_visibleGames);
try try
{ {
@@ -160,11 +169,13 @@ public partial class MainWindow : Window
_defaultBackdrop = null; // color background remains the fallback _defaultBackdrop = null; // color background remains the fallback
} }
GameList.ItemsSource = _libraryTiles; GameList.ItemsSource = _visibleGames;
_libraryWatcher.RefreshRequested += OnLibraryRefreshRequested; _libraryWatcher.RefreshRequested += OnLibraryRefreshRequested;
ConsoleList.ItemsSource = _consoleLines; ConsoleList.ItemsSource = _consoleLines;
_consoleMirror = GuiConsoleMirror.Install((line, isError) => _consoleMirror = GuiConsoleMirror.Install((line, isError) =>
_pendingLines.Enqueue((line, isError))); _pendingLines.Enqueue((line, isError)));
Closed += (_, _) => _emulator?.Stop();
_consoleFlushTimer = new DispatcherTimer _consoleFlushTimer = new DispatcherTimer
{ {
Interval = TimeSpan.FromMilliseconds(80), Interval = TimeSpan.FromMilliseconds(80),
@@ -176,6 +187,23 @@ public partial class MainWindow : Window
}; };
_consoleFlushTimer.Start(); _consoleFlushTimer.Start();
_libraryBlurTimer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(16),
};
_libraryBlurTimer.Tick += (_, _) => AdvanceLibraryBlur();
// Native popups float above every window on the desktop; they must
// follow the launcher into the background or a minimized state.
Activated += (_, _) =>
{
SessionLoadingPopup.IsOpen = _sessionLoadingActive;
};
Deactivated += (_, _) =>
{
SessionLoadingPopup.IsOpen = false;
};
TitleBar.PointerPressed += OnTitleBarPointerPressed; TitleBar.PointerPressed += OnTitleBarPointerPressed;
TitleBar.DoubleTapped += OnTitleBarDoubleTapped; TitleBar.DoubleTapped += OnTitleBarDoubleTapped;
MinimizeButton.Click += (_, _) => WindowState = WindowState.Minimized; MinimizeButton.Click += (_, _) => WindowState = WindowState.Minimized;
@@ -193,30 +221,21 @@ public partial class MainWindow : Window
UpdateWindowChromeState(); UpdateWindowChromeState();
}; };
UpdateWindowChromeState(); UpdateWindowChromeState();
GameList.SelectionChanged += (_, _) => OnGameListSelectionChanged(); GameList.SelectionChanged += (_, _) => UpdateSelectedGame();
GameList.DoubleTapped += OnGameListDoubleTapped; GameList.DoubleTapped += (_, _) => LaunchSelected();
SearchBox.TextChanged += (_, _) => RefreshVisibleGames(); SearchBox.TextChanged += (_, _) => RefreshVisibleGames();
ConsoleSearchBox.TextChanged += (_, _) => RefreshVisibleConsoleLines(); ConsoleSearchBox.TextChanged += (_, _) => RefreshVisibleConsoleLines();
AddFolderButton.Click += async (_, _) => await AddFolderAsync();
EmptyAddFolderButton.Click += async (_, _) => await AddFolderAsync();
OpenFileButton.Click += async (_, _) => await OpenFileAsync(); OpenFileButton.Click += async (_, _) => await OpenFileAsync();
LaunchButton.Click += (_, _) => LaunchButton.Click += (_, _) => LaunchSelected();
{
if (_isRunning)
{
StopEmulator();
}
else
{
LaunchSelected();
}
};
ClearLogButton.Click += (_, _) => { _consoleLines.Clear(); _allConsoleLines.Clear(); }; ClearLogButton.Click += (_, _) => { _consoleLines.Clear(); _allConsoleLines.Clear(); };
StopButton.Click += (_, _) => StopEmulator();
CopyLogButton.Click += async (_, _) => await CopyConsoleAsync(); CopyLogButton.Click += async (_, _) => await CopyConsoleAsync();
DetachConsoleButton.Click += (_, _) => ShowConsoleWindow(); DetachConsoleButton.Click += (_, _) => ShowConsoleWindow();
CloseConsoleButton.Click += (_, _) => ConsoleToggle.IsChecked = false;
LibraryTabButton.Click += (_, _) => SetActivePage(0); LibraryTabButton.Click += (_, _) => SetActivePage(0);
OptionsTabButton.Click += (_, _) => SetActivePage(1); OptionsTabButton.Click += (_, _) => SetActivePage(1);
ConsoleToggle.IsCheckedChanged += (_, _) => ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true && _consoleWindow is null; ConsoleToggle.IsCheckedChanged += (_, _) => ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true && _consoleWindow is null;
WireOptionsNavigation();
// The settings page edits _settings live, so a launch started while // The settings page edits _settings live, so a launch started while
// it is open already uses the new values. // it is open already uses the new values.
@@ -279,8 +298,6 @@ public partial class MainWindow : Window
SetEnvironmentToggle( SetEnvironmentToggle(
"SHARPEMU_GUEST_IMAGE_CPU_SYNC", "SHARPEMU_GUEST_IMAGE_CPU_SYNC",
EnvGuestImageCpuSyncToggle.IsChecked == true); EnvGuestImageCpuSyncToggle.IsChecked == true);
DefaultProfileBox.TextChanged += (_, _) =>
_settings.DefaultProfile = GuiSettings.NormalizeDefaultProfile(DefaultProfileBox.Text);
LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged(); LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel); GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel);
@@ -295,8 +312,7 @@ public partial class MainWindow : Window
CtxRemove.Click += (_, _) => RemoveSelectedFromLibrary(); CtxRemove.Click += (_, _) => RemoveSelectedFromLibrary();
Opened += async (_, _) => await OnOpenedAsync(); Opened += async (_, _) => await OnOpenedAsync();
Closing += (_, _) => BeginWindowClosing(); Closing += (_, _) => OnWindowClosing();
Closed += (_, _) => CompleteWindowClosing();
SdlLauncherGamepad.EnsureStarted(); SdlLauncherGamepad.EnsureStarted();
_gamepadTimer = new DispatcherTimer _gamepadTimer = new DispatcherTimer
@@ -316,6 +332,15 @@ public partial class MainWindow : Window
}); });
}; };
DiscordButton.Click += (_, _) =>
{
Process.Start(new ProcessStartInfo
{
FileName = "https://discord.com/invite/6GejPEDqpc",
UseShellExecute = true
});
};
LatestCommitHashText.Click += (_, _) => LatestCommitHashText.Click += (_, _) =>
{ {
if (string.IsNullOrWhiteSpace(_latestCommitSha)) if (string.IsNullOrWhiteSpace(_latestCommitSha))
@@ -371,94 +396,6 @@ public partial class MainWindow : Window
} }
} }
private void WireOptionsNavigation()
{
var buttons = OptionsNavigationButtons();
for (var index = 0; index < buttons.Length; index++)
{
var section = index;
buttons[index].Click += (_, _) => SetOptionsSection(section);
buttons[index].PointerEntered += (_, _) => SetOptionsNavigationIndicator(section);
buttons[index].GotFocus += (_, _) => SetOptionsNavigationIndicator(section);
}
OptionsNavHost.PointerExited += (_, _) =>
SetOptionsNavigationIndicator(_optionsSectionIndex);
SetOptionsSection(0);
}
private Button[] OptionsNavigationButtons() =>
[
OptionsGeneralNav,
OptionsLoggingNav,
OptionsLauncherNav,
OptionsRenderingNav,
OptionsEnvironmentNav,
OptionsAboutNav,
];
private Control[] OptionsSectionPanels() =>
[
OptionsGeneralPanel,
OptionsLoggingPanel,
OptionsLauncherPanel,
OptionsRenderingPanel,
OptionsEnvironmentPanel,
OptionsAboutPanel,
];
private void SetOptionsSection(int section)
{
var buttons = OptionsNavigationButtons();
var panels = OptionsSectionPanels();
section = Math.Clamp(section, 0, buttons.Length - 1);
_optionsSectionIndex = section;
SetOptionsNavigationIndicator(section);
for (var index = 0; index < buttons.Length; index++)
{
var active = index == section;
SetActiveClass(buttons[index], active);
SetOptionsPanelInteraction(panels[index], active);
}
}
private static void SetOptionsPanelInteraction(Control panel, bool active)
{
if (active)
{
if (!panel.Classes.Contains("active"))
{
panel.Classes.Add("active");
}
}
else
{
panel.Classes.Remove("active");
}
panel.IsHitTestVisible = active;
AutomationProperties.SetAccessibilityView(
panel,
active ? AccessibilityView.Content : AccessibilityView.Raw);
KeyboardNavigation.SetTabNavigation(
panel,
active ? KeyboardNavigationMode.Continue : KeyboardNavigationMode.None);
}
private void SetOptionsNavigationIndicator(int section)
{
if (OptionsNavIndicator.RenderTransform is not TranslateTransform transform)
{
return;
}
var buttons = OptionsNavigationButtons();
var button = buttons[Math.Clamp(section, 0, buttons.Length - 1)];
transform.Y = button.TranslatePoint(default, OptionsNavHost)?.Y
?? section * button.Bounds.Height;
}
// ---- Github http client config ---- // ---- Github http client config ----
// This is for getting lash commit id // This is for getting lash commit id
private static HttpClient CreateGithubHttpClient() private static HttpClient CreateGithubHttpClient()
@@ -590,6 +527,8 @@ public partial class MainWindow : Window
var now = Environment.TickCount64; var now = Environment.TickCount64;
var left = (pad.Buttons & HostGamepadButtons.Left) != 0 || pad.LeftX < 64; var left = (pad.Buttons & HostGamepadButtons.Left) != 0 || pad.LeftX < 64;
var right = (pad.Buttons & HostGamepadButtons.Right) != 0 || pad.LeftX > 192; var right = (pad.Buttons & HostGamepadButtons.Right) != 0 || pad.LeftX > 192;
var up = (pad.Buttons & HostGamepadButtons.Up) != 0 || pad.LeftY < 64;
var down = (pad.Buttons & HostGamepadButtons.Down) != 0 || pad.LeftY > 192;
if (ShouldNavigate(left, ref _navLeftNextAt, now)) if (ShouldNavigate(left, ref _navLeftNextAt, now))
{ {
@@ -601,6 +540,16 @@ public partial class MainWindow : Window
MoveSelection(1); MoveSelection(1);
} }
if (ShouldNavigate(up, ref _navUpNextAt, now))
{
MoveSelection(-TilesPerRow());
}
if (ShouldNavigate(down, ref _navDownNextAt, now))
{
MoveSelection(TilesPerRow());
}
var pressed = pad.Buttons & ~_previousPadButtons; var pressed = pad.Buttons & ~_previousPadButtons;
if ((pressed & HostGamepadButtons.Cross) != 0) if ((pressed & HostGamepadButtons.Cross) != 0)
{ {
@@ -639,13 +588,26 @@ public partial class MainWindow : Window
private void MoveSelection(int delta) private void MoveSelection(int delta)
{ {
if (_visibleGames.Count == 0)
{
return;
}
var index = GameList.SelectedIndex < 0 var index = GameList.SelectedIndex < 0
? 0 ? 0
: Math.Clamp(GameList.SelectedIndex + delta, 0, _libraryTiles.Count - 1); : Math.Clamp(GameList.SelectedIndex + delta, 0, _visibleGames.Count - 1);
GameList.SelectedIndex = index; GameList.SelectedIndex = index;
GameList.ScrollIntoView(index); GameList.ScrollIntoView(index);
} }
private int TilesPerRow()
{
// Tile footprint: 128 content + 20 item padding + 10 item margin.
const double TileOuterWidth = 158;
var width = GameList.Bounds.Width;
return width > TileOuterWidth ? (int)(width / TileOuterWidth) : 1;
}
private async Task OnOpenedAsync() private async Task OnOpenedAsync()
{ {
var version = Assembly.GetExecutingAssembly().GetName().Version; var version = Assembly.GetExecutingAssembly().GetName().Version;
@@ -702,60 +664,12 @@ public partial class MainWindow : Window
/// </summary> /// </summary>
private void ApplyLocalization() private void ApplyLocalization()
{ {
RefreshOptionsNavigationLabels();
RefreshLocalizedChoices(); RefreshLocalizedChoices();
UpdateLogFilePathText(); UpdateLogFilePathText();
RefreshHostRefreshRates(_settings.RefreshRate); RefreshHostRefreshRates(_settings.RefreshRate);
RefreshUpdateText(); RefreshUpdateText();
UpdateEmptyStateTexts(); UpdateEmptyStateTexts();
UpdateRunButtons(); UpdateSelectedGameTexts();
}
private void RefreshOptionsNavigationLabels()
{
var localization = Localization.Instance;
SetOptionsNavigationLabel(
OptionsGeneralNav,
OptionsGeneralNavLabel,
localization.Get("Options.General"));
SetOptionsNavigationLabel(
OptionsLoggingNav,
OptionsLoggingNavLabel,
localization.Get("Options.Section.Logging"));
SetOptionsNavigationLabel(
OptionsLauncherNav,
OptionsLauncherNavLabel,
localization.Get("Options.Section.Launcher"));
SetOptionsNavigationLabel(
OptionsRenderingNav,
OptionsRenderingNavLabel,
localization.Get("Options.Section.Rendering"));
SetOptionsNavigationLabel(
OptionsEnvironmentNav,
OptionsEnvironmentNavLabel,
localization.Get("Options.Env.Tab"));
SetOptionsNavigationLabel(
OptionsAboutNav,
OptionsAboutNavLabel,
localization.Get("Options.About"));
}
private static void SetOptionsNavigationLabel(Button button, TextBlock label, string value)
{
var navigationLabel = SectionNavigationLabel(value);
label.Text = navigationLabel;
AutomationProperties.SetName(button, navigationLabel);
}
private static string SectionNavigationLabel(string value)
{
if (string.IsNullOrEmpty(value) || value.Any(char.IsLower))
{
return value;
}
var lower = value.ToLowerInvariant();
return char.ToUpperInvariant(lower[0]) + lower[1..];
} }
// ---- Discord Rich Presence ---- // ---- Discord Rich Presence ----
@@ -815,44 +729,23 @@ public partial class MainWindow : Window
// still needs a preview hook for its own shortcuts. // still needs a preview hook for its own shortcuts.
} }
private void BeginWindowClosing() private void OnWindowClosing()
{ {
if (_isClosing)
{
return;
}
_isClosing = true; _isClosing = true;
Interlocked.Increment(ref _libraryScanGeneration); Interlocked.Increment(ref _libraryScanGeneration);
Interlocked.Increment(ref _detailLoadGeneration); Interlocked.Increment(ref _detailLoadGeneration);
_libraryWatcher.Dispose();
_settings.Save();
_consoleFlushTimer.Stop(); _consoleFlushTimer.Stop();
_libraryBlurTimer.Stop();
_gamepadTimer.Stop(); _gamepadTimer.Stop();
} SdlLauncherGamepad.Shutdown();
_sndPreview.Stop();
private void CompleteWindowClosing() _discord?.Dispose();
{ _consoleWindow?.Close();
RunShutdownStep("library watcher", _libraryWatcher.Dispose); _emulator?.Dispose();
RunShutdownStep("settings", _settings.Save); _consoleMirror?.Dispose();
RunShutdownStep("SDL gamepad", SdlLauncherGamepad.Shutdown); DropFileLog();
RunShutdownStep("title music", _sndPreview.Stop);
RunShutdownStep("Discord Rich Presence", () => _discord?.Dispose());
RunShutdownStep("console window", () => _consoleWindow?.Close());
RunShutdownStep("emulator process", () => _emulator?.Dispose());
RunShutdownStep("console mirror", () => _consoleMirror?.Dispose());
RunShutdownStep("file log", DropFileLog);
}
private static void RunShutdownStep(string component, Action action)
{
try
{
action();
}
catch (Exception exception)
{
Console.Error.WriteLine(
$"[GUI][WARN] Failed to clean up {component}: {exception}");
}
} }
private void OnTitleBarPointerPressed(object? sender, PointerPressedEventArgs e) private void OnTitleBarPointerPressed(object? sender, PointerPressedEventArgs e)
@@ -889,17 +782,14 @@ public partial class MainWindow : Window
return; return;
} }
var state = GetMaximizeButtonState(WindowState); var isMaximized = WindowState == WindowState.Maximized;
glyph.Text = state.Glyph; glyph.Text = isMaximized ? "❐" : "□";
ToolTip.SetTip(button, state.ToolTip); ToolTip.SetTip(button, isMaximized ? "Restore" : "Maximize");
AutomationProperties.SetName(button, state.AutomationName); AutomationProperties.SetName(
button,
isMaximized ? "Restore window" : "Maximize window");
} }
internal static WindowMaximizeButtonState GetMaximizeButtonState(WindowState windowState) =>
windowState == WindowState.Maximized
? new("filter_none", "Restore", "Restore window")
: new("crop_square", "Maximize", "Maximize window");
private void UpdateWindowChromeState() private void UpdateWindowChromeState()
{ {
UpdateMaximizeButton(); UpdateMaximizeButton();
@@ -1002,7 +892,6 @@ public partial class MainWindow : Window
EnvLogNpToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_NP"); EnvLogNpToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_NP");
EnvGuestImageCpuSyncToggle.IsChecked = EnvGuestImageCpuSyncToggle.IsChecked =
_settings.EnvironmentToggles.Contains("SHARPEMU_GUEST_IMAGE_CPU_SYNC"); _settings.EnvironmentToggles.Contains("SHARPEMU_GUEST_IMAGE_CPU_SYNC");
DefaultProfileBox.Text = _settings.DefaultProfile;
WindowModeBox.SelectedIndex = ChoiceIndex(_settings.WindowMode, "Windowed", "Borderless", "Exclusive"); WindowModeBox.SelectedIndex = ChoiceIndex(_settings.WindowMode, "Windowed", "Borderless", "Exclusive");
LoadHostDisplayOptions(); LoadHostDisplayOptions();
ScalingModeBox.SelectedIndex = ChoiceIndex(_settings.ScalingMode, "Fit", "Cover", "Stretch", "Integer"); ScalingModeBox.SelectedIndex = ChoiceIndex(_settings.ScalingMode, "Fit", "Cover", "Stretch", "Integer");
@@ -1229,8 +1118,6 @@ public partial class MainWindow : Window
} }
} }
private const string DefaultProfileEnvironmentName = "SHARPEMU_DEFAULT_PROFILE";
private string SelectedLogLevel() private string SelectedLogLevel()
{ {
return LogLevelBox.SelectedIndex switch return LogLevelBox.SelectedIndex switch
@@ -1722,71 +1609,6 @@ public partial class MainWindow : Window
// ---- Game context menu ---- // ---- Game context menu ----
private void OnGameListSelectionChanged()
{
if (_restoringGameSelection)
{
return;
}
if (GameList.SelectedItem is AddFolderTile)
{
_restoringGameSelection = true;
try
{
GameList.SelectedItem = _lastSelectedGame;
}
finally
{
_restoringGameSelection = false;
}
// A held direction must not reopen the picker after it closes.
_navLeftNextAt = long.MaxValue;
_navRightNextAt = long.MaxValue;
StartAddFolderFromTile();
return;
}
_lastSelectedGame = GameList.SelectedItem as GameEntry;
UpdateSelectedGame();
}
private void OnGameListDoubleTapped(object? sender, TappedEventArgs e)
{
var item = (e.Source as Visual)?.FindAncestorOfType<ListBoxItem>(includeSelf: true);
if (item?.DataContext is AddFolderTile)
{
e.Handled = true;
return;
}
LaunchSelected();
}
private void StartAddFolderFromTile()
{
if (_addFolderInProgress)
{
return;
}
_addFolderInProgress = true;
_ = AddFolderFromTileAsync();
}
private async Task AddFolderFromTileAsync()
{
try
{
await AddFolderAsync();
}
finally
{
_addFolderInProgress = false;
}
}
/// <summary> /// <summary>
/// Selects the tile under the pointer before its context menu opens, and /// Selects the tile under the pointer before its context menu opens, and
/// suppresses the menu on empty grid space. /// suppresses the menu on empty grid space.
@@ -1928,6 +1750,7 @@ public partial class MainWindow : Window
} }
else else
{ {
UpdateSelectedGameTexts();
UpdateRunButtons(); UpdateRunButtons();
if (selectedAfter is not null && backgroundsChanged?.Contains(selectedAfter) == true) if (selectedAfter is not null && backgroundsChanged?.Contains(selectedAfter) == true)
{ {
@@ -1955,21 +1778,26 @@ public partial class MainWindow : Window
EmptyStateHint.Text = hasFilter EmptyStateHint.Text = hasFilter
? Localization.Instance.Format("Library.Empty.SearchHint", query) ? Localization.Instance.Format("Library.Empty.SearchHint", query)
: Localization.Instance.Get("Library.Empty.Hint"); : Localization.Instance.Get("Library.Empty.Hint");
EmptyAddFolderButton.IsVisible = !hasFilter;
} }
private void UpdateSelectedGame() private void UpdateSelectedGame()
{ {
if (GameList.SelectedItem is GameEntry game) if (GameList.SelectedItem is GameEntry game)
{ {
LibrarySelectedDetails.DataContext = game; UpdateSelectedGameTexts();
LibrarySelectedDetails.IsVisible = true; SelectedCoverPanel.DataContext = game;
SelectedBadgesRow.DataContext = game;
SelectedBadgesRow.IsVisible = true;
_ = UpdateBackdropAsync(game); _ = UpdateBackdropAsync(game);
PlaySelectedGamePreview(game); PlaySelectedGamePreview(game);
} }
else else
{ {
LibrarySelectedDetails.DataContext = null; UpdateSelectedGameTexts();
LibrarySelectedDetails.IsVisible = false; SelectedCoverPanel.DataContext = null;
SelectedBadgesRow.DataContext = null;
SelectedBadgesRow.IsVisible = false;
_ = UpdateBackdropAsync(null); _ = UpdateBackdropAsync(null);
_sndPreview.Stop(); _sndPreview.Stop();
} }
@@ -1977,6 +1805,25 @@ public partial class MainWindow : Window
UpdateRunButtons(); UpdateRunButtons();
} }
/// <summary>
/// Text-only refresh of the launch bar's title/path, split out of
/// <see cref="UpdateSelectedGame"/> so a language change can re-apply it
/// without restarting the backdrop fade or preview music.
/// </summary>
private void UpdateSelectedGameTexts()
{
if (GameList.SelectedItem is GameEntry game)
{
SelectedGameTitle.Text = game.Name;
SelectedGamePath.Text = game.Path;
}
else
{
SelectedGameTitle.Text = Localization.Instance.Get("Launch.NoGameSelected");
SelectedGamePath.Text = Localization.Instance.Get("Launch.NoGameHint");
}
}
/// <summary> /// <summary>
/// Loops the selected game's sce_sys/snd0.at9 preview music, console /// Loops the selected game's sce_sys/snd0.at9 preview music, console
/// home screen style. Silent while a game is running or when disabled /// home screen style. Silent while a game is running or when disabled
@@ -2026,10 +1873,18 @@ public partial class MainWindow : Window
if (WindowState == WindowState.Minimized) if (WindowState == WindowState.Minimized)
{ {
_sndPreview.Pause(); _sndPreview.Pause();
if (SessionLoadingPopup is { } popup)
{
popup.IsOpen = false;
}
} }
else else
{ {
_sndPreview.Resume(); _sndPreview.Resume();
if (SessionLoadingPopup is { } popup)
{
popup.IsOpen = _sessionLoadingActive;
}
} }
} }
} }
@@ -2160,20 +2015,10 @@ public partial class MainWindow : Window
continue; continue;
} }
if (string.Equals(name, DefaultProfileEnvironmentName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
Environment.SetEnvironmentVariable(name, value); Environment.SetEnvironmentVariable(name, value);
_appliedEnvironmentVariables.Add(name); _appliedEnvironmentVariables.Add(name);
} }
Environment.SetEnvironmentVariable(
DefaultProfileEnvironmentName,
GuiSettings.NormalizeDefaultProfile(_settings.DefaultProfile));
_appliedEnvironmentVariables.Add(DefaultProfileEnvironmentName);
Environment.SetEnvironmentVariable( Environment.SetEnvironmentVariable(
"SHARPEMU_RENDER_SCALE", "SHARPEMU_RENDER_SCALE",
_settings.RenderResolutionScale.ToString( _settings.RenderResolutionScale.ToString(
@@ -2193,14 +2038,16 @@ public partial class MainWindow : Window
}; };
_isRunning = true; _isRunning = true;
_isStopping = false;
_runningGameName = displayName; _runningGameName = displayName;
_runningGameTitleId = resolvedTitleId; _runningGameTitleId = resolvedTitleId;
_runningSinceUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); _runningSinceUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
StatusDot.Fill = SuccessLineBrush;
StatusText.Text = Localization.Instance.Format("Launch.Running", displayName);
StatusBarRight.Text = Localization.Instance.Format("Status.Running", displayName); StatusBarRight.Text = Localization.Instance.Format("Status.Running", displayName);
UpdateRunButtons(); UpdateRunButtons();
UpdateDiscordPresence(); UpdateDiscordPresence();
BeginSessionUi();
_pendingLaunch = new PendingLaunch( _pendingLaunch = new PendingLaunch(
Path.GetFullPath(ebootPath), Path.GetFullPath(ebootPath),
displayName, displayName,
@@ -2246,14 +2093,15 @@ public partial class MainWindow : Window
} }
_isStopping = true; _isStopping = true;
UpdateRunButtons(); StopButton.IsEnabled = false;
ShowSessionLoading("Closing game", "Waiting for the emulation session to exit...");
_emulator.Stop(); _emulator.Stop();
_runningGameName = null; _runningGameName = null;
_runningGameTitleId = null; _runningGameTitleId = null;
StatusText.Text = Localization.Instance.Get("Launch.Stopping");
StatusBarRight.Text = Localization.Instance.Get("Status.Stopping"); StatusBarRight.Text = Localization.Instance.Get("Status.Stopping");
UpdateDiscordPresence(); UpdateDiscordPresence();
ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true && _consoleWindow is null; ReturnToLibraryWhileStopping();
Console.Error.WriteLine("[GUI][INFO] Waiting for the SDL game process to exit.");
} }
/// <summary> /// <summary>
@@ -2295,7 +2143,7 @@ public partial class MainWindow : Window
_emulator?.Dispose(); _emulator?.Dispose();
_emulator = null; _emulator = null;
_pendingLaunch = null; _pendingLaunch = null;
ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true && _consoleWindow is null; EndSessionUi();
var meaningKey = exitCode switch var meaningKey = exitCode switch
{ {
@@ -2317,6 +2165,10 @@ public partial class MainWindow : Window
brush); brush);
CloseFileLogSoon(); CloseFileLogSoon();
StatusDot.Fill = exitCode == 0 || stoppedByUser ? (IBrush)SuccessLineBrush : ErrorLineBrush;
StatusText.Text = stoppedByUser
? "Game closed by the user."
: Localization.Instance.Format("Launch.Exited", exitCode, meaning);
StatusBarRight.Text = Localization.Instance.Get("Status.Idle"); StatusBarRight.Text = Localization.Instance.Get("Status.Idle");
_runningGameName = null; _runningGameName = null;
_runningGameTitleId = null; _runningGameTitleId = null;
@@ -2397,6 +2249,137 @@ public partial class MainWindow : Window
private void OnEmulatorOutput(string line, bool isError) private void OnEmulatorOutput(string line, bool isError)
{ {
_pendingLines.Enqueue((line, isError)); _pendingLines.Enqueue((line, isError));
if (!line.Contains("Vulkan VideoOut presented first frame:", StringComparison.Ordinal) &&
!line.Contains("Vulkan VideoOut ready:", StringComparison.Ordinal))
{
return;
}
Dispatcher.UIThread.Post(() =>
{
if (_isRunning && !_isStopping)
{
ShowSessionStatus("Game is running");
}
});
}
private void BeginSessionUi()
{
_isStopping = false;
AnimateLibraryBlur(LaunchBlurRadius);
ShowSessionLoading("Loading game", "Preparing the emulation session...");
LaunchBar.IsVisible = true;
}
private void EndSessionUi()
{
HideSessionLoading();
AnimateLibraryBlur(0, clearWhenComplete: true);
LaunchBar.IsVisible = true;
ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true && _consoleWindow is null;
}
private void AnimateLibraryBlur(double targetRadius, bool clearWhenComplete = false)
{
_libraryBlur ??= new BlurEffect();
PagesHost.Effect = _libraryBlur;
_libraryBlurStartRadius = _libraryBlur.Radius;
_libraryBlurTargetRadius = Math.Max(0, targetRadius);
_libraryBlurStartedAt = Stopwatch.GetTimestamp();
_clearLibraryBlurWhenComplete = clearWhenComplete && _libraryBlurTargetRadius == 0;
if (Math.Abs(_libraryBlurStartRadius - _libraryBlurTargetRadius) < 0.01)
{
CompleteLibraryBlur();
return;
}
_libraryBlurTimer.Start();
}
private void AdvanceLibraryBlur()
{
if (_libraryBlur is null)
{
_libraryBlurTimer.Stop();
return;
}
var elapsed = (Stopwatch.GetTimestamp() - _libraryBlurStartedAt) /
(double)Stopwatch.Frequency;
var progress = Math.Clamp(elapsed / BlurTransitionSeconds, 0, 1);
// Cubic ease-out gives the loading transition a quick response while
// keeping the final change of sharpness unobtrusive.
var easedProgress = 1 - Math.Pow(1 - progress, 3);
_libraryBlur.Radius = _libraryBlurStartRadius +
((_libraryBlurTargetRadius - _libraryBlurStartRadius) * easedProgress);
if (progress >= 1)
{
CompleteLibraryBlur();
}
}
private void CompleteLibraryBlur()
{
_libraryBlurTimer.Stop();
if (_libraryBlur is not null)
{
_libraryBlur.Radius = _libraryBlurTargetRadius;
}
if (_clearLibraryBlurWhenComplete)
{
PagesHost.Effect = null;
_libraryBlur = null;
_clearLibraryBlurWhenComplete = false;
}
}
private void ClearLibraryBlur()
{
_libraryBlurTimer.Stop();
_libraryBlur = null;
_clearLibraryBlurWhenComplete = false;
PagesHost.Effect = null;
}
private void ShowSessionLoading(string title, string detail)
{
SessionLoadingTitle.Text = title;
SessionLoadingTitle.IsVisible = true;
SessionLoadingDetail.Text = detail;
SessionLoadingDetail.IsVisible = true;
SessionLoadingProgress.IsVisible = true;
_sessionLoadingActive = true;
SessionLoadingPopup.IsOpen = IsActive && WindowState != WindowState.Minimized;
}
private void ShowSessionStatus(string message)
{
SessionLoadingTitle.Text = message;
SessionLoadingTitle.IsVisible = true;
SessionLoadingDetail.IsVisible = false;
SessionLoadingProgress.IsVisible = false;
_sessionLoadingActive = true;
SessionLoadingPopup.IsOpen = IsActive && WindowState != WindowState.Minimized;
}
private void HideSessionLoading()
{
_sessionLoadingActive = false;
SessionLoadingPopup.IsOpen = false;
}
private void ReturnToLibraryWhileStopping()
{
AnimateLibraryBlur(LaunchBlurRadius);
ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true && _consoleWindow is null;
LaunchBar.IsVisible = true;
UpdateRunButtons();
Console.Error.WriteLine("[GUI][INFO] Waiting for the SDL game process to exit.");
} }
private void OpenFileLog(string? titleId) private void OpenFileLog(string? titleId)
@@ -2453,23 +2436,8 @@ public partial class MainWindow : Window
private void UpdateRunButtons() private void UpdateRunButtons()
{ {
LaunchButton.Classes.Remove("accent"); LaunchButton.IsEnabled = !_isRunning && GameList.SelectedItem is GameEntry;
LaunchButton.Classes.Remove("danger"); StopButton.IsEnabled = _isRunning && !_isStopping;
if (_isRunning)
{
LaunchButton.Classes.Add("danger");
LaunchButton.Content = Localization.Instance.Get(
_isStopping ? "Launch.Stopping" : "Launch.Stop");
LaunchButton.IsEnabled = !_isStopping;
}
else
{
LaunchButton.Classes.Add("accent");
LaunchButton.Content = Localization.Instance.Get("Launch.Launch");
LaunchButton.IsEnabled = GameList.SelectedItem is GameEntry;
}
OpenFileButton.IsEnabled = !_isRunning; OpenFileButton.IsEnabled = !_isRunning;
} }
-9
View File
@@ -36,7 +36,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AvaloniaResource Include="Assets\Fonts\MaterialSymbolsRounded-400.ttf" />
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" /> <AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
<AvaloniaResource Include="..\..\assets\images\github.png" Link="Assets/github.png" /> <AvaloniaResource Include="..\..\assets\images\github.png" Link="Assets/github.png" />
<AvaloniaResource Include="..\..\assets\images\discord.png" Link="Assets/discord.png" /> <AvaloniaResource Include="..\..\assets\images\discord.png" Link="Assets/discord.png" />
@@ -45,14 +44,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
<AvaloniaResource Include="..\..\assets\images\pic0.png" Link="Assets/pic0.png" /> <AvaloniaResource Include="..\..\assets\images\pic0.png" Link="Assets/pic0.png" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Include="..\..\LICENSES\Apache-2.0.txt"
Link="licenses\Apache-2.0.txt"
CopyToOutputDirectory="PreserveNewest"
CopyToPublishDirectory="PreserveNewest"
TargetPath="licenses\Apache-2.0.txt" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="Languages\*.json"> <EmbeddedResource Include="Languages\*.json">
<LogicalName>Languages.%(Filename)%(Extension)</LogicalName> <LogicalName>Languages.%(Filename)%(Extension)</LogicalName>
-15
View File
@@ -11,21 +11,6 @@ Window and text defaults shared by all launcher views.
<Setter Property="Foreground" Value="{StaticResource TextBrush}" /> <Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style> </Style>
<Style Selector="TextBlock.materialSymbol">
<Setter Property="FontFamily" Value="{StaticResource MaterialSymbolsRounded}" />
<Setter Property="FontFeatures" Value="+liga" />
<Setter Property="FontSize" Value="18" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="LineHeight" Value="20" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="TextAlignment" Value="Center" />
</Style>
<Style Selector="TextBlock.materialSymbol.compact">
<Setter Property="FontSize" Value="16" />
<Setter Property="LineHeight" Value="18" />
</Style>
<Style Selector="TextBlock.sectionTitle"> <Style Selector="TextBlock.sectionTitle">
<Setter Property="FontSize" Value="11" /> <Setter Property="FontSize" Value="11" />
<Setter Property="FontWeight" Value="SemiBold" /> <Setter Property="FontWeight" Value="SemiBold" />
@@ -19,12 +19,6 @@ Window chrome button sizing and interaction states.
<Setter Property="HorizontalContentAlignment" Value="Center" /> <Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" /> <Setter Property="VerticalContentAlignment" Value="Center" />
</Style> </Style>
<Style Selector="TextBlock.windowChromeGlyph">
<Setter Property="IsHitTestVisible" Value="False" />
</Style>
<Style Selector="TextBlock.windowCloseGlyph">
<Setter Property="Margin" Value="0,-1,0,1" />
</Style>
<Style Selector="Button.windowChrome:pointerover /template/ ContentPresenter#PART_ContentPresenter"> <Style Selector="Button.windowChrome:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" /> <Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" /> <Setter Property="Foreground" Value="{StaticResource TextBrush}" />
+15 -104
View File
@@ -6,121 +6,32 @@ Cover-art library item states and motion.
<Styles xmlns="https://github.com/avaloniaui" <Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style Selector="ListBox.tileGrid">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Hidden" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Disabled" />
</Style>
<Style Selector="ListBox.tileGrid ListBoxItem"> <Style Selector="ListBox.tileGrid ListBoxItem">
<Setter Property="Width" Value="160" /> <Setter Property="Padding" Value="10" />
<Setter Property="Height" Value="172" /> <Setter Property="Margin" Value="5" />
<Setter Property="Padding" Value="4" /> <Setter Property="CornerRadius" Value="14" />
<Setter Property="Margin" Value="0,8,12,8" />
<Setter Property="CornerRadius" Value="9" />
<Setter Property="Background" Value="Transparent" /> <Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" /> <Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderBrush" Value="Transparent" /> <Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="Opacity" Value="0.72" />
<Setter Property="RenderTransformOrigin" Value="50%,50%" />
<Setter Property="RenderTransform" Value="translateY(0px)" /> <Setter Property="RenderTransform" Value="translateY(0px)" />
<Setter Property="Transitions"> <Setter Property="Transitions">
<Transitions> <Transitions>
<DoubleTransition Property="Opacity" <TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.12" />
Duration="0:0:0.11"
Easing="QuadraticEaseOut" />
<TransformOperationsTransition Property="RenderTransform"
Duration="0:0:0.16"
Easing="CubicEaseOut" />
</Transitions> </Transitions>
</Setter> </Setter>
</Style> </Style>
<Style Selector="ListBox.tileGrid ListBoxItem:pointerover, <Style Selector="ListBox.tileGrid ListBoxItem:pointerover">
ListBox.tileGrid ListBoxItem:selected"> <Setter Property="RenderTransform" Value="translateY(-3px)" />
<Setter Property="Opacity" Value="1" />
<Setter Property="RenderTransform" Value="translateY(-5px) scale(1.035)" />
</Style> </Style>
<Style Selector="ListBox.tileGrid ListBoxItem /template/ ContentPresenter#PART_ContentPresenter, <Style Selector="ListBox.tileGrid ListBoxItem:pointerover /template/ ContentPresenter#PART_ContentPresenter">
ListBox.tileGrid ListBoxItem:pointerover /template/ ContentPresenter#PART_ContentPresenter, <Setter Property="Background" Value="{StaticResource TileHoverBrush}" />
ListBox.tileGrid ListBoxItem:selected /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
</Style> </Style>
<Style Selector="ListBox.tileGrid ListBoxItem:selected /template/ ContentPresenter#PART_ContentPresenter">
<Style Selector="Border.libraryGameCard"> <Setter Property="Background" Value="{StaticResource TileSelectedBrush}" />
<Setter Property="CornerRadius" Value="7" /> <Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
<Setter Property="BorderThickness" Value="2" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BoxShadow" Value="0 12 28 0 #61000000" />
<Setter Property="Transitions">
<Transitions>
<BrushTransition Property="BorderBrush"
Duration="0:0:0.12"
Easing="QuadraticEaseOut" />
</Transitions>
</Setter>
</Style> </Style>
<Style Selector="ListBox.tileGrid ListBoxItem:selected Border.libraryGameCard"> <Style Selector="ListBox.tileGrid ListBoxItem:selected:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="BorderBrush" Value="#E6FFFFFF" /> <Setter Property="Background" Value="{StaticResource TileSelectedBrush}" />
</Style> <Setter Property="BorderBrush" Value="{StaticResource AccentHoverBrush}" />
<Style Selector="Border.libraryGameCard Border.coverClip">
<Setter Property="CornerRadius" Value="5" />
</Style>
<Style Selector="Border.addFolderTile">
<Setter Property="CornerRadius" Value="7" />
<Setter Property="Background" Value="#0CFFFFFF" />
<Setter Property="BorderBrush" Value="#2EFFFFFF" />
<Setter Property="BorderThickness" Value="1.5" />
<Setter Property="BoxShadow" Value="0 12 28 0 #3A000000" />
<Setter Property="Transitions">
<Transitions>
<BrushTransition Property="Background"
Duration="0:0:0.18"
Easing="CubicEaseOut" />
<BrushTransition Property="BorderBrush"
Duration="0:0:0.18"
Easing="CubicEaseOut" />
</Transitions>
</Setter>
</Style>
<Style Selector="Border.addFolderTileIcon">
<Setter Property="Width" Value="60" />
<Setter Property="Height" Value="60" />
<Setter Property="CornerRadius" Value="30" />
<Setter Property="Background" Value="#1BFFFFFF" />
<Setter Property="Transitions">
<Transitions>
<BrushTransition Property="Background"
Duration="0:0:0.18"
Easing="CubicEaseOut" />
</Transitions>
</Setter>
</Style>
<Style Selector="TextBlock.addFolderGlyph">
<Setter Property="Foreground" Value="#A6FFFFFF" />
<Setter Property="Transitions">
<Transitions>
<BrushTransition Property="Foreground"
Duration="0:0:0.18"
Easing="CubicEaseOut" />
</Transitions>
</Setter>
</Style>
<Style Selector="TextBlock.addFolderIconGlyph">
<Setter Property="RenderTransform" Value="translateY(-7px)" />
</Style>
<Style Selector="ListBox.tileGrid ListBoxItem:pointerover TextBlock.addFolderGlyph,
ListBox.tileGrid ListBoxItem:selected TextBlock.addFolderGlyph">
<Setter Property="Foreground" Value="White" />
</Style>
<Style Selector="ListBox.tileGrid ListBoxItem:pointerover Border.addFolderTile,
ListBox.tileGrid ListBoxItem:selected Border.addFolderTile">
<Setter Property="Background" Value="#1AFFFFFF" />
<Setter Property="BorderBrush" Value="#55FFFFFF" />
</Style>
<Style Selector="ListBox.tileGrid ListBoxItem:pointerover Border.addFolderTileIcon,
ListBox.tileGrid ListBoxItem:selected Border.addFolderTileIcon">
<Setter Property="Background" Value="#2EFFFFFF" />
</Style> </Style>
</Styles> </Styles>
@@ -1,229 +0,0 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
Options page navigation, section transitions and content layout
-->
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:SharpEmu.GUI"
xmlns:settingsControls="using:SharpEmu.GUI.Controls.Settings">
<Style Selector="Border.optionsNavSurface">
<Setter Property="Width" Value="244" />
<Setter Property="Padding" Value="0" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="VerticalAlignment" Value="Top" />
</Style>
<Style Selector="Border.optionsNavIndicator">
<Setter Property="Height" Value="54" />
<Setter Property="Margin" Value="0,3,0,0" />
<Setter Property="CornerRadius" Value="12" />
<Setter Property="Background" Value="#13FFFFFF" />
<Setter Property="BorderBrush" Value="#E6FFFFFF" />
<Setter Property="BorderThickness" Value="2" />
<Setter Property="BoxShadow" Value="0 8 24 0 #24000000" />
</Style>
<Style Selector="Button.optionsNav">
<Setter Property="Height" Value="61" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Padding" Value="16,0" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="CornerRadius" Value="12" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Foreground" Value="#8FFFFFFF" />
<Setter Property="FontSize" Value="14" />
<Setter Property="FontWeight" Value="Medium" />
<Setter Property="FocusAdorner" Value="{x:Null}" />
<Setter Property="RenderTransform" Value="none" />
</Style>
<Style Selector="Button.optionsNav /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="Foreground" Value="#8FFFFFFF" />
</Style>
<Style Selector="Button.optionsNav:pointerover /template/ ContentPresenter#PART_ContentPresenter,
Button.optionsNav:focus-visible /template/ ContentPresenter#PART_ContentPresenter,
Button.optionsNav.active /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="Foreground" Value="White" />
</Style>
<Style Selector="Button.optionsNav:pressed">
<Setter Property="RenderTransform" Value="none" />
</Style>
<Style Selector="TextBlock.optionsNavIcon">
<Setter Property="Width" Value="20" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="Padding" Value="0,0,0,5" />
<Setter Property="Foreground" Value="#8FFFFFFF" />
<Setter Property="Transitions">
<Transitions>
<BrushTransition Property="Foreground"
Duration="0:0:0.18"
Easing="CubicEaseOut" />
</Transitions>
</Setter>
</Style>
<Style Selector="TextBlock.optionsNavLabel">
<Setter Property="FontSize" Value="14" />
<Setter Property="FontWeight" Value="Medium" />
<Setter Property="LineHeight" Value="20" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Padding" Value="0,0,0,4" />
<Setter Property="Foreground" Value="#8FFFFFFF" />
<Setter Property="Transitions">
<Transitions>
<BrushTransition Property="Foreground"
Duration="0:0:0.18"
Easing="CubicEaseOut" />
</Transitions>
</Setter>
</Style>
<Style Selector="Button.optionsNav:pointerover TextBlock.optionsNavIcon,
Button.optionsNav:focus-visible TextBlock.optionsNavIcon,
Button.optionsNav.active TextBlock.optionsNavIcon">
<Setter Property="Foreground" Value="White" />
</Style>
<Style Selector="Button.optionsNav:pointerover TextBlock.optionsNavLabel,
Button.optionsNav:focus-visible TextBlock.optionsNavLabel,
Button.optionsNav.active TextBlock.optionsNavLabel">
<Setter Property="Foreground" Value="White" />
</Style>
<Style Selector="Border.optionsContentSurface">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="ClipToBounds" Value="True" />
</Style>
<Style Selector="ScrollViewer.optionsSectionPanel">
<Setter Property="Opacity" Value="0" />
<Setter Property="RenderTransform" Value="translateY(22px)" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="HorizontalScrollBarVisibility" Value="Disabled" />
<Setter Property="VerticalScrollBarVisibility" Value="Auto" />
<Setter Property="Transitions">
<Transitions>
<DoubleTransition Property="Opacity"
Duration="0:0:0.18"
Easing="CubicEaseOut" />
<TransformOperationsTransition Property="RenderTransform"
Duration="0:0:0.28"
Easing="CubicEaseOut" />
</Transitions>
</Setter>
</Style>
<Style Selector="ScrollViewer.optionsSectionPanel.active">
<Setter Property="Opacity" Value="1" />
<Setter Property="RenderTransform" Value="translateY(0px)" />
</Style>
<Style Selector="Border.optionsGroup">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="0" />
</Style>
<Style Selector="Border.optionsInfoRow">
<Setter Property="MinHeight" Value="70" />
<Setter Property="Padding" Value="18,12" />
<Setter Property="CornerRadius" Value="14" />
<Setter Property="Background" Value="#06FFFFFF" />
<Setter Property="BorderBrush" Value="#0AFFFFFF" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style Selector="local|SettingRow.optionRow:focus-within /template/ Border#PART_RowSurface">
<Setter Property="Background" Value="#0DFFFFFF" />
<Setter Property="BorderBrush" Value="#22FFFFFF" />
</Style>
<Style Selector="ComboBox.optionValue, settingsControls|SplitNumericUpDown.optionValue, TextBox.optionValue">
<Setter Property="Width" Value="190" />
<Setter Property="MinHeight" Value="44" />
<Setter Property="CornerRadius" Value="14" />
<Setter Property="Background" Value="#12FFFFFF" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
</Style>
<Style Selector="TextBox.optionValue">
<Setter Property="Padding" Value="17,0" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="TextAlignment" Value="Left" />
<Setter Property="ClipToBounds" Value="True" />
<Setter Property="FocusAdorner" Value="{x:Null}" />
</Style>
<Style Selector="TextBox.optionValue /template/ Border#PART_BorderElement">
<Setter Property="CornerRadius" Value="14" />
<Setter Property="ClipToBounds" Value="True" />
<Setter Property="Background" Value="#12FFFFFF" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
</Style>
<!-- The outer split-input surface owns hover and focus animation -->
<Style Selector="TextBox.splitNumericTextBox /template/ Border#PART_BorderElement,
TextBox.splitNumericTextBox:pointerover /template/ Border#PART_BorderElement,
TextBox.splitNumericTextBox:focus /template/ Border#PART_BorderElement">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
</Style>
<Style Selector="ToggleSwitch.optionToggle">
<Setter Property="Theme" Value="{StaticResource OptionToggleTheme}" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style Selector="Button.optionAction">
<Setter Property="MinWidth" Value="156" />
<Setter Property="MinHeight" Value="44" />
<Setter Property="Padding" Value="18,0" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="CornerRadius" Value="12" />
<Setter Property="Background" Value="#12FFFFFF" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Foreground" Value="White" />
<Setter Property="FontWeight" Value="Medium" />
<Setter Property="FocusAdorner" Value="{x:Null}" />
</Style>
<Style Selector="Button.optionAction /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="#12FFFFFF" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Transitions">
<Transitions>
<BrushTransition Property="Background"
Duration="0:0:0.18"
Easing="CubicEaseOut" />
</Transitions>
</Setter>
</Style>
<Style Selector="Button.optionAction:pointerover /template/ ContentPresenter#PART_ContentPresenter,
Button.optionAction:focus-visible /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="#20FFFFFF" />
<Setter Property="BorderBrush" Value="Transparent" />
</Style>
</Styles>
@@ -1,76 +0,0 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
Options-only control resources: compact toggle theme
-->
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ControlTheme x:Key="OptionToggleTheme" TargetType="ToggleSwitch">
<Setter Property="Width" Value="46" />
<Setter Property="Height" Value="26" />
<Setter Property="MinWidth" Value="46" />
<Setter Property="MinHeight" Value="26" />
<Setter Property="Padding" Value="0" />
<Setter Property="Background" Value="#2BFFFFFF" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="FocusAdorner" Value="{x:Null}" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="KnobTransitions">
<Transitions>
<DoubleTransition Property="Canvas.Left"
Duration="0:0:0.18"
Easing="CubicEaseOut" />
</Transitions>
</Setter>
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="PART_Track"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
Padding="3"
Background="{TemplateBinding Background}"
BorderThickness="0"
CornerRadius="13"
ClipToBounds="True">
<Canvas x:Name="PART_SwitchKnob"
Width="20"
Height="20"
HorizontalAlignment="Left"
VerticalAlignment="Center">
<Grid x:Name="PART_MovingKnobs"
Width="20"
Height="20"
Canvas.Left="0">
<Border x:Name="PART_Thumb"
Background="{StaticResource TextBrush}"
BorderThickness="0"
CornerRadius="10" />
</Grid>
</Canvas>
<Border.Transitions>
<Transitions>
<BrushTransition Property="Background"
Duration="0:0:0.16"
Easing="CubicEaseOut" />
</Transitions>
</Border.Transitions>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:pointerover /template/ Border#PART_Track">
<Setter Property="Background" Value="#38FFFFFF" />
</Style>
<Style Selector="^:checked /template/ Border#PART_Track">
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
</Style>
<Style Selector="^:checked:pointerover /template/ Border#PART_Track">
<Setter Property="Background" Value="{StaticResource AccentHoverBrush}" />
</Style>
<Style Selector="^:disabled /template/ Border#PART_Track">
<Setter Property="Opacity" Value="0.45" />
</Style>
</ControlTheme>
</ResourceDictionary>
@@ -8,51 +8,26 @@ Control theme for the shared launcher settings row.
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SharpEmu.GUI"> xmlns:local="clr-namespace:SharpEmu.GUI">
<ControlTheme x:Key="{x:Type local:SettingRow}" TargetType="local:SettingRow"> <ControlTheme x:Key="{x:Type local:SettingRow}" TargetType="local:SettingRow">
<Setter Property="MinHeight" Value="70" />
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Border x:Name="PART_RowSurface" <Grid ColumnDefinitions="*,Auto">
Background="#06FFFFFF" <StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
BorderBrush="#0AFFFFFF" <TextBlock x:Name="PART_Label" Text="{TemplateBinding Label}" FontSize="13" />
BorderThickness="1" <TextBlock Text="{TemplateBinding Description}" FontSize="11"
CornerRadius="14" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap"
Padding="18,12"> IsVisible="{Binding Description, RelativeSource={RelativeSource TemplatedParent},
<Grid ColumnDefinitions="*,Auto"> Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
<StackPanel VerticalAlignment="Center" Spacing="3" Margin="0,0,18,0"> </StackPanel>
<TextBlock x:Name="PART_Label" <StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10" VerticalAlignment="Center">
Text="{TemplateBinding Label}" <ToggleSwitch OnContent="Override" OffContent="Override" MinWidth="0"
FontSize="14" VerticalAlignment="Center"
FontWeight="Medium" IsVisible="{TemplateBinding ShowOverride}"
TextWrapping="Wrap" IsChecked="{Binding IsOverridden, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" />
Foreground="{StaticResource TextBrush}" /> <ContentPresenter x:Name="PART_Slot"
<TextBlock Text="{TemplateBinding Description}" Content="{TemplateBinding Content}"
FontSize="11" VerticalAlignment="Center" />
FontWeight="Normal" </StackPanel>
Foreground="{StaticResource MutedBrush}" </Grid>
LineHeight="16"
MaxWidth="690"
HorizontalAlignment="Left"
TextAlignment="Left"
TextWrapping="Wrap"
IsVisible="{Binding Description, RelativeSource={RelativeSource TemplatedParent},
Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
</StackPanel>
<StackPanel Grid.Column="1"
Orientation="Horizontal"
Spacing="12"
VerticalAlignment="Center">
<ToggleSwitch OnContent="Override"
OffContent="Override"
MinWidth="0"
VerticalAlignment="Center"
IsVisible="{TemplateBinding ShowOverride}"
IsChecked="{Binding IsOverridden, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" />
<ContentPresenter x:Name="PART_Slot"
Content="{TemplateBinding Content}"
VerticalAlignment="Center" />
</StackPanel>
</Grid>
</Border>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
</ControlTheme> </ControlTheme>
@@ -1,197 +0,0 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
Control themes for SplitNumericUpDown and its spinner parts
-->
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:settingsControls="using:SharpEmu.GUI.Controls.Settings">
<ControlTheme x:Key="SplitNumericRepeatButtonTheme" TargetType="RepeatButton">
<Setter Property="MinWidth" Value="0" />
<Setter Property="MinHeight" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Foreground" Value="#B8FFFFFF" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="FocusAdorner" Value="{x:Null}" />
<Setter Property="Template">
<ControlTemplate>
<ContentPresenter x:Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
BorderBrush="Transparent"
BorderThickness="0"
Content="{TemplateBinding Content}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}">
<ContentPresenter.Transitions>
<Transitions>
<BrushTransition Property="Background"
Duration="0:0:0.18"
Easing="CubicEaseOut" />
<BrushTransition Property="Foreground"
Duration="0:0:0.18"
Easing="CubicEaseOut" />
</Transitions>
</ContentPresenter.Transitions>
</ContentPresenter>
</ControlTemplate>
</Setter>
<Style Selector="^:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="#16FFFFFF" />
<Setter Property="Foreground" Value="White" />
</Style>
<Style Selector="^:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="#22FFFFFF" />
<Setter Property="Foreground" Value="White" />
</Style>
<Style Selector="^:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Opacity" Value="0.45" />
</Style>
</ControlTheme>
<ControlTheme x:Key="SplitNumericSpinnerTheme" TargetType="ButtonSpinner">
<Setter Property="MinHeight" Value="44" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="Background" Value="#12FFFFFF" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Foreground" Value="#E8FFFFFF" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Stretch" />
<Setter Property="Focusable" Value="True" />
<Setter Property="Template">
<ControlTemplate>
<Grid ColumnDefinitions="*,52"
ColumnSpacing="8">
<Border x:Name="PART_ValueSurface"
MinHeight="{TemplateBinding MinHeight}"
Background="{TemplateBinding Background}"
BorderBrush="Transparent"
BorderThickness="0"
CornerRadius="14"
ClipToBounds="True">
<ContentPresenter x:Name="PART_ContentPresenter"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" />
<Border.Transitions>
<Transitions>
<BrushTransition Property="Background"
Duration="0:0:0.18"
Easing="CubicEaseOut" />
</Transitions>
</Border.Transitions>
</Border>
<Border x:Name="PART_SpinnerSurface"
Grid.Column="1"
MinHeight="{TemplateBinding MinHeight}"
Background="{TemplateBinding Background}"
BorderBrush="Transparent"
BorderThickness="0"
CornerRadius="14"
ClipToBounds="True"
IsVisible="{TemplateBinding ShowButtonSpinner}">
<Grid RowDefinitions="*,*">
<RepeatButton x:Name="PART_IncreaseButton"
Grid.Row="0"
Theme="{StaticResource SplitNumericRepeatButtonTheme}"
IsTabStop="{TemplateBinding IsTabStop}">
<TextBlock Classes="materialSymbol compact"
Text="keyboard_arrow_up" />
</RepeatButton>
<RepeatButton x:Name="PART_DecreaseButton"
Grid.Row="1"
Theme="{StaticResource SplitNumericRepeatButtonTheme}"
IsTabStop="{TemplateBinding IsTabStop}">
<TextBlock Classes="materialSymbol compact"
Text="keyboard_arrow_down" />
</RepeatButton>
<Border Height="1"
Grid.RowSpan="2"
Background="#14FFFFFF"
VerticalAlignment="Center"
IsHitTestVisible="False" />
</Grid>
<Border.Transitions>
<Transitions>
<BrushTransition Property="Background"
Duration="0:0:0.18"
Easing="CubicEaseOut" />
</Transitions>
</Border.Transitions>
</Border>
</Grid>
</ControlTemplate>
</Setter>
<Style Selector="^:pointerover /template/ Border#PART_ValueSurface,
^:focus-within /template/ Border#PART_ValueSurface">
<Setter Property="Background" Value="#20FFFFFF" />
</Style>
<Style Selector="^:pointerover /template/ Border#PART_SpinnerSurface,
^:focus-within /template/ Border#PART_SpinnerSurface">
<Setter Property="Background" Value="#20FFFFFF" />
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type settingsControls:SplitNumericUpDown}"
TargetType="settingsControls:SplitNumericUpDown">
<Setter Property="MinHeight" Value="44" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="Background" Value="#12FFFFFF" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Foreground" Value="#E8FFFFFF" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="TextAlignment" Value="Left" />
<Setter Property="Template">
<ControlTemplate>
<DataValidationErrors>
<ButtonSpinner x:Name="PART_Spinner"
Theme="{StaticResource SplitNumericSpinnerTheme}"
Background="{TemplateBinding Background}"
BorderBrush="Transparent"
BorderThickness="0"
IsTabStop="False"
Padding="0"
MinWidth="0"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
AllowSpin="{TemplateBinding AllowSpin}"
ShowButtonSpinner="{TemplateBinding ShowButtonSpinner}">
<TextBox x:Name="PART_TextBox"
Classes="splitNumericTextBox"
MinHeight="{TemplateBinding MinHeight}"
MinWidth="0"
Margin="0"
Padding="17,0"
Background="Transparent"
BorderBrush="Transparent"
BorderThickness="0"
Foreground="{TemplateBinding Foreground}"
PlaceholderText="{TemplateBinding PlaceholderText}"
PlaceholderForeground="{TemplateBinding PlaceholderForeground}"
IsReadOnly="{TemplateBinding IsReadOnly}"
VerticalContentAlignment="Center"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
Text="{TemplateBinding Text}"
TextAlignment="{TemplateBinding TextAlignment}"
AcceptsReturn="False"
TextWrapping="NoWrap"
InnerLeftContent="{Binding InnerLeftContent, RelativeSource={RelativeSource TemplatedParent}}"
InnerRightContent="{Binding InnerRightContent, RelativeSource={RelativeSource TemplatedParent}}" />
</ButtonSpinner>
</DataValidationErrors>
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>
-2
View File
@@ -6,8 +6,6 @@ Shared colors and brushes used throughout the launcher.
<ResourceDictionary xmlns="https://github.com/avaloniaui" <ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<FontFamily x:Key="MaterialSymbolsRounded">avares://SharpEmu.GUI/Assets/Fonts/MaterialSymbolsRounded-400.ttf#Material Symbols Rounded</FontFamily>
<Color x:Key="SystemAccentColor">#7C5CFC</Color> <Color x:Key="SystemAccentColor">#7C5CFC</Color>
<LinearGradientBrush x:Key="BgBrush" StartPoint="0%,0%" EndPoint="100%,100%"> <LinearGradientBrush x:Key="BgBrush" StartPoint="0%,0%" EndPoint="100%,100%">
@@ -1,9 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.GUI;
internal readonly record struct WindowMaximizeButtonState(
string Glyph,
string ToolTip,
string AutomationName);
+11 -281
View File
@@ -46,9 +46,6 @@ public static partial class AgcExports
private const uint ItNumInstances = 0x2F; private const uint ItNumInstances = 0x2F;
private const uint ItDrawIndexMultiAuto = 0x30; private const uint ItDrawIndexMultiAuto = 0x30;
private const uint ItDrawIndexOffset2 = 0x35; private const uint ItDrawIndexOffset2 = 0x35;
private const uint ItDrawIndexIndirectMulti = 0x38;
private const uint DrawIndexedIndirectArgsSize = 20;
private const uint DrawIndexedIndirectMaxScan = 1024;
private const uint ItWriteData = 0x37; private const uint ItWriteData = 0x37;
private const uint ItDispatchDirect = 0x15; private const uint ItDispatchDirect = 0x15;
private const uint ItDispatchIndirect = 0x16; private const uint ItDispatchIndirect = 0x16;
@@ -322,7 +319,6 @@ public static partial class AgcExports
private static int _tracedVertexRangeCount; private static int _tracedVertexRangeCount;
private static long _dcbWaitRegMemTraceCount; private static long _dcbWaitRegMemTraceCount;
private static long _createShaderTraceCount; private static long _createShaderTraceCount;
private static long _duplicateTargetTraceCount;
private static long _cbMetadataSkipTraceCount; private static long _cbMetadataSkipTraceCount;
private static long _packetPayloadTraceCount; private static long _packetPayloadTraceCount;
private static bool _tracedMissingPixelShaderBindings; private static bool _tracedMissingPixelShaderBindings;
@@ -1998,65 +1994,6 @@ public static partial class AgcExports
return ReturnPointer(ctx, drawCommand); return ReturnPointer(ctx, drawCommand);
} }
[SysAbiExport(
Nid = "1q1titRBL6o",
ExportName = "sceAgcDcbDrawIndirect",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DcbDrawIndirect(CpuContext ctx)
{
var commandBufferAddress = ctx[CpuRegister.Rdi];
var dataOffset = (uint)ctx[CpuRegister.Rsi];
var emit = Interlocked.Increment(ref _indirectDrawEmitCount);
if (emit <= 12 || emit % 250 == 0)
{
var rcx = ctx[CpuRegister.Rcx];
var dump = string.Empty;
for (var word = 0; word < 8; word++)
{
dump += TryReadUInt32(ctx, rcx + dataOffset + ((ulong)word * 4), out var raw)
? $" {raw}"
: " ?";
}
Console.Error.WriteLine(
$"[LOADER][WARN] agc.emit_indirect#{emit} buf=0x{commandBufferAddress:X16} " +
$"off=0x{dataOffset:X} rdx=0x{ctx[CpuRegister.Rdx]:X} rcx=0x{rcx:X} " +
$"r8=0x{ctx[CpuRegister.R8]:X} rcx_words:{dump}");
}
if (commandBufferAddress == 0)
{
Interlocked.Increment(ref _indirectDrawEmitRejectCount);
return ReturnPointer(ctx, 0);
}
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var drawCommand) ||
!TryWriteUInt32(ctx, drawCommand, Pm4(5, ItDrawIndirect, 0)) ||
!TryWriteUInt32(ctx, drawCommand + 4, dataOffset) ||
!TryWriteUInt32(ctx, drawCommand + 8, 0) ||
!TryWriteUInt32(ctx, drawCommand + 12, 0) ||
!TryWriteUInt32(ctx, drawCommand + 16, 0))
{
var rejects = Interlocked.Increment(ref _indirectDrawEmitRejectCount);
if (rejects <= 8 || rejects % 250 == 0)
{
Console.Error.WriteLine(
$"[LOADER][WARN] agc.emit_indirect_reject#{rejects} " +
$"buf=0x{commandBufferAddress:X16} reason=alloc_or_write");
}
return ReturnPointer(ctx, 0);
}
TraceAgc(
$"agc.dcb_draw_indirect buf=0x{commandBufferAddress:X16} " +
$"draw=0x{drawCommand:X16} offset=0x{dataOffset:X}");
return ReturnPointer(ctx, drawCommand);
}
[SysAbiExport( [SysAbiExport(
Nid = "Yw0jKSqop+E", Nid = "Yw0jKSqop+E",
ExportName = "sceAgcDcbDrawIndexAuto", ExportName = "sceAgcDcbDrawIndexAuto",
@@ -2115,39 +2052,6 @@ public static partial class AgcExports
return ReturnPointer(ctx, commandAddress); return ReturnPointer(ctx, commandAddress);
} }
[SysAbiExport(
Nid = "ypVBz4uPKcQ",
ExportName = "sceAgcDcbDrawIndexIndirectMulti",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DcbDrawIndexIndirectMulti(CpuContext ctx)
{
var commandBufferAddress = ctx[CpuRegister.Rdi];
var dataOffset = (uint)ctx[CpuRegister.Rsi];
var drawCount = (uint)ctx[CpuRegister.Rdx];
var stride = DrawIndexedIndirectArgsSize;
var modifier = (uint)ctx[CpuRegister.R8];
if (commandBufferAddress == 0 ||
!TryAllocateCommandDwords(ctx, commandBufferAddress, 8, out var commandAddress) ||
!TryWriteUInt32(ctx, commandAddress, Pm4(8, ItDrawIndexIndirectMulti, 0)) ||
!TryWriteUInt32(ctx, commandAddress + 4, dataOffset) ||
!TryWriteUInt32(ctx, commandAddress + 8, 0) ||
!TryWriteUInt32(ctx, commandAddress + 12, 0) ||
!TryWriteUInt32(ctx, commandAddress + 16, 0) ||
!TryWriteUInt32(ctx, commandAddress + 20, drawCount) ||
!TryWriteUInt32(ctx, commandAddress + 24, stride) ||
!TryWriteUInt32(ctx, commandAddress + 28, modifier))
{
return ReturnPointer(ctx, 0);
}
TraceAgc(
$"agc.dcb_draw_index_indirect_multi buf=0x{commandBufferAddress:X16} " +
$"cmd=0x{commandAddress:X16} offset=0x{dataOffset:X8} draws={drawCount} " +
$"stride={stride} modifier=0x{modifier:X8}");
return ReturnPointer(ctx, commandAddress);
}
[SysAbiExport( [SysAbiExport(
Nid = "mStuvI0zOtc", Nid = "mStuvI0zOtc",
ExportName = "sceAgcDcbDrawIndexIndirectGetSize", ExportName = "sceAgcDcbDrawIndexIndirectGetSize",
@@ -3908,7 +3812,6 @@ public static partial class AgcExports
if (TryReadSubmittedDrawCount( if (TryReadSubmittedDrawCount(
ctx, ctx,
gpuState,
state, state,
currentAddress, currentAddress,
length, length,
@@ -3932,8 +3835,7 @@ public static partial class AgcExports
var indexed = op is var indexed = op is
ItDrawIndex2 or ItDrawIndex2 or
ItDrawIndexOffset2 or ItDrawIndexOffset2 or
ItDrawIndexIndirect or ItDrawIndexIndirect;
ItDrawIndexIndirectMulti;
state.SawIndexedDraw |= indexed; state.SawIndexedDraw |= indexed;
TryTranslateGuestDraw(ctx, gpuState, state, indexCount, indexed); TryTranslateGuestDraw(ctx, gpuState, state, indexCount, indexed);
} }
@@ -4293,7 +4195,6 @@ public static partial class AgcExports
op is ItDispatchDirect or ItDispatchIndirect || op is ItDispatchDirect or ItDispatchIndirect ||
op is ItDrawIndirect or op is ItDrawIndirect or
ItDrawIndexIndirect or ItDrawIndexIndirect or
ItDrawIndexIndirectMulti or
ItDrawIndex2 or ItDrawIndex2 or
ItDrawIndexAuto or ItDrawIndexAuto or
ItDrawIndexMultiAuto or ItDrawIndexMultiAuto or
@@ -6263,24 +6164,8 @@ public static partial class AgcExports
} }
} }
private const int IndirectArgsFlushTimeoutMilliseconds = 250;
private static void FlushGpuWorkForIndirectArgs(SubmittedGpuState gpuState)
{
var pending = gpuState.WorkSequence;
if (pending == 0 || pending > long.MaxValue)
{
return;
}
GuestGpu.Current.WaitForGuestWork(
(long)pending,
IndirectArgsFlushTimeoutMilliseconds);
}
private static bool TryReadSubmittedDrawCount( private static bool TryReadSubmittedDrawCount(
CpuContext ctx, CpuContext ctx,
SubmittedGpuState gpuState,
SubmittedDcbState state, SubmittedDcbState state,
ulong packetAddress, ulong packetAddress,
uint packetLength, uint packetLength,
@@ -6311,89 +6196,17 @@ public static partial class AgcExports
drawCount = (control >> 21) & 0x7FFu; drawCount = (control >> 21) & 0x7FFu;
return true; return true;
case ItDrawIndexIndirectMulti when packetLength >= 8 && case ItDrawIndirect or ItDrawIndexIndirect
state.IndirectArgsAddress != 0: when packetLength >= 5 && state.IndirectArgsAddress != 0:
if (!TryReadUInt32(ctx, packetAddress + 4, out var multiOffset) || if (!TryReadUInt32(ctx, packetAddress + 4, out var dataOffset))
!TryReadUInt32(ctx, packetAddress + 20, out var multiDraws) ||
!TryReadUInt32(ctx, packetAddress + 24, out var multiStride))
{ {
return false; return false;
} }
return TryReadUInt32(
if (multiStride < DrawIndexedIndirectArgsSize) ctx,
{ state.IndirectArgsAddress + dataOffset,
multiStride = DrawIndexedIndirectArgsSize; out drawCount);
}
var multiTotal = 0UL;
var multiCapped = multiDraws == 0
? DrawIndexedIndirectMaxScan
: Math.Min(multiDraws, 4096u);
for (var draw = 0u; draw < multiCapped; draw++)
{
if (!TryReadUInt32(
ctx,
state.IndirectArgsAddress + multiOffset + ((ulong)draw * multiStride),
out var subCount))
{
break;
}
if (subCount == 0 && multiDraws == 0)
{
break;
}
multiTotal += subCount;
}
var multiProbe = Interlocked.Increment(ref _indirectMultiProbeCount);
if (multiProbe <= 12 || multiProbe % 250 == 0)
{
Console.Error.WriteLine(
$"[LOADER][WARN] agc.draw_multi#{multiProbe} args=0x{state.IndirectArgsAddress:X} " +
$"off=0x{multiOffset:X} draws={multiDraws} stride={multiStride} " +
$"total={multiTotal}");
}
drawCount = (uint)Math.Min(multiTotal, uint.MaxValue);
return drawCount != 0;
case ItDrawIndirect or ItDrawIndexIndirect:
var probe = Interlocked.Increment(ref _indirectDrawProbeCount);
if (!TryReadUInt32(ctx, packetAddress + 4, out var dataOffset))
{
dataOffset = 0xFFFFFFFFu;
}
var readable = packetLength >= 5 &&
state.IndirectArgsAddress != 0 &&
dataOffset != 0xFFFFFFFFu;
var resolved = readable &&
TryReadUInt32(
ctx,
state.IndirectArgsAddress + dataOffset,
out drawCount);
if (probe <= 12 || probe % 100 == 0)
{
var dump = string.Empty;
for (var word = 0; word < 8; word++)
{
dump += TryReadUInt32(
ctx,
state.IndirectArgsAddress + dataOffset + ((ulong)word * 4),
out var raw)
? $" {raw}"
: " ?";
}
Console.Error.WriteLine(
$"[LOADER][WARN] agc.draw_indirect#{probe} op=0x{op:X} len={packetLength} " +
$"args=0x{state.IndirectArgsAddress:X} off=0x{dataOffset:X} " +
$"resolved={resolved} count={drawCount} words:{dump}");
}
return resolved;
default: default:
return false; return false;
} }
@@ -8243,61 +8056,6 @@ public static partial class AgcExports
source.Format == destination.Format; source.Format == destination.Format;
} }
private static readonly HashSet<ulong> _renderTargetAddresses = new();
private static readonly HashSet<ulong> _sampledRenderTargets = new();
private static readonly object _renderTargetProbeGate = new();
private static long _renderTargetSampleTraceCount;
private static long _indirectDrawProbeCount;
private static long _indirectDrawEmitCount;
private static long _indirectDrawEmitRejectCount;
private static long _indirectMultiProbeCount;
private static void NoteRenderTargetAddress(ulong address)
{
if (address == 0)
{
return;
}
lock (_renderTargetProbeGate)
{
if (_renderTargetAddresses.Count < 512)
{
_renderTargetAddresses.Add(address);
}
}
}
private static void NoteSampledAddress(ulong address, uint format = 0, uint numberType = 0)
{
if (address == 0)
{
return;
}
bool firstTime;
int distinctTargets;
lock (_renderTargetProbeGate)
{
if (!_renderTargetAddresses.Contains(address))
{
return;
}
firstTime = _sampledRenderTargets.Add(address);
distinctTargets = _renderTargetAddresses.Count;
}
var count = Interlocked.Increment(ref _renderTargetSampleTraceCount);
if (firstTime || count % 2000 == 0)
{
var gpuResident = GuestGpu.Current.IsGpuGuestImageAvailable(address, format, numberType);
Console.Error.WriteLine(
$"[LOADER][WARN] agc.rt_sampled#{count} addr=0x{address:X} first={firstTime} " +
$"gpu_resident={gpuResident} fmt={format}/{numberType} known_targets={distinctTargets}");
}
}
private static IReadOnlyList<RenderTargetDescriptor> GetRenderTargets( private static IReadOnlyList<RenderTargetDescriptor> GetRenderTargets(
IReadOnlyDictionary<uint, uint> registers, IReadOnlyDictionary<uint, uint> registers,
bool includeMaskedTargets = false) bool includeMaskedTargets = false)
@@ -8324,13 +8082,6 @@ public static partial class AgcExports
continue; continue;
} }
if (targets.Exists(existing => existing.Address == address))
{
continue;
}
NoteRenderTargetAddress(address);
targets.Add(new RenderTargetDescriptor( targets.Add(new RenderTargetDescriptor(
slot, slot,
address, address,
@@ -8341,21 +8092,6 @@ public static partial class AgcExports
(attrib3 >> 14) & 0x1Fu)); (attrib3 >> 14) & 0x1Fu));
} }
if (targets.Count > 1 &&
targets.Select(t => t.Address).Distinct().Count() != targets.Count)
{
var dupCount = Interlocked.Increment(ref _duplicateTargetTraceCount);
if (dupCount <= 12 || dupCount % 500 == 0)
{
Console.Error.WriteLine(
$"[LOADER][WARN] agc.rt_duplicate#{dupCount} has_mask={hasTargetMask} " +
$"mask=0x{targetMask:X8} slots=[" +
string.Join(",", targets.Select(t =>
$"{t.Slot}:0x{t.Address:X}:m{(targetMask >> ((int)t.Slot * 4)) & 0xFu}")) +
"]");
}
}
return targets; return targets;
} }
@@ -9595,7 +9331,6 @@ public static partial class AgcExports
descriptor.Format, descriptor.Format,
descriptor.NumberType)) descriptor.NumberType))
{ {
NoteSampledAddress(descriptor.Address, descriptor.Format, descriptor.NumberType);
texture = new GuestDrawTexture( texture = new GuestDrawTexture(
descriptor.Address, descriptor.Address,
descriptor.Width, descriptor.Width,
@@ -9675,7 +9410,6 @@ public static partial class AgcExports
$"tile={descriptor.TileMode} mip={mipLevel}"); $"tile={descriptor.TileMode} mip={mipLevel}");
} }
NoteSampledAddress(descriptor.Address, descriptor.Format, descriptor.NumberType);
texture = new GuestDrawTexture( texture = new GuestDrawTexture(
descriptor.Address, descriptor.Address,
descriptor.Width, descriptor.Width,
@@ -9736,7 +9470,6 @@ public static partial class AgcExports
descriptor.Type, descriptor.Type,
textureDepth))) textureDepth)))
{ {
NoteSampledAddress(descriptor.Address, descriptor.Format, descriptor.NumberType);
texture = new GuestDrawTexture( texture = new GuestDrawTexture(
descriptor.Address, descriptor.Address,
descriptor.Width, descriptor.Width,
@@ -9799,8 +9532,7 @@ public static partial class AgcExports
if (readAllLayers) if (readAllLayers)
{ {
NoteSampledAddress(descriptor.Address, descriptor.Format, descriptor.NumberType); texture = new GuestDrawTexture(
texture = new GuestDrawTexture(
descriptor.Address, descriptor.Address,
descriptor.Width, descriptor.Width,
descriptor.Height, descriptor.Height,
@@ -9862,8 +9594,7 @@ public static partial class AgcExports
if (uploadedLayers == arrayLayers) if (uploadedLayers == arrayLayers)
{ {
NoteSampledAddress(descriptor.Address, descriptor.Format, descriptor.NumberType); texture = new GuestDrawTexture(
texture = new GuestDrawTexture(
descriptor.Address, descriptor.Address,
descriptor.Width, descriptor.Width,
descriptor.Height, descriptor.Height,
@@ -9966,8 +9697,7 @@ public static partial class AgcExports
if (IsGpuDetileEquation(gpuDetileParams.Equation) && if (IsGpuDetileEquation(gpuDetileParams.Equation) &&
(long)elementsWide * elementsHigh * bytesPerElement <= source.Length) (long)elementsWide * elementsHigh * bytesPerElement <= source.Length)
{ {
NoteSampledAddress(descriptor.Address, descriptor.Format, descriptor.NumberType); texture = new GuestDrawTexture(
texture = new GuestDrawTexture(
descriptor.Address, descriptor.Address,
descriptor.Width, descriptor.Width,
descriptor.Height, descriptor.Height,
-175
View File
@@ -1,175 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Buffers.Binary;
using System.Text;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Ime;
public static class ImeDialogExports
{
private const int StatusNone = 0;
private const int StatusRunning = 1;
private const int StatusFinished = 2;
private const int EndStatusOk = 0;
private const ulong ParamMaxTextLengthOffset = 0x24;
private const ulong ParamInputTextBufferOffset = 0x28;
private const int ImeDialogErrorInvalidAddress = unchecked((int)0x80BC0001);
private const string DefaultInputText = "Sharp";
private static readonly object _gate = new();
private static int _status = StatusNone;
[SysAbiExport(
Nid = "NUeBrN7hzf0",
ExportName = "sceImeDialogInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceImeDialog")]
public static int ImeDialogInit(CpuContext ctx)
{
var parameterAddress = ctx[CpuRegister.Rdi];
if (parameterAddress == 0)
{
return SetReturn(ctx, ImeDialogErrorInvalidAddress);
}
TryWriteInputText(ctx, parameterAddress);
lock (_gate)
{
_status = StatusFinished;
}
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "IADmD4tScBY",
ExportName = "sceImeDialogGetStatus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceImeDialog")]
public static int ImeDialogGetStatus(CpuContext ctx)
{
int status;
lock (_gate)
{
status = _status;
}
ctx[CpuRegister.Rax] = unchecked((ulong)(long)status);
return status;
}
[SysAbiExport(
Nid = "x01jxu+vxlc",
ExportName = "sceImeDialogGetResult",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceImeDialog")]
public static int ImeDialogGetResult(CpuContext ctx)
{
var resultAddress = ctx[CpuRegister.Rdi];
if (resultAddress == 0)
{
return SetReturn(ctx, ImeDialogErrorInvalidAddress);
}
Span<byte> result = stackalloc byte[8];
result.Clear();
BinaryPrimitives.WriteInt32LittleEndian(result, EndStatusOk);
ctx.Memory.TryWrite(resultAddress, result);
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "oBmw4xrmfKs",
ExportName = "sceImeDialogAbort",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceImeDialog")]
public static int ImeDialogAbort(CpuContext ctx)
{
lock (_gate)
{
_status = StatusFinished;
}
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "gyTyVn+bXMw",
ExportName = "sceImeDialogTerm",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceImeDialog")]
public static int ImeDialogTerm(CpuContext ctx)
{
lock (_gate)
{
_status = StatusNone;
}
return SetReturn(ctx, 0);
}
private static void TryWriteInputText(CpuContext ctx, ulong parameterAddress)
{
Span<byte> field = stackalloc byte[8];
if (!ctx.Memory.TryRead(parameterAddress + ParamInputTextBufferOffset, field))
{
return;
}
var bufferAddress = BinaryPrimitives.ReadUInt64LittleEndian(field);
if (bufferAddress == 0)
{
return;
}
var maxTextLength = 16u;
Span<byte> lengthField = stackalloc byte[4];
if (ctx.Memory.TryRead(parameterAddress + ParamMaxTextLengthOffset, lengthField))
{
var declared = BinaryPrimitives.ReadUInt32LittleEndian(lengthField);
if (declared is > 0 and <= 256)
{
maxTextLength = declared;
}
}
var text = Environment.GetEnvironmentVariable("SHARPEMU_DEFAULT_PROFILE");
if (string.IsNullOrEmpty(text))
{
text = DefaultInputText;
}
if ((uint)text.Length > maxTextLength)
{
text = text[..(int)maxTextLength];
}
var encoded = Encoding.Unicode.GetBytes(text);
Span<byte> payload = stackalloc byte[encoded.Length + 2];
encoded.CopyTo(payload);
payload[^2] = 0;
payload[^1] = 0;
if (ctx.Memory.TryWrite(bufferAddress, payload))
{
Console.Error.WriteLine(
$"[LOADER][WARN] ime.dialog_autofill buf=0x{bufferAddress:X16} " +
$"max={maxTextLength} text=\"{text}\"");
}
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(long)result);
return result;
}
}
@@ -33,17 +33,12 @@ public static class SaveDataDialogExports
LibraryName = "libSceSaveDataDialog")] LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogInitialize(CpuContext ctx) public static int SaveDataDialogInitialize(CpuContext ctx)
{ {
var previous = Interlocked.CompareExchange( if (Interlocked.CompareExchange(ref _status, StatusInitialized, StatusNone) != StatusNone)
ref _status,
StatusInitialized,
StatusNone);
if (previous == StatusNone || previous == StatusFinished)
{ {
Interlocked.Exchange(ref _status, StatusInitialized); return ctx.SetReturn(ErrorAlreadyInitialized);
return ctx.SetReturn(ErrorOk);
} }
return ctx.SetReturn(ErrorAlreadyInitialized); return ctx.SetReturn(ErrorOk);
} }
[SysAbiExport( [SysAbiExport(
@@ -1046,50 +1046,14 @@ internal static unsafe class VulkanVideoPresenter
targets.Count > 8 || targets.Count > 8 ||
AnyRenderTargetInvalid(targets)) AnyRenderTargetInvalid(targets))
{ {
var dropCount = Interlocked.Increment(ref _offscreenDropTraceCount);
if (dropCount <= 16 || dropCount % 500 == 0)
{
var detail = targets.Count == 0
? "<no targets>"
: string.Join(
" ",
targets.Select(t => $"0x{t.Address:X}:{t.Width}x{t.Height}"));
Console.Error.WriteLine(
$"[LOADER][WARN] vk.offscreen_drop#{dropCount} spirv={pixelSpirv.Length} " +
$"mrt={targets.Count} vs=0x{shaderAddress:X16} {detail}");
}
return; return;
} }
var firstTarget = targets[0]; var firstTarget = targets[0];
if (RenderTargetsMismatchedOrAliased(targets, firstTarget)) if (RenderTargetsMismatchedOrAliased(targets, firstTarget))
{ {
var skipCount = Interlocked.Increment(ref _mrtSkipTraceCount); Console.Error.WriteLine(
if (skipCount <= 16 || skipCount % 200 == 0) "[LOADER][WARN] Vulkan skipped MRT draw with mismatched dimensions or aliased targets.");
{
var aliased = false;
for (var i = 0; i < targets.Count && !aliased; i++)
{
for (var j = i + 1; j < targets.Count; j++)
{
if (targets[i].Address == targets[j].Address)
{
aliased = true;
break;
}
}
}
var detail = string.Join(
" ",
targets.Select(t =>
$"0x{t.Address:X}:{t.Width}x{t.Height}:f{t.Format}/{t.NumberType}"));
Console.Error.WriteLine(
$"[LOADER][WARN] vk.mrt_skip#{skipCount} mrt={targets.Count} " +
$"aliased={aliased} vs=0x{shaderAddress:X16} {detail}");
}
return; return;
} }
@@ -1334,8 +1298,6 @@ internal static unsafe class VulkanVideoPresenter
} }
} }
private static long _mrtSkipTraceCount;
private static long _offscreenDropTraceCount;
private static long _perfDrawCount; private static long _perfDrawCount;
private static long _perfDrawTicks; private static long _perfDrawTicks;
private static long _perfPipelineCreations; private static long _perfPipelineCreations;
@@ -1872,22 +1834,8 @@ internal static unsafe class VulkanVideoPresenter
lock (_gate) lock (_gate)
{ {
if (!_availableGuestImages.TryGetValue(address, out var availableFormat)) return _availableGuestImages.TryGetValue(address, out var availableFormat) &&
{ availableFormat == guestFormat;
return false;
}
if (availableFormat == guestFormat)
{
return true;
}
return TryDecodeRenderTargetFormat(
(availableFormat >> 8) & 0x1FFu,
availableFormat & 0xFFu,
out var resident) &&
TryDecodeRenderTargetFormat(format, numberType, out var requested) &&
Presenter.IsCompatibleViewFormat(resident.Format, requested.Format);
} }
} }
@@ -9271,27 +9219,6 @@ internal static unsafe class VulkanVideoPresenter
!_guestImages.ContainsKey(texture.Address)) !_guestImages.ContainsKey(texture.Address))
{ {
var guestFormat = GetGuestTextureFormat(texture.Format, texture.NumberType); var guestFormat = GetGuestTextureFormat(texture.Format, texture.NumberType);
var canonicalView = view;
if (texture.DstSelect != 0xFAC)
{
var identityViewInfo = new ImageViewCreateInfo
{
SType = StructureType.ImageViewCreateInfo,
Image = image,
ViewType = GetGuestTextureViewType(
texture.Type,
texture.ArrayedView),
Format = vkFormat,
Components = ToVkComponentMapping(0xFAC),
SubresourceRange = ColorSubresourceRange(layerCount: layers),
};
Check(
_vk.CreateImageView(_device, &identityViewInfo, null, out canonicalView),
"vkCreateImageView(texture identity)");
SetDebugName(ObjectType.ImageView, canonicalView.Handle, $"{debugName} identity view");
}
var guestImage = new GuestImageResource var guestImage = new GuestImageResource
{ {
Address = texture.Address, Address = texture.Address,
@@ -9307,7 +9234,7 @@ internal static unsafe class VulkanVideoPresenter
Format = vkFormat, Format = vkFormat,
Image = image, Image = image,
Memory = imageMemory, Memory = imageMemory,
View = canonicalView, View = view,
InitialUploadPending = true, InitialUploadPending = true,
IsCpuBacked = true, IsCpuBacked = true,
CpuContentFingerprint = contentFingerprint, CpuContentFingerprint = contentFingerprint,
@@ -11207,8 +11134,6 @@ internal static unsafe class VulkanVideoPresenter
EnsureGuestSubmissionCapacity(); EnsureGuestSubmissionCapacity();
resources = CreateComputeDispatchResources(work); resources = CreateComputeDispatchResources(work);
FlushBatchedGuestCommands();
var batchCount = Math.Max( var batchCount = Math.Max(
1u, 1u,
(uint)Math.Ceiling(work.GroupCountZ / (double)MaxComputeZSlicesPerSubmission)); (uint)Math.Ceiling(work.GroupCountZ / (double)MaxComputeZSlicesPerSubmission));
@@ -12150,7 +12075,6 @@ internal static unsafe class VulkanVideoPresenter
{ {
Console.Error.WriteLine( Console.Error.WriteLine(
$"[LOADER][WARN] Vulkan skipped storage render-target feedback loop " + $"[LOADER][WARN] Vulkan skipped storage render-target feedback loop " +
$"vs=0x{work.ShaderAddress:X16} " +
$"targets={string.Join(',', work.Targets.Where(target => target.Address != 0).Select(target => $"0x{target.Address:X16}"))}; " + $"targets={string.Join(',', work.Targets.Where(target => target.Address != 0).Select(target => $"0x{target.Address:X16}"))}; " +
"sampled aliases use ordered snapshots"); "sampled aliases use ordered snapshots");
ReturnPooledGuestData(work.Draw); ReturnPooledGuestData(work.Draw);
@@ -14361,6 +14285,11 @@ internal static unsafe class VulkanVideoPresenter
pendingGuestWork.Queue.Name, pendingGuestWork.Queue.Name,
StringComparison.Ordinal)) StringComparison.Ordinal))
{ {
// A host command buffer must never contain commands from
// two independent guest queues: an ordered action fences
// only its own queue's predecessor submissions.
// Keep the previous work label so a device-lost on this
// flush still names the draws that filled the batch.
FlushBatchedGuestCommands(); FlushBatchedGuestCommands();
} }
@@ -14400,7 +14329,14 @@ internal static unsafe class VulkanVideoPresenter
} }
try try
{ {
// A host-decoded movie only overrides which image gets
// presented (see the presentation selection below); the
// guest's own command stream keeps draining normally.
// Silently discarding these instead of executing them
// leaves guest-visible completion state (labels, buffers,
// job results) permanently unwritten, which desyncs the
// engine's own job system and previously crashed it
// shortly after the movie finished.
switch (work) switch (work)
{ {
case VulkanOffscreenGuestDraw offscreenDraw: case VulkanOffscreenGuestDraw offscreenDraw:
@@ -14722,8 +14658,6 @@ internal static unsafe class VulkanVideoPresenter
$"[LOADER][ERROR] Vulkan VideoOut translated draw setup failed: {exception.Message}"); $"[LOADER][ERROR] Vulkan VideoOut translated draw setup failed: {exception.Message}");
return; return;
} }
FlushBatchedGuestCommands();
} }
uint imageIndex; uint imageIndex;
@@ -15335,11 +15269,6 @@ internal static unsafe class VulkanVideoPresenter
RecordGuestDepthForSampling(depth, shaderStage); RecordGuestDepthForSampling(depth, shaderStage);
} }
if (!texture.IsStorage && texture.GuestImage is { } sampledGuestImage)
{
RecordGuestImageForSampling(sampledGuestImage, shaderStage);
}
if (!texture.NeedsUpload) if (!texture.NeedsUpload)
{ {
continue; continue;
@@ -15547,76 +15476,6 @@ internal static unsafe class VulkanVideoPresenter
depth.Layout = ImageLayout.ShaderReadOnlyOptimal; depth.Layout = ImageLayout.ShaderReadOnlyOptimal;
} }
private void RecordGuestImageForSampling(
GuestImageResource guestImage,
PipelineStageFlags shaderStage)
{
if (guestImage.Initialized || guestImage.InitialUploadPending)
{
return;
}
var range = ColorSubresourceRange(0, Math.Max(guestImage.MipLevels, 1));
var toTransfer = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
DstAccessMask = AccessFlags.TransferWriteBit,
OldLayout = ImageLayout.Undefined,
NewLayout = ImageLayout.TransferDstOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = guestImage.Image,
SubresourceRange = range,
};
_vk.CmdPipelineBarrier(
_commandBuffer,
PipelineStageFlags.TopOfPipeBit,
PipelineStageFlags.TransferBit,
0,
0,
null,
0,
null,
1,
&toTransfer);
var clearValue = new ClearColorValue(0f, 0f, 0f, 0f);
_vk.CmdClearColorImage(
_commandBuffer,
guestImage.Image,
ImageLayout.TransferDstOptimal,
&clearValue,
1,
&range);
var toShaderRead = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = AccessFlags.TransferWriteBit,
DstAccessMask = AccessFlags.ShaderReadBit,
OldLayout = ImageLayout.TransferDstOptimal,
NewLayout = ImageLayout.ShaderReadOnlyOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = guestImage.Image,
SubresourceRange = range,
};
_vk.CmdPipelineBarrier(
_commandBuffer,
PipelineStageFlags.TransferBit,
shaderStage,
0,
0,
null,
0,
null,
1,
&toShaderRead);
guestImage.Initialized = true;
}
private void RecordStandaloneGuestDepthClear(GuestDepthResource depth) private void RecordStandaloneGuestDepthClear(GuestDepthResource depth)
{ {
var depthRange = new ImageSubresourceRange( var depthRange = new ImageSubresourceRange(
@@ -95,67 +95,6 @@ public sealed class GuiSettingsTests
Assert.Equal(["SHARPEMU_TRACE"], settings.EnvironmentToggles); Assert.Equal(["SHARPEMU_TRACE"], settings.EnvironmentToggles);
} }
[Fact]
public void NormalizeFromJson_AllLauncherOptions_ArePreserved()
{
const string json = """
{
"LogLevel": "Debug",
"ImportTraceLimit": 96,
"StrictDynlibResolution": true,
"LogToFile": true,
"LogFilePath": "C:\\Logs\\sharpemu.log",
"OverrideLogFile": true,
"PlayTitleMusic": false,
"EmulatorPath": "C:\\SharpEmu\\SharpEmu.exe",
"Language": "ru",
"DefaultProfile": "Player",
"DiscordRichPresence": false,
"CheckForUpdatesOnStartup": false,
"WindowMode": "Borderless",
"Resolution": "2560x1440",
"DisplayIndex": 2,
"RefreshRate": 144,
"ScalingMode": "Integer",
"VSync": false,
"HdrMode": "On",
"EnvironmentToggles": [
"SHARPEMU_VK_VALIDATION",
"SHARPEMU_GUEST_IMAGE_CPU_SYNC"
],
"RenderResolutionScale": 0.5,
"DiscordClientId": "999"
}
""";
var settings = GuiSettings.NormalizeFromJson(json);
Assert.Equal("Debug", settings.LogLevel);
Assert.Equal(96, settings.ImportTraceLimit);
Assert.True(settings.StrictDynlibResolution);
Assert.True(settings.LogToFile);
Assert.Equal("C:\\Logs\\sharpemu.log", settings.LogFilePath);
Assert.True(settings.OverrideLogFile);
Assert.False(settings.PlayTitleMusic);
Assert.Equal("C:\\SharpEmu\\SharpEmu.exe", settings.EmulatorPath);
Assert.Equal("ru", settings.Language);
Assert.Equal("Player", settings.DefaultProfile);
Assert.False(settings.DiscordRichPresence);
Assert.False(settings.CheckForUpdatesOnStartup);
Assert.Equal("Borderless", settings.WindowMode);
Assert.Equal("2560x1440", settings.Resolution);
Assert.Equal(2, settings.DisplayIndex);
Assert.Equal(144, settings.RefreshRate);
Assert.Equal("Integer", settings.ScalingMode);
Assert.False(settings.VSync);
Assert.Equal("On", settings.HdrMode);
Assert.Equal(
["SHARPEMU_VK_VALIDATION", "SHARPEMU_GUEST_IMAGE_CPU_SYNC"],
settings.EnvironmentToggles);
Assert.Equal(0.5, settings.RenderResolutionScale);
Assert.Equal("999", settings.DiscordClientId);
}
// An empty Discord client ID intentionally disables Rich Presence. // An empty Discord client ID intentionally disables Rich Presence.
[Fact] [Fact]
public void NormalizeFromJson_EmptyDiscordClientId_IsPreservedNotNormalized() public void NormalizeFromJson_EmptyDiscordClientId_IsPreservedNotNormalized()
@@ -1,52 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.GUI;
using System.Collections;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using Xunit;
namespace SharpEmu.Libs.Tests.GUI;
public sealed class LibraryTileCollectionTests
{
[Fact]
public void KeepsAddFolderTileAfterVisibleGames()
{
var games = new ObservableCollection<GameEntry>();
var tiles = new LibraryTileCollection(games);
var first = CreateGame("First");
var second = CreateGame("Second");
games.Add(first);
games.Add(second);
Assert.Equal(3, tiles.Count);
Assert.True(tiles is IList { IsReadOnly: true });
Assert.Same(first, tiles[0]);
Assert.Same(second, tiles[1]);
Assert.Same(AddFolderTile.Instance, tiles[2]);
}
[Fact]
public void ForwardsGameChangesAtTheirOriginalIndices()
{
var games = new ObservableCollection<GameEntry>();
var tiles = new LibraryTileCollection(games);
NotifyCollectionChangedEventArgs? change = null;
tiles.CollectionChanged += (_, args) => change = args;
var game = CreateGame("Game");
games.Add(game);
Assert.NotNull(change);
Assert.Equal(NotifyCollectionChangedAction.Add, change.Action);
Assert.Equal(0, change.NewStartingIndex);
Assert.Same(game, Assert.Single(change.NewItems!.Cast<GameEntry>()));
Assert.Same(AddFolderTile.Instance, tiles[1]);
}
private static GameEntry CreateGame(string name) =>
new(name, null, null, $"/games/{name}/eboot.bin", 0, null, null);
}
@@ -1,27 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using Avalonia.Controls;
using SharpEmu.GUI;
using Xunit;
namespace SharpEmu.Libs.Tests.GUI;
public sealed class WindowChromeTests
{
[Theory]
[InlineData(WindowState.Normal, "crop_square", "Maximize", "Maximize window")]
[InlineData(WindowState.Maximized, "filter_none", "Restore", "Restore window")]
public void GetMaximizeButtonState_ReturnsConsistentVisualAndAccessibleState(
WindowState windowState,
string expectedGlyph,
string expectedToolTip,
string expectedAutomationName)
{
var state = MainWindow.GetMaximizeButtonState(windowState);
Assert.Equal(expectedGlyph, state.Glyph);
Assert.Equal(expectedToolTip, state.ToolTip);
Assert.Equal(expectedAutomationName, state.AutomationName);
}
}