mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-30 22:49:53 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39d7ccf2f7 | |||
| c2cee59fda | |||
| 0324d5561b | |||
| 1aef9a0e6e | |||
| 1cfc0239b8 | |||
| d5108e854d | |||
| b020f1676a | |||
| 02938b5d5b | |||
| 7b7a48a834 |
@@ -231,6 +231,11 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Download build artifacts
|
- name: Download build artifacts
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
@@ -258,6 +263,61 @@ jobs:
|
|||||||
tar -czf "release-assets/sharpemu-${VERSION}-linux-x64.tar.gz" -C "${linux_dir}" .
|
tar -czf "release-assets/sharpemu-${VERSION}-linux-x64.tar.gz" -C "${linux_dir}" .
|
||||||
tar -czf "release-assets/sharpemu-${VERSION}-osx-x64.tar.gz" -C "${macos_dir}" .
|
tar -czf "release-assets/sharpemu-${VERSION}-osx-x64.tar.gz" -C "${macos_dir}" .
|
||||||
|
|
||||||
|
- name: Build release notes
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
MAX_COMMITS: 200
|
||||||
|
RELEASE_TAG: ${{ needs.init.outputs.release-tag }}
|
||||||
|
REPO_URL: ${{ github.server_url }}/${{ github.repository }}
|
||||||
|
SHORT_SHA: ${{ needs.init.outputs.short-sha }}
|
||||||
|
VERSION: ${{ needs.init.outputs.version }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
previous_tag="$(git describe --tags --abbrev=0 --match 'v*' "${RELEASE_TAG}^" 2>/dev/null || true)"
|
||||||
|
|
||||||
|
if [ -n "${previous_tag}" ]; then
|
||||||
|
range="${previous_tag}..${RELEASE_TAG}"
|
||||||
|
else
|
||||||
|
range="${RELEASE_TAG}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
git log --no-merges --reverse --pretty=tformat:"%H%x1f%h%x1f%s" "${range}" > commits.txt
|
||||||
|
total="$(wc -l < commits.txt)"
|
||||||
|
|
||||||
|
{
|
||||||
|
printf 'Automated SharpEmu v%s build for commit [`%s`](%s/commit/%s).\n\n' \
|
||||||
|
"${VERSION}" "${SHORT_SHA}" "${REPO_URL}" "${GITHUB_SHA}"
|
||||||
|
|
||||||
|
if [ -n "${previous_tag}" ]; then
|
||||||
|
printf '## Changes since %s\n\n' "${previous_tag}"
|
||||||
|
else
|
||||||
|
printf '## Changes\n\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${total}" -eq 0 ]; then
|
||||||
|
printf '_No commits since %s._\n' "${previous_tag}"
|
||||||
|
else
|
||||||
|
head -n "${MAX_COMMITS}" commits.txt | while IFS=$'\x1f' read -r sha short subject; do
|
||||||
|
printf -- '- %s ([`%s`](%s/commit/%s))\n' "${subject}" "${short}" "${REPO_URL}" "${sha}"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${total}" -gt "${MAX_COMMITS}" ]; then
|
||||||
|
printf '\n_…and %s more commits._\n' "$((total - MAX_COMMITS))"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
if [ -n "${previous_tag}" ]; then
|
||||||
|
printf '**Full changelog**: %s/compare/%s...%s\n' "${REPO_URL}" "${previous_tag}" "${RELEASE_TAG}"
|
||||||
|
else
|
||||||
|
printf '**Full changelog**: %s/commits/%s\n' "${REPO_URL}" "${RELEASE_TAG}"
|
||||||
|
fi
|
||||||
|
} > release-notes.md
|
||||||
|
|
||||||
|
cat release-notes.md
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
@@ -265,7 +325,6 @@ jobs:
|
|||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
RELEASE_NAME: ${{ needs.init.outputs.release-name }}
|
RELEASE_NAME: ${{ needs.init.outputs.release-name }}
|
||||||
RELEASE_TAG: ${{ needs.init.outputs.release-tag }}
|
RELEASE_TAG: ${{ needs.init.outputs.release-tag }}
|
||||||
VERSION: ${{ needs.init.outputs.version }}
|
|
||||||
run: |
|
run: |
|
||||||
mapfile -t assets < <(find release-assets -maxdepth 1 -type f \( -name '*.zip' -o -name '*.tar.gz' \) | sort)
|
mapfile -t assets < <(find release-assets -maxdepth 1 -type f \( -name '*.zip' -o -name '*.tar.gz' \) | sort)
|
||||||
if [ "${#assets[@]}" -ne 3 ]; then
|
if [ "${#assets[@]}" -ne 3 ]; then
|
||||||
@@ -273,8 +332,6 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
notes="Automated SharpEmu v${VERSION} build for commit ${GITHUB_SHA}."
|
|
||||||
|
|
||||||
if gh release view "${RELEASE_TAG}" >/dev/null 2>&1; then
|
if gh release view "${RELEASE_TAG}" >/dev/null 2>&1; then
|
||||||
echo "Release ${RELEASE_TAG} already exists and will not be modified." >&2
|
echo "Release ${RELEASE_TAG} already exists and will not be modified." >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -283,4 +340,4 @@ jobs:
|
|||||||
gh release create "${RELEASE_TAG}" "${assets[@]}" \
|
gh release create "${RELEASE_TAG}" "${assets[@]}" \
|
||||||
--verify-tag \
|
--verify-tag \
|
||||||
--title "${RELEASE_NAME}" \
|
--title "${RELEASE_NAME}" \
|
||||||
--notes "${notes}"
|
--notes-file release-notes.md
|
||||||
|
|||||||
@@ -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-1</SharpEmuVersion>
|
<SharpEmuVersion>0.0.3-hotfix-2</SharpEmuVersion>
|
||||||
<Version>$(SharpEmuVersion)</Version>
|
<Version>$(SharpEmuVersion)</Version>
|
||||||
|
|
||||||
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
|
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
|
||||||
|
|||||||
@@ -157,6 +157,62 @@ def ensure_tag_does_not_exist(
|
|||||||
raise ReleaseError(f"Tag {tag} already exists on {remote}.")
|
raise ReleaseError(f"Tag {tag} already exists on {remote}.")
|
||||||
|
|
||||||
|
|
||||||
|
def get_previous_tag(repository_root: Path) -> str:
|
||||||
|
try:
|
||||||
|
return run_git(
|
||||||
|
"describe",
|
||||||
|
"--tags",
|
||||||
|
"--abbrev=0",
|
||||||
|
"--match",
|
||||||
|
"v*",
|
||||||
|
"HEAD",
|
||||||
|
cwd=repository_root,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
except ReleaseError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def get_release_commits(
|
||||||
|
repository_root: Path,
|
||||||
|
previous_tag: str,
|
||||||
|
) -> list[str]:
|
||||||
|
revision_range = f"{previous_tag}..HEAD" if previous_tag else "HEAD"
|
||||||
|
|
||||||
|
output = run_git(
|
||||||
|
"log",
|
||||||
|
"--no-merges",
|
||||||
|
"--reverse",
|
||||||
|
"--pretty=tformat:%h %s",
|
||||||
|
revision_range,
|
||||||
|
cwd=repository_root,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return output.splitlines()
|
||||||
|
|
||||||
|
|
||||||
|
def print_release_notes_preview(repository_root: Path) -> None:
|
||||||
|
previous_tag = get_previous_tag(repository_root)
|
||||||
|
commits = get_release_commits(repository_root, previous_tag)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
if previous_tag:
|
||||||
|
print(f"Commits since {previous_tag} ({len(commits)}):")
|
||||||
|
else:
|
||||||
|
print(f"Commits in this release ({len(commits)}):")
|
||||||
|
|
||||||
|
if not commits:
|
||||||
|
print(" (none)")
|
||||||
|
|
||||||
|
for commit in commits:
|
||||||
|
print(f" {commit}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("These commits go into the generated release notes.")
|
||||||
|
|
||||||
|
|
||||||
def read_version(props_path: Path) -> str:
|
def read_version(props_path: Path) -> str:
|
||||||
if not props_path.exists():
|
if not props_path.exists():
|
||||||
raise ReleaseError(f"Version file not found: {props_path}")
|
raise ReleaseError(f"Version file not found: {props_path}")
|
||||||
@@ -323,6 +379,8 @@ def create_release_tag(
|
|||||||
remote,
|
remote,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
print_release_notes_preview(repository_root)
|
||||||
|
|
||||||
run_git(
|
run_git(
|
||||||
"tag",
|
"tag",
|
||||||
"-a",
|
"-a",
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ 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;
|
||||||
|
|
||||||
@@ -112,6 +115,18 @@ 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)
|
||||||
{
|
{
|
||||||
@@ -152,6 +167,12 @@ 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
|
||||||
|
|||||||
@@ -66,6 +66,8 @@
|
|||||||
|
|
||||||
"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": "إيقاف",
|
||||||
@@ -153,7 +155,7 @@
|
|||||||
"About.Discord.Label": "دسكورد",
|
"About.Discord.Label": "دسكورد",
|
||||||
"About.Discord.Desc": "انضم إلى المجتمع واحصل على الدعم وتابع التطوير.",
|
"About.Discord.Desc": "انضم إلى المجتمع واحصل على الدعم وتابع التطوير.",
|
||||||
"About.GithubButton": "ساهم على GitHub!",
|
"About.GithubButton": "ساهم على GitHub!",
|
||||||
"About.DiscordButton": "انضم إلى دسكوردنا!",
|
"About.DiscordComingSoon": "قريبًا",
|
||||||
"Updater.Auto.Label": "التحقق من التحديثات عند بدء التشغيل",
|
"Updater.Auto.Label": "التحقق من التحديثات عند بدء التشغيل",
|
||||||
"Updater.Auto.Desc": "يتحقق من GitHub دون تأخير بدء التشغيل.",
|
"Updater.Auto.Desc": "يتحقق من GitHub دون تأخير بدء التشغيل.",
|
||||||
"Updater.Label": "التحديثات",
|
"Updater.Label": "التحديثات",
|
||||||
|
|||||||
@@ -76,6 +76,8 @@
|
|||||||
|
|
||||||
"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",
|
||||||
@@ -142,7 +144,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.DiscordButton": "Entre no nosso Discord!",
|
"About.DiscordComingSoon": "Em breve",
|
||||||
|
|
||||||
"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.",
|
||||||
|
|||||||
@@ -66,6 +66,8 @@
|
|||||||
|
|
||||||
"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",
|
||||||
@@ -153,7 +155,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.DiscordButton": "Tritt unserem Discord bei!",
|
"About.DiscordComingSoon": "Demnächst verfügbar",
|
||||||
"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",
|
||||||
|
|||||||
@@ -66,6 +66,8 @@
|
|||||||
|
|
||||||
"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",
|
||||||
@@ -153,7 +155,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.DiscordButton": "Bliv medlem af vores Discord!",
|
"About.DiscordComingSoon": "Kommer snart",
|
||||||
"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",
|
||||||
|
|||||||
@@ -38,6 +38,8 @@
|
|||||||
"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",
|
||||||
@@ -186,7 +188,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.DiscordButton": "Join our Discord!",
|
"About.DiscordComingSoon": "Coming soon",
|
||||||
|
|
||||||
"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.",
|
||||||
|
|||||||
@@ -66,6 +66,8 @@
|
|||||||
|
|
||||||
"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",
|
||||||
@@ -134,7 +136,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.DiscordButton": "Únete a nuestro Discord!",
|
"About.DiscordComingSoon": "Próximamente",
|
||||||
|
|
||||||
"Library.Context.GameSettings": "Ajustes del juego…",
|
"Library.Context.GameSettings": "Ajustes del juego…",
|
||||||
"Options.Env.Tab": "Entorno",
|
"Options.Env.Tab": "Entorno",
|
||||||
|
|||||||
@@ -76,6 +76,8 @@
|
|||||||
|
|
||||||
"Options.Language.Label": "Langue de l’émulateur",
|
"Options.Language.Label": "Langue de l’émulateur",
|
||||||
"Options.Language.Desc": "Langue utilisée dans l’ensemble du lanceur. Le changement est immédiat.",
|
"Options.Language.Desc": "Langue utilisée dans l’ensemble du lanceur. Le changement est immédiat.",
|
||||||
|
"Options.DefaultProfile.Label": "Nom de profil par défaut",
|
||||||
|
"Options.DefaultProfile.Desc": "Nom utilisé lorsqu’un 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é",
|
||||||
@@ -142,7 +144,7 @@
|
|||||||
"About.Discord.Label": "Discord",
|
"About.Discord.Label": "Discord",
|
||||||
"About.Discord.Desc": "Rejoignez la communauté, obtenez de l’aide et suivez le développement.",
|
"About.Discord.Desc": "Rejoignez la communauté, obtenez de l’aide et suivez le développement.",
|
||||||
"About.GithubButton": "Contribuer sur GitHub !",
|
"About.GithubButton": "Contribuer sur GitHub !",
|
||||||
"About.DiscordButton": "Rejoindre notre Discord !",
|
"About.DiscordComingSoon": "Bientôt disponible",
|
||||||
|
|
||||||
"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 d’installation.\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 d’installation.\nNécessaire pour les dumps non empaquetés qui écrivent leurs sauvegardes ou leur configuration sous /app0.",
|
||||||
|
|||||||
@@ -76,6 +76,8 @@
|
|||||||
|
|
||||||
"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",
|
||||||
@@ -142,7 +144,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.DiscordButton": "Csatlakozz a Discordunhoz!",
|
"About.DiscordComingSoon": "Hamarosan",
|
||||||
|
|
||||||
"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.",
|
||||||
|
|||||||
@@ -69,6 +69,8 @@
|
|||||||
|
|
||||||
"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",
|
||||||
@@ -158,7 +160,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.DiscordButton": "Unisciti al nostro Discord!",
|
"About.DiscordComingSoon": "Prossimamente",
|
||||||
"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",
|
||||||
|
|||||||
@@ -66,6 +66,8 @@
|
|||||||
|
|
||||||
"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": "オフ",
|
||||||
@@ -153,7 +155,7 @@
|
|||||||
"About.Discord.Label": "Discord",
|
"About.Discord.Label": "Discord",
|
||||||
"About.Discord.Desc": "コミュニティに参加して、サポートを受けたり開発を追いかけたりしましょう。",
|
"About.Discord.Desc": "コミュニティに参加して、サポートを受けたり開発を追いかけたりしましょう。",
|
||||||
"About.GithubButton": "GitHubで貢献しよう!",
|
"About.GithubButton": "GitHubで貢献しよう!",
|
||||||
"About.DiscordButton": "Discordに参加しよう!",
|
"About.DiscordComingSoon": "近日公開",
|
||||||
"Updater.Auto.Label": "起動時にアップデートを確認",
|
"Updater.Auto.Label": "起動時にアップデートを確認",
|
||||||
"Updater.Auto.Desc": "起動を遅らせずにGitHubへ確認します。",
|
"Updater.Auto.Desc": "起動を遅らせずにGitHubへ確認します。",
|
||||||
"Updater.Label": "アップデート",
|
"Updater.Label": "アップデート",
|
||||||
|
|||||||
@@ -66,6 +66,8 @@
|
|||||||
|
|
||||||
"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": "끔",
|
||||||
@@ -153,7 +155,7 @@
|
|||||||
"About.Discord.Label": "디스코드",
|
"About.Discord.Label": "디스코드",
|
||||||
"About.Discord.Desc": "커뮤니티에 참여해 지원을 받고 개발 소식을 확인하세요.",
|
"About.Discord.Desc": "커뮤니티에 참여해 지원을 받고 개발 소식을 확인하세요.",
|
||||||
"About.GithubButton": "GitHub에서 기여하기!",
|
"About.GithubButton": "GitHub에서 기여하기!",
|
||||||
"About.DiscordButton": "디스코드 참여하기!",
|
"About.DiscordComingSoon": "곧 공개",
|
||||||
"Updater.Auto.Label": "시작 시 업데이트 확인",
|
"Updater.Auto.Label": "시작 시 업데이트 확인",
|
||||||
"Updater.Auto.Desc": "시작을 지연시키지 않고 GitHub를 확인합니다.",
|
"Updater.Auto.Desc": "시작을 지연시키지 않고 GitHub를 확인합니다.",
|
||||||
"Updater.Label": "업데이트",
|
"Updater.Label": "업데이트",
|
||||||
|
|||||||
@@ -66,6 +66,8 @@
|
|||||||
|
|
||||||
"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",
|
||||||
@@ -153,7 +155,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.DiscordButton": "Word lid van onze Discord!",
|
"About.DiscordComingSoon": "Binnenkort",
|
||||||
"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",
|
||||||
|
|||||||
@@ -76,6 +76,8 @@
|
|||||||
|
|
||||||
"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",
|
||||||
@@ -142,7 +144,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.DiscordButton": "Junte-se ao nosso Discord!",
|
"About.DiscordComingSoon": "Em breve",
|
||||||
|
|
||||||
"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.",
|
||||||
|
|||||||
@@ -109,6 +109,8 @@
|
|||||||
|
|
||||||
"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": "Выключено",
|
||||||
@@ -186,7 +188,7 @@
|
|||||||
"About.Discord.Label": "Discord",
|
"About.Discord.Label": "Discord",
|
||||||
"About.Discord.Desc": "Присоединяйтесь к сообществу, получайте поддержку и следите за разработкой.",
|
"About.Discord.Desc": "Присоединяйтесь к сообществу, получайте поддержку и следите за разработкой.",
|
||||||
"About.GithubButton": "Участвовать в разработке на GitHub!",
|
"About.GithubButton": "Участвовать в разработке на GitHub!",
|
||||||
"About.DiscordButton": "Присоединиться к нашему Discord!",
|
"About.DiscordComingSoon": "Скоро",
|
||||||
|
|
||||||
"Updater.Auto.Label": "Проверять обновления при запуске",
|
"Updater.Auto.Label": "Проверять обновления при запуске",
|
||||||
"Updater.Auto.Desc": "Проверяет GitHub без задержки запуска.",
|
"Updater.Auto.Desc": "Проверяет GitHub без задержки запуска.",
|
||||||
|
|||||||
@@ -173,6 +173,8 @@
|
|||||||
"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})",
|
||||||
@@ -189,7 +191,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.DiscordButton": "Discord'umuza katıl!",
|
"About.DiscordComingSoon": "Yakında",
|
||||||
"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.",
|
||||||
|
|||||||
@@ -346,6 +346,15 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</ComboBox>
|
</ComboBox>
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
|
|
||||||
|
<local:SettingRow x:Name="DefaultProfileRow"
|
||||||
|
Label="{Binding [Options.DefaultProfile.Label], Source={x:Static local:Localization.Instance}}"
|
||||||
|
Description="{Binding [Options.DefaultProfile.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
|
<TextBox x:Name="DefaultProfileBox"
|
||||||
|
Width="180"
|
||||||
|
MaxLength="16"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</local:SettingRow>
|
||||||
|
|
||||||
<local:SettingRow x:Name="TitleMusicRow"
|
<local:SettingRow x:Name="TitleMusicRow"
|
||||||
Label="{Binding [Options.TitleMusic.Label], Source={x:Static local:Localization.Instance}}"
|
Label="{Binding [Options.TitleMusic.Label], Source={x:Static local:Localization.Instance}}"
|
||||||
Description="{Binding [Options.TitleMusic.Desc], Source={x:Static local:Localization.Instance}}">
|
Description="{Binding [Options.TitleMusic.Desc], Source={x:Static local:Localization.Instance}}">
|
||||||
@@ -473,11 +482,12 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Button Grid.Column="1"
|
<TextBlock Grid.Column="1"
|
||||||
x:Name="DiscordButton"
|
x:Name="DiscordComingSoonText"
|
||||||
Classes="ghost"
|
Text="{Binding [About.DiscordComingSoon], Source={x:Static local:Localization.Instance}}"
|
||||||
Content="{Binding [About.DiscordButton], Source={x:Static local:Localization.Instance}}"
|
FontSize="12"
|
||||||
VerticalAlignment="Center" />
|
Foreground="{StaticResource MutedBrush}"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
@@ -645,6 +655,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
OffContent="{Binding [Common.Off], Source={x:Static local:Localization.Instance}}"
|
OffContent="{Binding [Common.Off], Source={x:Static local:Localization.Instance}}"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
</local:SettingRow>
|
</local:SettingRow>
|
||||||
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
|||||||
@@ -298,6 +298,8 @@ 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);
|
||||||
@@ -332,15 +334,6 @@ 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))
|
||||||
@@ -892,6 +885,7 @@ 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");
|
||||||
@@ -1118,6 +1112,8 @@ 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
|
||||||
@@ -2015,10 +2011,20 @@ 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(
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ 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;
|
||||||
@@ -319,6 +322,7 @@ 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;
|
||||||
@@ -1994,6 +1998,65 @@ 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",
|
||||||
@@ -2052,6 +2115,39 @@ 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",
|
||||||
@@ -3812,6 +3908,7 @@ public static partial class AgcExports
|
|||||||
|
|
||||||
if (TryReadSubmittedDrawCount(
|
if (TryReadSubmittedDrawCount(
|
||||||
ctx,
|
ctx,
|
||||||
|
gpuState,
|
||||||
state,
|
state,
|
||||||
currentAddress,
|
currentAddress,
|
||||||
length,
|
length,
|
||||||
@@ -3835,7 +3932,8 @@ public static partial class AgcExports
|
|||||||
var indexed = op is
|
var indexed = op is
|
||||||
ItDrawIndex2 or
|
ItDrawIndex2 or
|
||||||
ItDrawIndexOffset2 or
|
ItDrawIndexOffset2 or
|
||||||
ItDrawIndexIndirect;
|
ItDrawIndexIndirect or
|
||||||
|
ItDrawIndexIndirectMulti;
|
||||||
state.SawIndexedDraw |= indexed;
|
state.SawIndexedDraw |= indexed;
|
||||||
TryTranslateGuestDraw(ctx, gpuState, state, indexCount, indexed);
|
TryTranslateGuestDraw(ctx, gpuState, state, indexCount, indexed);
|
||||||
}
|
}
|
||||||
@@ -4195,6 +4293,7 @@ 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
|
||||||
@@ -6164,8 +6263,24 @@ 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,
|
||||||
@@ -6196,17 +6311,89 @@ public static partial class AgcExports
|
|||||||
|
|
||||||
drawCount = (control >> 21) & 0x7FFu;
|
drawCount = (control >> 21) & 0x7FFu;
|
||||||
return true;
|
return true;
|
||||||
case ItDrawIndirect or ItDrawIndexIndirect
|
case ItDrawIndexIndirectMulti when packetLength >= 8 &&
|
||||||
when packetLength >= 5 && state.IndirectArgsAddress != 0:
|
state.IndirectArgsAddress != 0:
|
||||||
if (!TryReadUInt32(ctx, packetAddress + 4, out var dataOffset))
|
if (!TryReadUInt32(ctx, packetAddress + 4, out var multiOffset) ||
|
||||||
|
!TryReadUInt32(ctx, packetAddress + 20, out var multiDraws) ||
|
||||||
|
!TryReadUInt32(ctx, packetAddress + 24, out var multiStride))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return TryReadUInt32(
|
|
||||||
ctx,
|
if (multiStride < DrawIndexedIndirectArgsSize)
|
||||||
state.IndirectArgsAddress + dataOffset,
|
{
|
||||||
out drawCount);
|
multiStride = DrawIndexedIndirectArgsSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
@@ -8056,6 +8243,61 @@ 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)
|
||||||
@@ -8082,6 +8324,13 @@ 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,
|
||||||
@@ -8092,6 +8341,21 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9331,6 +9595,7 @@ 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,
|
||||||
@@ -9410,6 +9675,7 @@ 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,
|
||||||
@@ -9470,6 +9736,7 @@ 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,
|
||||||
@@ -9532,7 +9799,8 @@ public static partial class AgcExports
|
|||||||
|
|
||||||
if (readAllLayers)
|
if (readAllLayers)
|
||||||
{
|
{
|
||||||
texture = new GuestDrawTexture(
|
NoteSampledAddress(descriptor.Address, descriptor.Format, descriptor.NumberType);
|
||||||
|
texture = new GuestDrawTexture(
|
||||||
descriptor.Address,
|
descriptor.Address,
|
||||||
descriptor.Width,
|
descriptor.Width,
|
||||||
descriptor.Height,
|
descriptor.Height,
|
||||||
@@ -9594,7 +9862,8 @@ public static partial class AgcExports
|
|||||||
|
|
||||||
if (uploadedLayers == arrayLayers)
|
if (uploadedLayers == arrayLayers)
|
||||||
{
|
{
|
||||||
texture = new GuestDrawTexture(
|
NoteSampledAddress(descriptor.Address, descriptor.Format, descriptor.NumberType);
|
||||||
|
texture = new GuestDrawTexture(
|
||||||
descriptor.Address,
|
descriptor.Address,
|
||||||
descriptor.Width,
|
descriptor.Width,
|
||||||
descriptor.Height,
|
descriptor.Height,
|
||||||
@@ -9697,7 +9966,8 @@ 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)
|
||||||
{
|
{
|
||||||
texture = new GuestDrawTexture(
|
NoteSampledAddress(descriptor.Address, descriptor.Format, descriptor.NumberType);
|
||||||
|
texture = new GuestDrawTexture(
|
||||||
descriptor.Address,
|
descriptor.Address,
|
||||||
descriptor.Width,
|
descriptor.Width,
|
||||||
descriptor.Height,
|
descriptor.Height,
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
// 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,12 +33,17 @@ public static class SaveDataDialogExports
|
|||||||
LibraryName = "libSceSaveDataDialog")]
|
LibraryName = "libSceSaveDataDialog")]
|
||||||
public static int SaveDataDialogInitialize(CpuContext ctx)
|
public static int SaveDataDialogInitialize(CpuContext ctx)
|
||||||
{
|
{
|
||||||
if (Interlocked.CompareExchange(ref _status, StatusInitialized, StatusNone) != StatusNone)
|
var previous = Interlocked.CompareExchange(
|
||||||
|
ref _status,
|
||||||
|
StatusInitialized,
|
||||||
|
StatusNone);
|
||||||
|
if (previous == StatusNone || previous == StatusFinished)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(ErrorAlreadyInitialized);
|
Interlocked.Exchange(ref _status, StatusInitialized);
|
||||||
|
return ctx.SetReturn(ErrorOk);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.SetReturn(ErrorOk);
|
return ctx.SetReturn(ErrorAlreadyInitialized);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
|
|||||||
@@ -1046,14 +1046,50 @@ 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))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
var skipCount = Interlocked.Increment(ref _mrtSkipTraceCount);
|
||||||
"[LOADER][WARN] Vulkan skipped MRT draw with mismatched dimensions or aliased targets.");
|
if (skipCount <= 16 || skipCount % 200 == 0)
|
||||||
|
{
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1298,6 +1334,8 @@ 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;
|
||||||
@@ -1834,8 +1872,22 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
return _availableGuestImages.TryGetValue(address, out var availableFormat) &&
|
if (!_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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12075,6 +12127,7 @@ 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);
|
||||||
|
|||||||
Reference in New Issue
Block a user