Compare commits

..

1 Commits

Author SHA1 Message Date
ParantezTech 3f857bef20 chore: bump version to 0.0.3-release.3 2026-08-18 00:19:11 +03:00
28 changed files with 306 additions and 2423 deletions
@@ -8,7 +8,6 @@ using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using Iced.Intel;
using SharpEmu.Core.Cpu;
using SharpEmu.Core.Cpu.Debugging;
using SharpEmu.Core.Loader;
@@ -1639,15 +1638,18 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
if (_moduleManager.TryGetExport(nid, out ExportedFunction export))
{
var preferLleForLibc = IsLibcLibrary(export.LibraryName) && PreferLleForLibcExport(export.Name);
if (!ShouldResolveRegisteredExportViaLle(export, preferLleForLibc))
if (IsKernelLibrary(export.LibraryName))
{
if (_logAllImports && IsKernelLibrary(export.LibraryName))
if (_logAllImports)
{
Console.Error.WriteLine($"[LOADER][DEBUG] TryResolveDirectImportTarget: {nid} ({export.LibraryName}:{export.Name}) -> HLE (kernel library)");
}
return false;
}
if (!IsLibcLibrary(export.LibraryName) || !PreferLleForLibcExport(export.Name))
{
return false;
}
if (TryResolveRuntimeSymbolAddress(nid, out var value2) && IsDirectImportTargetUsable(value2))
{
targetAddress = value2;
@@ -1701,14 +1703,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return false;
}
internal static bool ShouldResolveRegisteredExportViaLle(
ExportedFunction export,
bool preferLleForLibc)
{
ArgumentNullException.ThrowIfNull(export);
return !IsKernelLibrary(export.LibraryName) && (export.PreferLle || preferLleForLibc);
}
private static bool IsHlePreferredNid(string nid)
{
return string.Equals(nid, "QrZZdJ8XsX0", StringComparison.Ordinal) ||
@@ -3206,7 +3200,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
{
nint address = (nint)(ptr + i);
int remainingBytes = scanBytes - i;
if (TryPatchTlsLoadInstruction(address, ptr + i, remainingBytes, i))
if (TryPatchTlsLoadInstruction(address, ptr + i, remainingBytes))
{
num3++;
}
@@ -3349,19 +3343,13 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return true;
}
private unsafe bool TryPatchTlsLoadInstruction(nint address, byte* source, int availableLength, int regionOffset)
private unsafe bool TryPatchTlsLoadInstruction(nint address, byte* source, int availableLength)
{
if (availableLength < MinTlsPatchInstructionBytes)
{
return false;
}
var region = new ReadOnlySpan<byte>(source - regionOffset, regionOffset + availableLength);
if (IsTlsLoadCandidateInsideShortJump(region, regionOffset))
{
return false;
}
var offset = 0;
while (offset < availableLength && source[offset] == 0x66)
{
@@ -3414,76 +3402,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return PatchTlsLoadInstruction(address, instructionLength, destinationRegister);
}
internal static bool IsTlsLoadCandidateInsideShortJump(ReadOnlySpan<byte> region, int candidateOffset)
{
if ((uint)candidateOffset >= (uint)region.Length ||
candidateOffset < 1 ||
region[candidateOffset - 1] != 0xEB)
{
return false;
}
// Accept EB when it is an aligned rel8 operand.
if (IsRel8ControlFlowInstructionEndingAtCandidate(region, candidateOffset))
{
return false;
}
return true;
}
private static bool IsRel8ControlFlowInstructionEndingAtCandidate(
ReadOnlySpan<byte> region,
int candidateOffset)
{
if (candidateOffset < 2)
{
return false;
}
var branchOffset = candidateOffset - 2;
var opcode = region[branchOffset];
if (!((opcode >= 0x70 && opcode <= 0x7F) ||
opcode is >= 0xE0 and <= 0xE3 ||
opcode == 0xEB))
{
return false;
}
var branchTarget = candidateOffset + (sbyte)region[candidateOffset - 1];
if (branchTarget < 0 || branchTarget >= branchOffset)
{
return false;
}
// Require an aligned instruction stream.
var decoder = Decoder.Create(
64,
new ByteArrayCodeReader(region[branchTarget..candidateOffset].ToArray()));
decoder.IP = (ulong)branchTarget;
while (decoder.IP < (ulong)candidateOffset)
{
var instructionOffset = (int)decoder.IP;
decoder.Decode(out var instruction);
if (instruction.Code == Code.INVALID || instruction.Length <= 0)
{
return false;
}
if (instructionOffset == branchOffset)
{
return instruction.Length == 2 && decoder.IP == (ulong)candidateOffset;
}
if (decoder.IP > (ulong)branchOffset)
{
return false;
}
}
return false;
}
private unsafe bool PatchTlsLoadInstruction(nint address, int instructionLength, int destinationRegister)
{
uint flNewProtect = default(uint);
@@ -1,7 +1,6 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using SharpEmu.Core.Loader;
using SharpEmu.HLE;
@@ -19,7 +18,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
private readonly object _allocationSearchHintGate = new();
private readonly List<MemoryRegion> _regions = new();
private readonly Dictionary<(ulong DesiredAddress, ulong Alignment, bool Executable), ulong> _allocationSearchHints = new();
private readonly ConcurrentDictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
private bool _disposed;
[ThreadStatic]
@@ -29,13 +28,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
private const ulong PageSize = 0x1000;
private const ulong HostAllocationGranularity = 0x10000;
private const ulong GuestAllocationArenaAddress = 0x00006000_0000_0000;
// Full C++ runtimes can route large numbers of HLE-backed heap allocations
// through this arena. The original 16 MiB capacity could be exhausted
// during asset setup, turning a valid allocation request into a null
// pointer that failed later in unrelated guest code. Keep enough capacity
// for those workloads. Adapted from foufouadi's allocator-exhaustion
// investigation.
private const ulong GuestAllocationArenaSize = 0x2000_0000;
private const ulong GuestAllocationArenaSize = 0x0100_0000;
private const ulong GuestAllocationArenaStartOffset = PageSize;
private const ulong LargeDataReserveThreshold = 0x4000_0000UL; // 1 GiB
private const ulong FullCommitRegionLimit = 4UL << 30;
-213
View File
@@ -1,213 +0,0 @@
{
"_languageName": "Eesti",
"Page.Library": "Teek",
"Page.Options": "Seaded",
"Page.GameCount.One": "1 mäng",
"Page.GameCount.Other": "{0} mängu",
"Library.SearchWatermark": "Otsi teegist…",
"Library.AddFolder": "Lisa kaust",
"Library.OpenFile": "Ava fail…",
"Library.View.Grid": "Ruudustikvaade",
"Library.View.Carousel": "Karussellvaade",
"Library.Context.Launch": "Käivita",
"Library.Context.OpenFolder": "Ava mängukaust",
"Library.Context.CopyPath": "Kopeeri tee",
"Library.Context.CopyTitleId": "Kopeeri Title ID",
"Library.Context.GameSettings": "Mängu seaded…",
"Library.Context.Remove": "Eemalda teegist",
"Library.Empty.Title": "Sinu teek on tühi",
"Library.Empty.Hint": "Alustamiseks lisa kaust, kus sinu mängud asuvad.",
"Library.Empty.SearchTitle": "Otsingule ei vasta ükski mäng",
"Library.Empty.SearchHint": "Ükski teegis olev mäng ei vasta otsingule “{0}”.",
"Library.Empty.AddFolder": " Lisa mängukaust",
"Library.Loading": "Teeki laaditakse…",
"Library.Stat.Version": "Versioon",
"Library.Stat.Installed": "Paigaldatud",
"Library.Stat.TitleId": "Title ID",
"Common.Back": "Tagasi",
"Options.General": "Üldine",
"Options.Logging": "Logimine",
"Options.Env.Tab": "Keskkond",
"Options.Section.Environment": "KESKKONNAMUUTUJAD",
"Options.Env.Desc": "Lülitid, mis edastatakse emulaatorile käivitamisel keskkonnamuutujatena.",
"Options.Env.Bthid.Desc": "Märgi Bluetooth HID kättesaamatuks mängude puhul, mille rooli/FFB-vahetarkvara küsib andmeid lõputult.\nTavaliselt jäta välja lülitatuks. Mõni mäng hangub, kui initsialiseerimine ebaõnnestub.",
"Options.Env.LoopGuard.Desc": "Ära sulge sundkorras mänge, mis kordavad sama kutset liiga kaua.\nProovi seda, kui mäng laadimise ajal iseenesest sulgub.",
"Options.Env.WritableApp0.Desc": "Luba mängudel luua ja kirjutada faile oma paigalduskausta.\nVajalik pakendamata koopiatele, mis salvestavad oma andmed või seadistused kausta /app0.",
"Options.Env.VkValidation.Desc": "Luba Vulkani valideerimiskihid GPU silumiseks.\nAeglane. Nõuab paigaldatud Vulkan SDK-d.",
"Options.Env.DumpSpirv.Desc": "Salvesta AGC-shaderid ja nende SPIR-V tõlked kausta shader-dumps.\nKasuta shaderi- või kuvamisvea teatamisel.",
"Options.Env.LogDirectMemory.Desc": "Logi konsooli otsese mälu eraldamised ja tõrked.\nKasuta siis, kui mäng katkeb või sulgub käivitamise ajal.",
"Options.Env.LogIo.Desc": "Logi konsooli failide avamise, lugemise ja tee lahendamise tegevused.\nKasuta siis, kui mäng ei leia käivitamisel oma andmefaile.",
"Options.Env.LogNp.Desc": "Logi konsooli NP (PlayStation Network) teegi kutsed.",
"Options.Env.Group.Debug": "Silumine",
"Options.Env.Group.General": "Üldine",
"Options.Env.RenderDoc.Desc": "Laadib RenderDoc'i rakendusesisese API, et kaadreid saaks hõivata otse emulaatorist.\nVajuta mängu käigus F10 ühe kaadri hõivamiseks; hõivatised salvestatakse kausta user/logs/capture_logs/<TITLE_ID>.\nNõuab paigaldatud RenderDoc'i. Aeglustab GPU-d ja põhjustab mõningate mängude hangumist, seega hoia välja lülitatuna, kui sa ei silu.",
"Options.Env.GuestImageCpuSync.Desc": "Laadi uuesti üles mängupinnad, mida selle enda CPU-kood ümber kirjutab.\nTavaliselt jäta välja lülitatuks. Lülita sisse mängude puhul, mille CPU-ga joonistatud pinnad ei jõua kunagi ekraanile.\nVähendab jõudlust ja halvendab mõne mängu töökindlust (nt GTA V).",
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Edasta GPU käsupuhvrite preambulid isegi siis, kui nende sihtjärjekord neid kunagi ei võta.\nTavaliselt jäta välja lülitatuks. Lülita sisse mängude puhul, mis hanguvad oodates GPU-fence'i, mis kunagi ei rakendu.",
"Options.DefaultProfile.Label": "Vaikimisi profiili nimi",
"Options.DefaultProfile.Desc": "Nimi, mida kasutatakse, kui mäng küsib tekstisisestust. Vaikimisi Sharp.",
"Options.Section.Emulation": "EMULATSIOON",
"Options.Section.Logging": "LOGIMINE",
"Options.Section.Launcher": "KÄIVITI",
"Options.Section.Rendering": "RENDERDAMINE",
"Options.Section.Display": "KUVA",
"Options.Graphics": "Graafika",
"Options.RenderResolution.Label": "Sisemine eraldusvõime",
"Options.RenderResolution.Desc": "Renderdab ekraanivälised sihid natiivsest madalamal eraldusvõimel ja skaleerib kuvamisel üles. Madalamad väärtused annavad pildikvaliteedi arvelt GPU-le jõudlusvaru; rakendub järgmisel käivitamisel.",
"Options.RenderResolution.Native": "100% (natiivne)",
"Options.WindowMode.Label": "Aknarežiim",
"Options.WindowMode.Desc": "Tavaline aken, äärteta töölauavaade või eksklusiivne täisekraan.",
"Options.WindowMode.Windowed": "Aken",
"Options.WindowMode.Borderless": "Äärteta",
"Options.WindowMode.Exclusive": "Eksklusiivne",
"Options.Resolution.Label": "Eraldusvõime",
"Options.Resolution.Desc": "Algne akna suurus või eksklusiivse täisekraani eraldusvõime.",
"Options.Display.Label": "Kuvar",
"Options.Display.Desc": "Monitor, mida kasutatakse tsentreerimiseks ja täisekraanrežiimiks.",
"Options.RefreshRate.Label": "Värskendussagedus",
"Options.RefreshRate.Desc": "Eksklusiivse täisekraani värskendussagedus. Automaatne valib lähima režiimi.",
"Options.RefreshRate.Automatic": "Automaatne",
"Options.Scaling.Label": "Skaleerimine",
"Options.Scaling.Desc": "Skaleeri algset mängupilti muutmata selle sisemist eraldusvõimet.",
"Options.Scaling.Fit": "Mahuta",
"Options.Scaling.Cover": "Kata",
"Options.Scaling.Stretch": "Venita",
"Options.Scaling.Integer": "Täisarvuline",
"Options.VSync.Label": "VSync",
"Options.VSync.Desc": "Kasuta FIFO-esitust pildi rebimise vältimiseks.",
"Options.Hdr.Label": "HDR-väljund",
"Options.Hdr.Desc": "Kasuta HDR-i, kui valitud kuvar ja graafikaliides seda toetavad. Automaatrežiim lülitub tagasi SDR-ile.",
"Options.Hdr.Auto": "Automaatne",
"Options.CpuEngine.Label": "CPU-mootor",
"Options.CpuEngine.Desc": "Käitusmootor, mida kasutatakse mängukoodi käivitamiseks.",
"Options.CpuEngine.Native": "Natiivne",
"Options.Strict.Label": "Range dynlib-lahendamine",
"Options.Strict.Desc": "Katkesta käivitamine, kui imporditud sümbolit ei õnnestu lahendada.",
"Options.LogLevel.Label": "Logimise tase",
"Options.LogLevel.Desc": "Emulaatori konsooliväljundi üksikasjalikkus.",
"Options.LogLevel.Trace": "Trace (Jälitus)",
"Options.LogLevel.Debug": "Debug (Silumine)",
"Options.LogLevel.Info": "Info",
"Options.LogLevel.Warning": "Hoiatus",
"Options.LogLevel.Error": "Viga",
"Options.LogLevel.Critical": "Kriitiline",
"Options.TraceImports.Label": "Impordi jälituse piir",
"Options.TraceImports.Desc": "Jälita iga mooduli esimesi N importi (0 = väljas).",
"Options.LogToFile.Label": "Logi faili",
"Options.LogToFile.Desc": "Kopeeri emulaatori väljund logifaili.",
"Options.LogFilePath.Label": "Logifaili tee",
"Options.LogFilePath.Default": "Kohandatud tee puudub — logid salvestatakse emulaatori kõrvale kausta user/logs.",
"Options.LogFilePath.Select": "Vali…",
"Options.OverrideLogFile.Label": "Kirjuta logifail üle",
"Options.OverrideLogFile.Desc": "Kasuta täpset failiteed selle asemel, et lisada Title ID ja ajatempel.",
"Options.TitleMusic.Label": "Mängu muusika",
"Options.TitleMusic.Desc": "Esita valitud mängu eelvaatemuusikat teegis korduvalt.",
"Options.Discord.Label": "Discordi olek",
"Options.Discord.Desc": "Näita käimasolevat mängu oma Discordi profiilis.",
"Options.Language.Label": "Emulaatori keel",
"Options.Language.Desc": "Käivitis kasutatav keel. Rakendub kohe.",
"Common.On": "Sisse",
"Common.Off": "Välja",
"Common.Save": "Salvesta",
"Common.Cancel": "Tühista",
"Console.Title": "KONSOOL",
"Console.SearchWatermark": "Otsi…",
"Console.AutoScroll": "Automaatne kerimine",
"Console.Split": "Poolita",
"Console.Copy": "Kopeeri",
"Console.Clear": "Tühjenda",
"Console.WindowTitle": "SharpEmu konsool",
"Launch.NoGameSelected": "Mäng pole valitud",
"Launch.NoGameHint": "Vali mäng teegist või ava eboot.bin fail otse.",
"Launch.Idle": "Ootel",
"Launch.Console": "≡ Konsool",
"Launch.Launch": "▶ Käivita",
"Launch.Stop": "■ Peata",
"Launch.Running": "Käib — {0}",
"Launch.Stopping": "Peatamine…",
"Launch.Exited": "Väljus koodiga {0} ({1})",
"Launch.ExeNotFound": "SharpEmu käivitavat faili ei leitud. Ehita esmalt SharpEmu.CLI projekt (dotnet build).",
"Launch.LogFile": "Logifail: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Emulaatori käivitamine nurjus: {0}",
"Launch.ProcessExited": "Protsess väljus koodiga {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "vigased argumendid",
"Exit.EbootNotFound": "eboot-faili ei leitud",
"Exit.RuntimeException": "käitusaegne erand",
"Exit.EmulationError": "emulatsioonitõrge",
"Exit.Unknown": "tundmatu",
"Status.EmulatorLocating": "Emulaator: otsimine…",
"Status.EmulatorPath": "Emulaator: {0}",
"Status.EmulatorNotFound": "Emulaator: SharpEmu käivitavat faili ei leitud — ehita esmalt SharpEmu.CLI.",
"Status.ScanningLibrary": "Teegi skaneerimine…",
"Status.AddFolderPrompt": "Lisa mängukaust teegi täitmiseks.",
"Status.LibraryScanned": "Teek skaneeritud: {0} mängu {1} kaustas.",
"Status.CouldNotOpenFolder": "Kausta ei saanud avada: {0}",
"Status.CopiedToClipboard": "{0} kopeeritud lõikelauale.",
"Status.RemovedFromLibrary": "Eemaldati “{0}” teegist. Taastamiseks lisa selle kaust uuesti.",
"Status.Running": "Käib: {0}",
"Status.Stopping": "Peatamine…",
"Status.Idle": "Ootel",
"Clipboard.Path": "Asukoht",
"Clipboard.TitleId": "Title ID",
"Discord.Playing": "Mängib: {0}",
"Discord.Browsing": "Sirvib teeki",
"Dialog.ChooseGameFolder": "Vali mänge sisaldav kaust",
"Dialog.OpenExecutable": "Ava käivitatav fail käivitamiseks",
"Dialog.PsExecutables": "PS käivitusfailid",
"Dialog.SaveLogFile": "Vali logifaili salvestamiskoht",
"Dialog.PlainTextFiles": "Tavatekstifailid",
"Dialog.LogFiles": "Logifailid",
"Options.About": "Teave",
"About.Github.Label": "GitHub",
"About.Github.Desc": "Lähtekood, vearaportid ja projekti arendus.",
"About.Github.LatestCommitLabel": "Viimane muudatus (commit)",
"About.Github.LatestCommitDescription": "Viimane muudatus peaharul",
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Liitu kogukonnaga, saa tuge ja jälgi arendust.",
"About.GithubButton": "Löö GitHubis kaasa!",
"About.DiscordComingSoon": "Tulekul",
"Updater.Auto.Label": "Kontrolli värskendusi käivitamisel",
"Updater.Auto.Desc": "Kontrollib GitHubist värskendusi käivitamist viivitamata.",
"Updater.Label": "Värskendused",
"Updater.Check": "Kontrolli värskendusi",
"Updater.DownloadRestart": "Laadi alla ja taaskäivita",
"Updater.Status.Ready": "Praegune versioon: {0}",
"Updater.Status.Checking": "Värskenduste kontrollimine…",
"Updater.Status.Current": "Sinu versioon on ajakohane ({0}).",
"Updater.Status.Available": "Uus versioon on saadaval: {0}",
"Updater.Status.Downloading": "Värskenduse allalaadimine… {0}%",
"Updater.Status.Installing": "Värskenduse paigaldamine…",
"Updater.Status.Timeout": "Värskenduste kontroll aegus 10 sekundi järel.",
"Updater.Status.Failed": "Värskendusi ei saanud kontrollida.",
"Updater.Status.ChecksumFailed": "Allalaaditud värskendus ei läbinud SHA-256 kontrolli.",
"Updater.Status.Unsupported": "Automaatne värskendamine nõuab Windowsi, Linuxi või macOS-i x64 versiooni."
}
-213
View File
@@ -1,213 +0,0 @@
{
"_languageName": "Polski",
"Page.Library": "Biblioteka",
"Page.Options": "Opcje",
"Page.GameCount.One": "1 gra",
"Page.GameCount.Other": "{0} gier",
"Library.SearchWatermark": "Szukaj w bibliotece…",
"Library.AddFolder": "Dodaj folder",
"Library.OpenFile": "Otwórz plik…",
"Library.View.Grid": "Widok siatki",
"Library.View.Carousel": "Widok listy",
"Library.Context.Launch": "Uruchom",
"Library.Context.OpenFolder": "Otwórz folder gry",
"Library.Context.CopyPath": "Kopiuj ścieżkę",
"Library.Context.CopyTitleId": "Kopiuj numer seryjny",
"Library.Context.GameSettings": "Ustawienia gry…",
"Library.Context.Remove": "Usuń z biblioteki",
"Library.Empty.Title": "Twoja biblioteka jest pusta",
"Library.Empty.Hint": "Dodaj folder zawierający twoje gry, aby rozpocząć.",
"Library.Empty.SearchTitle": "Brak gier pasujących do twojego wyszukiwania",
"Library.Empty.SearchHint": "Nic w bibliotece nie pasuje do “{0}”.",
"Library.Empty.AddFolder": " Dodaj folder z grami",
"Library.Loading": "Ładowanie biblioteki…",
"Library.Stat.Version": "Wersja",
"Library.Stat.Installed": "Zainstalowano",
"Library.Stat.TitleId": "Numer seryjny",
"Common.Back": "Wstecz",
"Options.General": "Ogólne",
"Options.Logging": "Dziennik zdarzeń",
"Options.Env.Tab": "Środowisko",
"Options.Section.Environment": "ZMIENNE ŚRODOWISKOWE",
"Options.Env.Desc": "Przełączniki przekazywane emulatorowi jako zmienne środowiskowe przy uruchomieniu.",
"Options.Env.Bthid.Desc": "Zgłaszaj Bluetooth HID jako niedostępny dla tytułów, których middleware kierownicy/FFB odpytuje w nieskończoność.\nZostaw wyłączone normalnie. Niektóre tytuły zawieszają się, gdy inicjalizacja zawiedzie.",
"Options.Env.LoopGuard.Desc": "Nie wymuszaj zamknięcia tytułów, które powtarzają to samo wywołanie zbyt długo.\nSpróbuj tego, gdy gra zamyka się sama podczas ładowania.",
"Options.Env.WritableApp0.Desc": "Pozwól tytułom tworzyć i zapisywać pliki wewnątrz folderu instalacji.\nPotrzebne w przypadku niespakowanych zrzutów, które zapisują dane zapisu lub konfiguracji w /app0.",
"Options.Env.VkValidation.Desc": "Włącz warstwy walidacji Vulkan do debugowania GPU.\nWolne. Wymaga zainstalowanego Vulkan SDK.",
"Options.Env.DumpSpirv.Desc": "Zrzuć shadery AGC i ich translacje SPIR-V do folderu shader-dumps.\nUżyj przy zgłaszaniu błędów shaderów lub renderowania.",
"Options.Env.LogDirectMemory.Desc": "Loguj alokacje pamięci bezpośredniej i błędy do konsoli.\nUżyj, gdy gra przerywa działanie lub zamyka się podczas uruchamiania.",
"Options.Env.LogIo.Desc": "Loguj otwieranie plików, odczyt i rozwiązywanie ścieżek do konsoli.\nUżyj, gdy gra nie może znaleźć swoich plików danych podczas uruchamiania.",
"Options.Env.LogNp.Desc": "Loguj wywołania biblioteki NP (PlayStation Network) do konsoli.",
"Options.Env.Group.Debug": "Debugowanie",
"Options.Env.Group.General": "Ogólne",
"Options.Env.RenderDoc.Desc": "Załaduj API RenderDoc w aplikacji, aby można było przechwytywać klatki z wnętrza emulatora.\nNaciśnij F10 podczas działania gry, aby przechwycić jedną klatkę; przechwycone klatki trafiają do user/logs/capture_logs/<TITLE_ID>.\nWymaga zainstalowanego RenderDoc. Spowalnia GPU i zawiesza niektóre tytuły, więc zostaw wyłączone, chyba że debugujesz.",
"Options.Env.GuestImageCpuSync.Desc": "Ponownie przesyłaj powierzchnie gościa, które procesor gry nadpisuje.\nZostaw wyłączone normalnie. Włącz dla tytułów, których powierzchnie rysowane przez CPU nigdy nie docierają na ekran.\nKosztuje wydajność i pogarsza działanie niektórych tytułów, takich jak GTA V.",
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Dostarczaj preambuły buforów poleceń GPU nawet gdy ich docelowa kolejka ich nie odbiera.\nZostaw wyłączone normalnie. Włącz dla tytułów, które zawieszają się w oczekiwaniu na sygnał z GPU.",
"Options.DefaultProfile.Label": "Domyślna nazwa profilu",
"Options.DefaultProfile.Desc": "Nazwa używana, gdy gra prosi o wprowadzenie tekstu. Domyślnie Sharp.",
"Options.Section.Emulation": "EMULACJA",
"Options.Section.Logging": "DZIENNIK ZDARZEŃ",
"Options.Section.Launcher": "LAUNCHER",
"Options.Section.Rendering": "RENDEROWANIE",
"Options.Section.Display": "WYŚWIETLANIE",
"Options.Graphics": "Grafika",
"Options.RenderResolution.Label": "Rozdzielczość wewnętrzna",
"Options.RenderResolution.Desc": "Renderuj cele pozaekranowe poniżej natywnej rozdzielczości i skaluj w górę przy prezentacji. Niższe wartości zamieniają jakość obrazu na zapas GPU; działa od następnego uruchomienia.",
"Options.RenderResolution.Native": "100% (natywna)",
"Options.WindowMode.Label": "Tryb okna",
"Options.WindowMode.Desc": "Zwykłe okno, bezramkowe na pulpicie lub pełny ekran wyłączny.",
"Options.WindowMode.Windowed": "W oknie",
"Options.WindowMode.Borderless": "Bezramkowe",
"Options.WindowMode.Exclusive": "Wyłączne",
"Options.Resolution.Label": "Rozdzielczość",
"Options.Resolution.Desc": "Początkowy rozmiar okna lub rozdzielczość pełnego ekranu wyłącznego.",
"Options.Display.Label": "Wyświetlacz",
"Options.Display.Desc": "Monitor używany do centrowania i trybu pełnoekranowego.",
"Options.RefreshRate.Label": "Częstotliwość odświeżania",
"Options.RefreshRate.Desc": "Częstotliwość odświeżania w trybie pełnego ekranu wyłącznego. Automatyczna wybiera najbliższy tryb.",
"Options.RefreshRate.Automatic": "Automatyczna",
"Options.Scaling.Label": "Skalowanie",
"Options.Scaling.Desc": "Skaluj natywny obraz gościa bez zmiany jego wewnętrznej rozdzielczości.",
"Options.Scaling.Fit": "Dopasuj",
"Options.Scaling.Cover": "Wypełnij",
"Options.Scaling.Stretch": "Rozciągnij",
"Options.Scaling.Integer": "Całkowitoliczbowe",
"Options.VSync.Label": "VSync",
"Options.VSync.Desc": "Używaj prezentacji FIFO dla obrazu bez rozrywania.",
"Options.Hdr.Label": "Wyjście HDR",
"Options.Hdr.Desc": "Używaj HDR, gdy wybrany wyświetlacz i backend graficzny to obsługują. Automatyczny przełącza na SDR.",
"Options.Hdr.Auto": "Automatyczny",
"Options.CpuEngine.Label": "Silnik CPU",
"Options.CpuEngine.Desc": "Silnik wykonawczy używany do uruchamiania kodu gry.",
"Options.CpuEngine.Native": "Natywny",
"Options.Strict.Label": "Ścisłe rozwiązywanie dynlib",
"Options.Strict.Desc": "Przerwij uruchomienie, gdy importowany symbol nie może zostać rozwiązany.",
"Options.LogLevel.Label": "Poziom logowania",
"Options.LogLevel.Desc": "Szczegółowość wyjścia konsoli emulatora.",
"Options.LogLevel.Trace": "Śledzenie",
"Options.LogLevel.Debug": "Debugowanie",
"Options.LogLevel.Info": "Informacja",
"Options.LogLevel.Warning": "Ostrzeżenie",
"Options.LogLevel.Error": "Błąd",
"Options.LogLevel.Critical": "Krytyczny",
"Options.TraceImports.Label": "Limit śledzenia importów",
"Options.TraceImports.Desc": "Śledź pierwszych N importów na moduł (0 = wyłączone).",
"Options.LogToFile.Label": "Loguj do pliku",
"Options.LogToFile.Desc": "Kopiuj wyjście emulatora do pliku logu.",
"Options.LogFilePath.Label": "Ścieżka pliku logu",
"Options.LogFilePath.Default": "Brak niestandardowej ścieżki — logi trafiają do user/logs obok emulatora.",
"Options.LogFilePath.Select": "Wybierz…",
"Options.OverrideLogFile.Label": "Nadpisz plik logu",
"Options.OverrideLogFile.Desc": "Użyj dokładnej ścieżki pliku zamiast dopisywania ID tytułu i znacznika czasu.",
"Options.TitleMusic.Label": "Muzyka tytułowa",
"Options.TitleMusic.Desc": "Odtwarzaj w pętli muzykę podglądu wybranej gry w bibliotece.",
"Options.Discord.Label": "Discord Rich Presence",
"Options.Discord.Desc": "Pokazuj uruchomioną grę na swoim profilu Discord.",
"Options.Language.Label": "Język emulatora",
"Options.Language.Desc": "Język używany w całym launcherze. Działa natychmiast.",
"Common.On": "Włącz",
"Common.Off": "Wyłącz",
"Common.Save": "Zapisz",
"Common.Cancel": "Anuluj",
"Console.Title": "KONSOLA",
"Console.SearchWatermark": "Szukaj...",
"Console.AutoScroll": "Autoprzewijanie",
"Console.Split": "Podziel",
"Console.Copy": "Kopiuj",
"Console.Clear": "Wyczyść",
"Console.WindowTitle": "Konsola SharpEmu",
"Launch.NoGameSelected": "Nie wybrano gry",
"Launch.NoGameHint": "Wybierz grę z biblioteki lub otwórz bezpośrednio plik eboot.bin.",
"Launch.Idle": "Bezczynny",
"Launch.Console": "≡ Konsola",
"Launch.Launch": "▶ Uruchom",
"Launch.Stop": "■ Zatrzymaj",
"Launch.Running": "Działa — {0}",
"Launch.Stopping": "Zatrzymywanie…",
"Launch.Exited": "Zakończono z kodem {0} ({1})",
"Launch.ExeNotFound": "Nie znaleziono pliku wykonywalnego SharpEmu. Najpierw zbuduj projekt SharpEmu.CLI (dotnet build).",
"Launch.LogFile": "Plik logu: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Nie udało się uruchomić emulatora: {0}",
"Launch.ProcessExited": "Proces zakończony z kodem {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "nieprawidłowe argumenty",
"Exit.EbootNotFound": "nie znaleziono eboot",
"Exit.RuntimeException": "wyjątek czasu wykonania",
"Exit.EmulationError": "błąd emulacji",
"Exit.Unknown": "nieznany",
"Status.EmulatorLocating": "Emulator: lokalizowanie…",
"Status.EmulatorPath": "Emulator: {0}",
"Status.EmulatorNotFound": "Emulator: nie znaleziono pliku wykonywalnego SharpEmu — najpierw zbuduj SharpEmu.CLI.",
"Status.ScanningLibrary": "Skanowanie biblioteki…",
"Status.AddFolderPrompt": "Dodaj folder z grami, aby wypełnić bibliotekę.",
"Status.LibraryScanned": "Biblioteka przeskanowana: {0} gier w {1} folderach.",
"Status.CouldNotOpenFolder": "Nie można otworzyć folderu: {0}",
"Status.CopiedToClipboard": "{0} skopiowano do schowka.",
"Status.RemovedFromLibrary": "Usunięto \u201c{0}\u201d z biblioteki. Dodaj ponownie folder, aby przywrócić.",
"Status.Running": "Uruchomiono {0}",
"Status.Stopping": "Zatrzymywanie…",
"Status.Idle": "Bezczynny",
"Clipboard.Path": "Ścieżka",
"Clipboard.TitleId": "ID tytułu",
"Discord.Playing": "Gra w {0}",
"Discord.Browsing": "Przegląda bibliotekę",
"Dialog.ChooseGameFolder": "Wybierz folder zawierający gry",
"Dialog.OpenExecutable": "Otwórz plik wykonywalny do uruchomienia",
"Dialog.PsExecutables": "Pliki wykonywalne PS",
"Dialog.SaveLogFile": "Wybierz, gdzie zapisać plik logu",
"Dialog.PlainTextFiles": "Pliki tekstowe",
"Dialog.LogFiles": "Pliki logów",
"Options.About" : "O programie",
"About.Github.Label": "GitHub",
"About.Github.Desc": "Kod źródłowy, zgłoszenia i rozwój projektu.",
"About.Github.LatestCommitLabel": "Najnowszy commit",
"About.Github.LatestCommitDescription": "Najnowszy commit na gałęzi głównej",
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Dołącz do społeczności, uzyskaj wsparcie i śledź rozwój.",
"About.GithubButton": "Wspieraj na GitHubie!",
"About.DiscordComingSoon": "Wkrótce",
"Updater.Auto.Label": "Sprawdzaj aktualizacje przy uruchomieniu",
"Updater.Auto.Desc": "Sprawdza GitHub bez opóźniania uruchomienia.",
"Updater.Label": "Aktualizacje",
"Updater.Check": "Sprawdź aktualizacje",
"Updater.DownloadRestart": "Pobierz i uruchom ponownie",
"Updater.Status.Ready": "Obecna wersja: {0}",
"Updater.Status.Checking": "Sprawdzanie aktualizacji…",
"Updater.Status.Current": "Masz najnowszą wersję ({0}).",
"Updater.Status.Available": "Dostępna nowa wersja: {0}",
"Updater.Status.Downloading": "Pobieranie aktualizacji… {0}%",
"Updater.Status.Installing": "Instalowanie aktualizacji…",
"Updater.Status.Timeout": "Sprawdzanie aktualizacji przekroczyło limit 10 sekund.",
"Updater.Status.Failed": "Nie udało się sprawdzić aktualizacji.",
"Updater.Status.ChecksumFailed": "Pobrana aktualizacja nie przeszła weryfikacji SHA-256.",
"Updater.Status.Unsupported": "Automatyczna aktualizacja wymaga wersji dla Windows, Linux lub macOS x64."
}
+1 -14
View File
@@ -5,13 +5,7 @@ namespace SharpEmu.HLE;
public sealed class ExportedFunction
{
public ExportedFunction(
string libraryName,
string nid,
string name,
Generation target,
SysAbiFunction function,
bool preferLle = false)
public ExportedFunction(string libraryName, string nid, string name, Generation target, SysAbiFunction function)
{
ArgumentException.ThrowIfNullOrWhiteSpace(libraryName);
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
@@ -23,7 +17,6 @@ public sealed class ExportedFunction
Name = name;
Target = target;
Function = function;
PreferLle = preferLle;
}
public string LibraryName { get; }
@@ -35,10 +28,4 @@ public sealed class ExportedFunction
public Generation Target { get; }
public SysAbiFunction Function { get; }
/// <summary>
/// A loaded guest export is authoritative for this registration. The HLE function
/// remains available as an explicit fallback when no usable guest target exists.
/// </summary>
public bool PreferLle { get; }
}
+1 -8
View File
@@ -3,7 +3,7 @@
namespace SharpEmu.HLE;
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
public sealed class SysAbiExportAttribute : Attribute
{
public string LibraryName { get; set; } = "libKernel";
@@ -13,11 +13,4 @@ public sealed class SysAbiExportAttribute : Attribute
public string ExportName { get; set; } = string.Empty;
public Generation Target { get; set; } = Generation.None;
/// <summary>
/// Prefer a matching export from a loaded guest module and use this handler only
/// as the explicit fallback when that LLE provider is unavailable. Individual
/// handlers define whether that fallback is fail-closed or compatibility behavior.
/// </summary>
public bool PreferLle { get; set; }
}
+14 -330
View File
@@ -118,29 +118,6 @@ public static partial class AgcExports
// Multiple producers can share one target label; last-writer-wins would
// starve waits on the others.
private static readonly Dictionary<ulong, List<ulong>> _cbReleaseMemTargets = new();
// CMASK meta-state tracking: maps colour-buffer addresses to their
// compression metadata. Keyed by colour-buffer base address so the
// consumption path (which only knows the surface address) can query
// directly without a reverse lookup.
private record struct MetaSurfaceInfo(
ulong CmaskAddress,
uint ClearWord0,
uint ClearWord1,
bool IsCleared);
private static readonly Dictionary<ulong, MetaSurfaceInfo> _metaSurfaces = new();
// Reverse map: CMASK address → colour-buffer address. Needed so
// CheckCmaskWrite (which only sees the write target address) can
// find the owning surface.
private static readonly Dictionary<ulong, ulong> _cmaskToColorBuffer = new();
// Guards _metaSurfaces and _cmaskToColorBuffer. Two threads touch them:
// the parse thread (registration in TrackCmaskAddresses, CheckCmaskWrite
// from DMA/compute writes, EFC consumption) and the render thread
// (MarkAllSurfacesCleared at guest flip, IsMetaClearedForSurface /
// ConsumeMetaClear / GetMetaClearValue at pass-record time). Plain
// Dictionaries corrupt under concurrent write, so every access below
// holds this gate. Keep the critical sections tiny and never block on
// anything external while holding it.
private static readonly object _metaSurfaceGate = new();
// header -> {ring base, write cursor} of the last submitted slice.
// Submissions stay cursor-bounded since rings aren't zeroed. Lap
// distinguishes a stale cursor from a previous pass over the same base.
@@ -1118,20 +1095,15 @@ public static partial class AgcExports
private const uint CbColor0Base = 0x318;
private const uint CbColorRegisterStride = 15;
private const uint CbColor0Info = 0x31C;
private const uint CbColor0Cmask = 0x31F;
private const uint CbColor0ClearWord0 = 0x323;
private const uint CbColor0ClearWord1 = 0x324;
private const uint CbColor0DccBase = 0x325;
private const uint CbColor0BaseExt = 0x390;
private const uint CbColor0CmaskBaseExt = 0x398;
private const uint CbColor0DccBaseExt = 0x3A8;
private const uint CbColor0Attrib2 = 0x3B0;
private const uint CbColor0Attrib3 = 0x3B8;
// CB_COLORn_INFO.DCC_ENABLE (gc_10_1_0_sh_mask.h). On GFX10 the legacy
// FAST_CLEAR and COMPRESSION bits stay clear because DCC, not CMASK,
// carries the compression.
private const uint CbColorInfoDccEnableMask = 1u << 28;
private const uint CbColorInfoFastClearEnableMask = 1u << 12;
private const uint CbBlend0Control = 0x1E0;
private const uint PaScModeCntl0 = 0x292;
// GFX10 DB context registers (register byte address minus 0x28000, / 4).
@@ -1151,8 +1123,8 @@ public static partial class AgcExports
private const uint EsUserDataRegister = 0xCC;
private const uint ComputeUserDataRegister = 0x240;
private const uint NggUserDataScalarRegisterBase = 8;
internal const uint Gen5TextureFormatR8G8B8A8Unorm = 10;
internal const uint Gen5TextureFormatR16G16B16A16Float = 12;
private const uint Gen5TextureFormatR8G8B8A8Unorm = 10;
private const uint Gen5TextureFormatR16G16B16A16Float = 12;
private const uint Gen5TextureType1D = 8;
private const uint Gen5TextureType2D = 9;
private const uint Gen5TextureType3D = 10;
@@ -5111,10 +5083,6 @@ public static partial class AgcExports
if (op == ItNop && register == RDmaData && length >= 7)
{
// Ensure CMASK addresses are tracked before DMA fills
var tempTargets = GetRenderTargets(state.CxRegisters);
TrackCmaskAddresses(state.CxRegisters, tempTargets);
ApplySubmittedDmaData(
ctx,
gpuState,
@@ -5275,7 +5243,6 @@ public static partial class AgcExports
{
TraceFramePacketSummary(state);
SyncCpuWrittenGuestImages(ctx);
GpuWaitRegistry.AdvanceFrame();
if (!TryReadUInt32(ctx, currentAddress + 4, out var videoOutHandle) ||
!TryReadUInt32(ctx, currentAddress + 8, out var displayBufferIndexRaw) ||
!TryReadUInt32(ctx, currentAddress + 12, out var flipMode) ||
@@ -6297,13 +6264,6 @@ public static partial class AgcExports
ulong byteCount,
uint? fillValue)
{
// Check if this DMA write targets a CMASK address (shadPS4's FillBuffer
// logic: when a buffer fill targets CMASK metadata, mark it as "all clear")
if (fillValue is { } fillVal && fillVal == 0)
{
CheckCmaskWrite(destinationAddress, null);
}
var hasImage = GuestGpu.Current.TryGetGuestImageExtent(
destinationAddress,
out var width,
@@ -6518,25 +6478,6 @@ public static partial class AgcExports
var targetAddress = destinationAddress +
(incrementAddress ? (ulong)index * sizeof(uint) : 0);
wroteData = TryWriteUInt32(ctx, targetAddress, values[index]);
if (wroteData)
{
GpuWaitRegistry.RecordProduced(
ctx.Memory, targetAddress, values[index]);
}
}
// Like ReleaseMem dataSel=2: a 64-bit WAIT_REG_MEM watches an
// 8-byte label written as two 32-bit dwords. Record the combined
// 64-bit value so a 64-bit EQ can latch even though the writes
// landed as two 32-bit stores.
if (wroteData && dwordCount >= 2 && incrementAddress)
{
var combined = ((ulong)values[1] << 32) | values[0];
GpuWaitRegistry.RecordProduced(
ctx.Memory, destinationAddress, combined);
// Also latch the high half's address for symmetry: a stray
// 32-bit wait on the high dword should not be confused, but
// recording it does not hurt and mirrors the per-dword stores.
}
if (tracePacket)
@@ -7112,13 +7053,7 @@ public static partial class AgcExports
if (hasCurrent && GpuWaitRegistry.Compare(waiter, currentValue))
{
// Value satisfies the condition, but only bypass if the label was
// written in the current frame. A stale label from a previous frame
// means the producer hasn't written yet this frame — must wait.
if (GpuWaitRegistry.IsLabelFresh(ctx.Memory, waitAddress))
{
return false; // satisfied by current-frame write — keep parsing
}
return false; // already satisfied — keep parsing
}
if (!_gpuWaitSuspendEnabled)
@@ -8109,8 +8044,7 @@ public static partial class AgcExports
var hasPsInputEna = state.CxRegisters.TryGetValue(SpiPsInputEna, out var psInputEna);
var hasPsInputAddr = state.CxRegisters.TryGetValue(SpiPsInputAddr, out var psInputAddr);
state.UcRegisters.TryGetValue(VgtPrimitiveType, out var primitiveType);
var renderTargets = GetRenderTargets(state.CxRegisters);
TrackCmaskAddresses(state.CxRegisters, renderTargets);
var renderTargets = GetRenderTargets(state.CxRegisters);
var drawSequence = ++gpuState.WorkSequence;
if (state.PendingTargetlessDraw is { } stalePendingDraw)
{
@@ -8130,31 +8064,6 @@ var renderTargets = GetRenderTargets(state.CxRegisters);
if (TryGetCbColorControlMode(state.CxRegisters, out var cbMode) &&
IsCbMetadataColorMode(cbMode))
{
// EliminateFastClear: the game explicitly asks the CB to clear
// the fast-clear metadata and the colour buffer.
if (cbMode == (uint)CbColorMode.EliminateFastClear &&
renderTargets.Count > 0 &&
renderTargets[0].Address != 0)
{
var targetAddr = renderTargets[0].Address;
bool requestClear;
lock (_metaSurfaceGate)
{
requestClear =
_metaSurfaces.TryGetValue(targetAddr, out var meta) &&
meta.IsCleared;
if (requestClear)
{
_metaSurfaces[targetAddr] = meta with { IsCleared = false };
}
}
if (requestClear)
{
VulkanVideoPresenter.RequestGuestColorClear(targetAddr);
}
}
if (_traceAgcShader || ShouldTraceHotPath(ref _cbMetadataSkipTraceCount))
{
TraceAgcShader(
@@ -8369,34 +8278,6 @@ var renderTargets = GetRenderTargets(state.CxRegisters);
return;
}
// DbRenderControl CLEARON (bit0): when set, the CB clears color
// targets on first draw. Handle color targets (depth is already
// handled by DecodeDepthState).
if (state.CxRegisters.TryGetValue(DbRenderControl, out var rc) && (rc & 0x1u) != 0)
{
foreach (var rt in translatedDraw.RenderTargets)
{
if (rt.Address != 0)
{
VulkanVideoPresenter.RequestGuestColorClear(rt.Address);
}
}
}
// CMASK fast clear: CB_COLORn_INFO.FAST_CLEAR (bit12) set on
// one or more targets. The CB clears via CMASK before the draw
// writes; mark targets for clear-on-first-use.
if (IsCmaskFastClearDraw(state.CxRegisters, translatedDraw.RenderTargets))
{
foreach (var rt in translatedDraw.RenderTargets)
{
if (rt.Address != 0)
{
VulkanVideoPresenter.RequestGuestColorClear(rt.Address);
}
}
}
var firstTarget = translatedDraw.RenderTargets.FirstOrDefault();
if (firstTarget.Address != 0)
{
@@ -9689,193 +9570,6 @@ var renderTargets = GetRenderTargets(state.CxRegisters);
CoversClipSpace(vertexInputs, vertexCount);
}
/// <summary>
/// GFX10 CMASK fast clear: CB_COLORn_INFO.FAST_CLEAR (bit 12) set on
/// one or more targets. The CB clears via CMASK before the draw writes;
/// mark targets for clear-on-first-use. Unlike DCC, the draw content
/// IS written (not dropped). Dead Cells uses DbRenderControl CLEARON
/// instead (bit0), not this mechanism.
/// </summary>
private static bool IsCmaskFastClearDraw(
IReadOnlyDictionary<uint, uint> registers,
IReadOnlyList<RenderTargetDescriptor> renderTargets)
{
foreach (var rt in renderTargets)
{
var stride = rt.Slot * CbColorRegisterStride;
if (registers.TryGetValue(CbColor0Info + stride, out var info) &&
(info & CbColorInfoFastClearEnableMask) != 0)
{
return true;
}
}
return false;
}
/// <summary>
/// Registers the CMASK metadata mapping for each colour buffer.
/// Does NOT mark as cleared — clearing only happens on actual clear
/// events (DMA fill, compute write, EFC draw).
/// </summary>
private static void TrackCmaskAddresses(
IReadOnlyDictionary<uint, uint> registers,
IReadOnlyList<RenderTargetDescriptor> renderTargets)
{
foreach (var rt in renderTargets)
{
var stride = rt.Slot * CbColorRegisterStride;
// CMASK metadata address (legacy GCN path).
var cmaskRegAddr = CbColor0Cmask + stride;
registers.TryGetValue(cmaskRegAddr, out var cmaskLow);
var cmaskExtAddr = CbColor0CmaskBaseExt + rt.Slot;
registers.TryGetValue(cmaskExtAddr, out var cmaskExt);
var cmaskAddress = ((ulong)(cmaskExt & 0xFFu) << 40) |
((ulong)(cmaskLow & 0x1FFFFFFFu) << 8);
// DCC metadata address (GFX10+ primary path).
var dccRegAddr = CbColor0DccBase + stride;
registers.TryGetValue(dccRegAddr, out var dccLow);
var dccExtAddr = CbColor0DccBaseExt + rt.Slot;
registers.TryGetValue(dccExtAddr, out var dccExt);
var dccAddress = ((ulong)(dccExt & 0xFFu) << 40) |
((ulong)(dccLow & 0x1FFFFFFFu) << 8);
// Prefer CMASK if present; fall back to DCC.
var metaAddress = cmaskAddress != 0 ? cmaskAddress : dccAddress;
var cw0Addr = CbColor0ClearWord0 + stride;
var cw1Addr = CbColor0ClearWord1 + stride;
registers.TryGetValue(cw0Addr, out var cw0);
registers.TryGetValue(cw1Addr, out var cw1);
lock (_metaSurfaceGate)
{
_metaSurfaces[rt.Address] = new MetaSurfaceInfo(
metaAddress, cw0, cw1,
// Re-registration runs on every draw; keep the cleared state
// so a mark-clear event survives until the pass consumes it.
// If the metadata binding changed, the old state refers to
// the old metadata and must be reset.
IsCleared: _metaSurfaces.TryGetValue(rt.Address, out var prev) &&
prev.IsCleared &&
prev.CmaskAddress == metaAddress);
if (metaAddress != 0)
{
_cmaskToColorBuffer[metaAddress] = rt.Address;
}
}
}
}
/// <summary>
/// Checks if a write targets a registered CMASK address. If so,
/// marks the owning colour buffer's metadata as "all clear".
/// </summary>
private static void CheckCmaskWrite(
ulong writeAddress,
SubmittedGpuState? gpuState)
{
if (writeAddress == 0)
{
return;
}
lock (_metaSurfaceGate)
{
// Exact match: write directly to a registered CMASK address.
if (_cmaskToColorBuffer.TryGetValue(writeAddress, out var cbAddr))
{
if (_metaSurfaces.TryGetValue(cbAddr, out var meta))
{
_metaSurfaces[cbAddr] = meta with { IsCleared = true };
}
return;
}
// CMASK surfaces are small (typically ≤ 4 KiB). Check the ±1024
// window around each registered address to catch partial writes.
foreach (var (cmaskAddr, colorBufAddr) in _cmaskToColorBuffer)
{
if (writeAddress >= cmaskAddr && writeAddress < cmaskAddr + 1024)
{
if (_metaSurfaces.TryGetValue(colorBufAddr, out var meta))
{
_metaSurfaces[colorBufAddr] = meta with { IsCleared = true };
}
return;
}
}
}
}
/// <summary>
/// Returns true if the colour buffer at <paramref name="colorBufferAddress"/>
/// has pending CMASK "all clear" metadata — i.e. the surface was fast-cleared
/// but not yet rendered into.
/// </summary>
internal static bool IsMetaClearedForSurface(ulong colorBufferAddress)
{
lock (_metaSurfaceGate)
{
return _metaSurfaces.TryGetValue(colorBufferAddress, out var meta) &&
meta.IsCleared;
}
}
/// <summary>
/// Consumes the "all clear" state for the given surface, marking it dirty.
/// Called after the first render pass uses LoadOp.Clear.
/// </summary>
internal static void ConsumeMetaClear(ulong colorBufferAddress)
{
lock (_metaSurfaceGate)
{
if (_metaSurfaces.TryGetValue(colorBufferAddress, out var meta))
{
_metaSurfaces[colorBufferAddress] = meta with { IsCleared = false };
}
}
}
/// <summary>
/// Returns the CB clear word values for the given colour buffer.
/// </summary>
internal static (uint Cw0, uint Cw1) GetMetaClearValue(ulong colorBufferAddress)
{
lock (_metaSurfaceGate)
{
if (_metaSurfaces.TryGetValue(colorBufferAddress, out var meta))
{
return (meta.ClearWord0, meta.ClearWord1);
}
}
return (0, 0);
}
/// <summary>
/// Marks all registered surfaces as "all clear". Called at guest flip
/// (frame boundary). Real hardware applies a fast clear / load-clear to
/// its per-frame surfaces every frame; the emulator restores that
/// per-frame clear here, per surface, at flip time. This is the
/// per-surface successor of the removed flip-arm heuristic (which reset
/// only the first multi-attachment group).
/// </summary>
internal static void MarkAllSurfacesCleared()
{
lock (_metaSurfaceGate)
{
foreach (var (addr, meta) in _metaSurfaces)
{
_metaSurfaces[addr] = meta with { IsCleared = true };
}
}
}
/// <summary>
/// True when the draw's float32x3 position stream spans the full clip
/// rectangle, i.e. x and y both reach -1 and +1.
@@ -10288,7 +9982,7 @@ var renderTargets = GetRenderTargets(state.CxRegisters);
private static readonly HashSet<ulong> _sampledRenderTargets = new();
private static readonly object _renderTargetProbeGate = new();
private static long _renderTargetSampleTraceCount;
private static long _indirectDrawProbeCount;
private static long _indirectDrawProbeCount;
private static long _indirectDrawEmitCount;
private static long _indirectDrawEmitRejectCount;
private static long _indirectMultiProbeCount;
@@ -12480,18 +12174,15 @@ private static long _indirectDrawProbeCount;
if (dispatchEndX == 0 || dispatchEndY == 0 || dispatchEndZ == 0)
{
// For indirect dispatches (both absolute and base), zero dimensions are a valid outcome
// of GPU culling passes (0 workgroups). VulkanVideoPresenter handles groupCount = 0 as a clean no-op.
if (opcode == ItDispatchIndirect || dispatchSource is "absolute-indirect" or "base-indirect")
// Indirect dispatches read their dimensions from a guest buffer a
// prior GPU dispatch fills. Zero here means that producer has not run
// yet — signal the caller to suspend on the dims buffer and retry,
// rather than dropping the work (which black-screens GPU-driven games
// like Astro Bot). Direct dispatches carry dims inline, so a zero is
// genuinely malformed and still rejected.
if (opcode == ItDispatchIndirect)
{
var waveCount = (initiator & (1u << 15)) != 0 ? 32u : 64u;
dispatch = new ComputeDispatch(
0, 0, 0,
0, 0, 0,
waveCount,
IsIndirect: true,
0, 0, 0);
return true;
indirectDimsRetryAddress = dimensionsAddress;
}
return RejectComputeDispatch(
@@ -12774,10 +12465,6 @@ private static long _indirectDrawProbeCount;
shaderAddress,
binding.Opcode);
// Check if this compute shader writes to a CMASK address
// (shadPS4's IsComputeMetaClear logic)
CheckCmaskWrite(texture.Address, gpuState);
TraceAgcShader(
$"agc.compute_writer addr=0x{texture.Address:X16} " +
$"fmt={texture.Format} num={texture.NumberType} tile={texture.TileMode} " +
@@ -13373,14 +13060,11 @@ private static long _indirectDrawProbeCount;
return;
}
GuestImageWriteTracker.Track(
GuestImageWriteTracker.Track(
destinationAddress,
(ulong)output.Length,
VulkanVideoPresenter.CurrentGuestWorkSequenceForDiagnostics,
"agc.constant-fill");
VulkanVideoPresenter.RequestGuestColorClear(destinationAddress);
},
$"constant_fill dst=0x{destinationAddress:X16} bytes={output.Length}");
description =
+118 -55
View File
@@ -238,12 +238,10 @@ internal static class AgcVertexMetadata
}
/// <summary>
/// Patch IR-discovered fetches from the attrib table onto the V# layout.
/// Patch IR-discovered fetches from the attrib table onto the V# format/offset.
/// Prefer 1:1 Location pairing when counts match on one interleaved stream
/// (GTA UI glyphs). Otherwise match by the effective captured byte offset.
/// Never rebases BaseAddress/Data/Location/Pc or overwrites a discovered
/// offset: metadata may refine the format, stride and instance rate only
/// after both address keys independently resolve to the same attribute.
/// (GTA UI glyphs). Otherwise match by stride + byte offset. Never rebases
/// BaseAddress/Data/Location/Pc/PerInstance.
/// </summary>
internal static IReadOnlyList<Gen5VertexInputBinding> MergeVertexInputsFromMetadata(
CpuContext ctx,
@@ -271,13 +269,13 @@ internal static class AgcVertexMetadata
var changed = false;
foreach (var input in discovered)
{
if (!TryMatchMetadataResource(input, resources, usedResources, out var resource))
if (!TryMatchMetadataResource(input, resources, usedResources, out var resource, out var fillOffset))
{
merged.Add(input);
continue;
}
var refined = ApplyMetadataFormat(input, resource);
var refined = ApplyMetadataFormat(input, resource, fillOffset);
changed |= refined != input;
merged.Add(refined);
}
@@ -288,7 +286,7 @@ internal static class AgcVertexMetadata
/// <summary>
/// When discovery and metadata describe the same interleaved stream with
/// equal attribute counts, pair by sorted Location (semantic order).
/// Keeps each binding's Pc/Location/address for SPIR-V and overlays layout.
/// Keeps each binding's Pc/Location for SPIR-V; overlays format + offset.
/// </summary>
private static bool TryMergeByLocationPairing(
IReadOnlyList<Gen5VertexInputBinding> discovered,
@@ -301,36 +299,34 @@ internal static class AgcVertexMetadata
return false;
}
var orderedInputs = discovered
.Select(static (input, originalIndex) => (Input: input, OriginalIndex: originalIndex))
.OrderBy(static entry => entry.Input.Location)
.ThenBy(static entry => entry.OriginalIndex)
.ToArray();
var orderedInputs = discovered.OrderBy(static input => input.Location).ToArray();
var orderedResources = resources.OrderBy(static resource => resource.Location).ToArray();
var streamBase = orderedResources[0].SharpBase;
var streamStride = orderedResources[0].Stride;
for (var index = 0; index < orderedResources.Length; index++)
{
var resource = orderedResources[index];
var input = orderedInputs[index].Input;
var input = orderedInputs[index];
if (resource.SharpBase != streamBase ||
resource.Stride != streamStride ||
!TryGetMetadataOffset(input, resource, out var resolvedOffset) ||
resolvedOffset != input.OffsetBytes)
(input.Stride != 0 && input.Stride != streamStride) ||
!IsSameVertexStream(input, resource))
{
return false;
}
}
var result = discovered.ToArray();
var byPc = new Dictionary<uint, Gen5VertexInputBinding>(discovered.Count);
var changed = false;
for (var index = 0; index < orderedInputs.Length; index++)
{
var input = orderedInputs[index].Input;
var input = orderedInputs[index];
var resource = orderedResources[index];
var refined = ApplyMetadataFormat(input, resource);
var fillOffset = input.BaseAddress == resource.SharpBase ||
IsAddressInsideCapturedSpan(input, resource.SharpBase);
var refined = ApplyMetadataFormat(input, resource, fillOffset);
changed |= refined != input;
result[orderedInputs[index].OriginalIndex] = refined;
byPc[input.Pc] = refined;
}
if (!changed)
@@ -338,13 +334,20 @@ internal static class AgcVertexMetadata
return false;
}
var result = new Gen5VertexInputBinding[discovered.Count];
for (var index = 0; index < discovered.Count; index++)
{
result[index] = byPc[discovered[index].Pc];
}
merged = result;
return true;
}
private static Gen5VertexInputBinding ApplyMetadataFormat(
Gen5VertexInputBinding input,
MetadataVertexResource resource)
MetadataVertexResource resource,
bool fillOffsetBytes)
{
var components = input.ComponentCount != 0 &&
input.ComponentCount < resource.ComponentCount
@@ -356,8 +359,7 @@ internal static class AgcVertexMetadata
DataFormat = resource.DataFormat,
NumberFormat = resource.NumberFormat,
ComponentCount = components,
Stride = resource.Stride,
PerInstance = resource.PerInstance,
OffsetBytes = fillOffsetBytes ? resource.OffsetBytes : input.OffsetBytes,
};
}
@@ -432,11 +434,14 @@ internal static class AgcVertexMetadata
Gen5VertexInputBinding input,
IReadOnlyList<MetadataVertexResource> resources,
bool[] usedResources,
out MetadataVertexResource resource)
out MetadataVertexResource resource,
out bool fillOffsetBytes)
{
resource = default;
fillOffsetBytes = false;
var bestScore = int.MinValue;
var bestIndex = -1;
var bestFillOffset = false;
for (var index = 0; index < resources.Count; index++)
{
if (usedResources[index])
@@ -445,64 +450,100 @@ internal static class AgcVertexMetadata
}
var candidate = resources[index];
if (!TryGetMetadataOffset(input, candidate, out var resolvedOffset) ||
resolvedOffset != input.OffsetBytes)
if (candidate.Stride != 0 &&
input.Stride != 0 &&
candidate.Stride != input.Stride)
{
continue;
}
// The effective captured offset (including any base rebasing done
// while coalescing adjacent vertex streams) is the primary key.
var score = 400;
// Discovery can carry a stale inferred stride (notably 32 for a
// real stride-40 interleaved layout). Prefer a matching stride
// when candidates are otherwise equivalent, but do not reject an
// unambiguous metadata match: the V# descriptor is authoritative.
if (input.Stride == candidate.Stride)
if (!IsSameVertexStream(input, candidate))
{
score += 25;
continue;
}
var attrAddress = candidate.SharpBase + candidate.OffsetBytes;
var score = int.MinValue;
var fillOffset = false;
// Post-capture interleaved: shared BaseAddress, distinct OffsetBytes.
if (input.OffsetBytes == candidate.OffsetBytes &&
(input.BaseAddress == candidate.SharpBase ||
IsAddressInsideCapturedSpan(input, candidate.SharpBase)))
{
score = 400;
}
// IR prolog baked attrib offset into the V# base.
else if (input.BaseAddress == attrAddress)
{
score = 350;
}
// Discovery never saw the attrib offset — only safe when this
// resource's offset uniquely identifies it among unused entries.
else if (input.BaseAddress == candidate.SharpBase &&
input.OffsetBytes == 0 &&
candidate.OffsetBytes != 0 &&
IsUniqueUnusedOffset(resources, usedResources, candidate.OffsetBytes, index))
{
score = 300;
fillOffset = true;
}
else if (input.BaseAddress == candidate.SharpBase &&
input.OffsetBytes == 0 &&
candidate.OffsetBytes == 0)
{
score = 250;
}
if (score > bestScore)
{
bestScore = score;
bestIndex = index;
bestFillOffset = fillOffset;
}
}
if (bestIndex < 0)
// Require an offset-aware match. Bare SharpBase ties (score 250) are
// only accepted when a single unused resource remains for that stream.
if (bestIndex < 0 || bestScore < 300)
{
return false;
if (bestIndex < 0 || bestScore < 250)
{
return false;
}
var unusedSameStream = 0;
for (var index = 0; index < resources.Count; index++)
{
if (!usedResources[index] && IsSameVertexStream(input, resources[index]))
{
unusedSameStream++;
}
}
if (unusedSameStream != 1)
{
return false;
}
}
usedResources[bestIndex] = true;
resource = resources[bestIndex];
fillOffsetBytes = bestFillOffset;
return true;
}
private static bool TryGetMetadataOffset(
private static bool IsSameVertexStream(
Gen5VertexInputBinding input,
MetadataVertexResource resource,
out uint offsetBytes)
MetadataVertexResource resource)
{
offsetBytes = input.OffsetBytes;
if (resource.SharpBase < input.BaseAddress ||
(!IsAddressInsideCapturedSpan(input, resource.SharpBase) &&
resource.SharpBase != input.BaseAddress))
if (input.BaseAddress == resource.SharpBase ||
input.BaseAddress == resource.SharpBase + resource.OffsetBytes)
{
return false;
return true;
}
var relativeBase = resource.SharpBase - input.BaseAddress;
var resolvedOffset = relativeBase + resource.OffsetBytes;
if (resolvedOffset > uint.MaxValue)
{
return false;
}
offsetBytes = (uint)resolvedOffset;
return true;
return IsAddressInsideCapturedSpan(input, resource.SharpBase);
}
private static bool IsAddressInsideCapturedSpan(
@@ -512,6 +553,28 @@ internal static class AgcVertexMetadata
address >= input.BaseAddress &&
address < input.BaseAddress + (ulong)input.DataLength;
private static bool IsUniqueUnusedOffset(
IReadOnlyList<MetadataVertexResource> resources,
bool[] usedResources,
uint offsetBytes,
int candidateIndex)
{
for (var index = 0; index < resources.Count; index++)
{
if (index == candidateIndex || usedResources[index])
{
continue;
}
if (resources[index].OffsetBytes == offsetBytes)
{
return false;
}
}
return true;
}
/// <summary>
/// Attrib-table format
/// fields are VertexAttribFormat; V# / Vulkan paths need BufferFormat.
-33
View File
@@ -59,10 +59,6 @@ internal static class GpuWaitRegistry
// cycle forever even though a real producer did signal it. Keyed by (memory,
// address) so distinct guest processes never alias.
private static readonly Dictionary<(object, ulong), ulong> _lastProduced = new();
// Frame-staleness guard: tracks the frame ID of each label write so that
// WAIT_REG_MEM in frame N+1 is not satisfied by a stale write from frame N.
private static readonly Dictionary<(object, ulong), long> _labelFrameIds = new();
private static long _currentFrameId;
private static object? Canonicalize(object? memory)
@@ -75,34 +71,6 @@ internal static class GpuWaitRegistry
return memory;
}
/// <summary>
/// Advances the frame counter. Called at each frame boundary (flip) so that
/// stale label writes from previous frames cannot satisfy WAIT_REG_MEM.
/// </summary>
public static void AdvanceFrame()
{
System.Threading.Interlocked.Increment(ref _currentFrameId);
}
/// <summary>
/// Returns true if the label at (memory, address) was written in the
/// current frame, or has never been written (uninitialized).
/// Only labels written in a PREVIOUS frame are considered stale.
/// </summary>
public static bool IsLabelFresh(object memory, ulong address)
{
memory = Canonicalize(memory)!;
lock (_gate)
{
if (!_labelFrameIds.TryGetValue((memory, address), out var frameId))
{
return true; // never written — treat as fresh (not stale)
}
return frameId >= System.Threading.Volatile.Read(ref _currentFrameId);
}
}
public static int Count
{
get
@@ -608,7 +576,6 @@ internal static class GpuWaitRegistry
}
_lastProduced[(memory, address)] = value;
_labelFrameIds[(memory, address)] = System.Threading.Volatile.Read(ref _currentFrameId);
}
return LatchSatisfiedByValue(memory, address, value);
@@ -5884,7 +5884,6 @@ internal static unsafe class VulkanVideoPresenter
private void ExecuteOrderedGuestFlip(VulkanOrderedGuestFlip work)
{
Agc.AgcExports.MarkAllSurfacesCleared();
FlushBatchedGuestCommands();
_guestImages.TryGetValue(work.Address, out var source);
if (_deviceLost ||
@@ -12771,18 +12770,6 @@ internal static unsafe class VulkanVideoPresenter
targets[index].Initialized = false;
}
// CMASK meta-state: if the surface's metadata says "all clear",
// start this pass from LoadOp.Clear and consume the state.
// CPU-backed targets are skipped (their guest memory contents
// are uploaded, not cleared) — same rule the flip-arm used.
if (work.Targets[index].Address != 0 &&
!targets[index].IsCpuBacked &&
Agc.AgcExports.IsMetaClearedForSurface(work.Targets[index].Address))
{
targets[index].Initialized = false;
Agc.AgcExports.ConsumeMetaClear(work.Targets[index].Address);
}
if (work.Targets[index].Address != 0 &&
TakeGuestImageInitialData(work.Targets[index].Address) is { } initialData &&
!targets[index].Initialized &&
@@ -13044,31 +13031,13 @@ internal static unsafe class VulkanVideoPresenter
&toDepthAttachment);
}
ClearColorValue[]? metaClearValues = null;
for (var ci = 0; ci < targets.Length; ci++)
{
if (!targets[ci].Initialized &&
work.Targets[ci].Address != 0)
{
var (cw0, cw1) = Agc.AgcExports.GetMetaClearValue(
work.Targets[ci].Address);
if (cw0 != 0 || cw1 != 0)
{
metaClearValues ??= new ClearColorValue[targets.Length];
metaClearValues[ci] = UnpackMetaClearValue(
work.Targets[ci].Format, cw0, cw1);
}
}
}
BeginTranslatedRenderPass(
renderPass,
framebuffer,
extent,
colorAttachmentCount: targets.Length,
hasDepthAttachment: depth is not null && !clearDepthSeparately,
clearDepth: depth?.ClearDepth ?? 1f,
colorClearValues: metaClearValues);
clearDepth: depth?.ClearDepth ?? 1f);
RecordTranslatedDrawInPass(resources, extent);
_vk.CmdEndRenderPass(_commandBuffer);
@@ -13859,10 +13828,16 @@ internal static unsafe class VulkanVideoPresenter
existing.LogicalDepth == depth &&
existing.Type == type &&
existing.MipLevels == mipLevels &&
(!requiresStorage || existing.SupportsStorageUsage) &&
(exactFormatMatch ||
IsAliasableGuestImageFormat(existing.Format, format)))
(IsAliasableGuestImageFormat(existing.Format, format) &&
(!requiresStorage || existing.SupportsStorageUsage))))
{
if (requiresStorage && !existing.SupportsStorageUsage)
{
throw new InvalidOperationException(
$"Guest image 0x{target.Address:X16} was created without storage usage.");
}
existing.IsCpuBacked = false;
existing.CpuContentFingerprint = 0;
if (existing.RenderPass.Handle == 0 &&
@@ -13895,9 +13870,14 @@ internal static unsafe class VulkanVideoPresenter
if (existing.Width == target.Width &&
existing.Height == target.Height &&
existing.MipLevels == mipLevels &&
(!requiresStorage || existing.SupportsStorageUsage) &&
IsCompatibleViewFormat(existing.Format, format))
{
if (requiresStorage && !existing.SupportsStorageUsage)
{
throw new InvalidOperationException(
$"Guest image 0x{target.Address:X16} was created without storage usage.");
}
if (_traceGuestImageEvents)
{
Console.Error.WriteLine(
@@ -13972,52 +13952,50 @@ internal static unsafe class VulkanVideoPresenter
{
if (requiresStorage && !retained.SupportsStorageUsage)
{
// Do not reuse retained image if it lacks required storage usage
DestroyGuestImage(retained);
throw new InvalidOperationException(
$"Retained guest image 0x{target.Address:X16} was created without storage usage.");
}
else
retained.IsCpuBacked = false;
retained.CpuContentFingerprint = 0;
_guestImages.Add(target.Address, retained);
var retainedByteCount = GetTextureByteCount(
target.Format,
target.Width,
target.Height,
depth);
lock (_gate)
{
retained.IsCpuBacked = false;
retained.CpuContentFingerprint = 0;
_guestImages.Add(target.Address, retained);
var retainedByteCount = GetTextureByteCount(
target.Format,
_cpuBackedUploadGenerations.Remove(target.Address);
_guestImageExtents[target.Address] = (
target.Width,
target.Height,
depth);
lock (_gate)
{
_cpuBackedUploadGenerations.Remove(target.Address);
_guestImageExtents[target.Address] = (
target.Width,
target.Height,
retainedByteCount);
}
// Arm the exact extent the flip/acquire sync path would read
// back, budgeted by bytes rather than by resolution: the old
// 1920x1080 cap left every 4K surface permanently
// un-invalidated, so a guest CPU rewrite of one was never
// reflected and the sample served stale bytes.
if (ShouldTrackGuestImageWrites(retainedByteCount))
{
SharpEmu.HLE.GuestImageWriteTracker.Track(
target.Address,
retainedByteCount,
CurrentGuestWorkSequenceForDiagnostics,
"vulkan.render-target");
}
if (_traceGuestImageEvents)
{
Console.Error.WriteLine(
$"[GIMG] retained addr=0x{target.Address:X} " +
$"{target.Width}x{target.Height} fmt={format} " +
$"initialized={retained.Initialized}");
}
return retained;
retainedByteCount);
}
// Arm the exact extent the flip/acquire sync path would read
// back, budgeted by bytes rather than by resolution: the old
// 1920x1080 cap left every 4K surface permanently
// un-invalidated, so a guest CPU rewrite of one was never
// reflected and the sample served stale bytes.
if (ShouldTrackGuestImageWrites(retainedByteCount))
{
SharpEmu.HLE.GuestImageWriteTracker.Track(
target.Address,
retainedByteCount,
CurrentGuestWorkSequenceForDiagnostics,
"vulkan.render-target");
}
if (_traceGuestImageEvents)
{
Console.Error.WriteLine(
$"[GIMG] retained addr=0x{target.Address:X} " +
$"{target.Width}x{target.Height} fmt={format} " +
$"initialized={retained.Initialized}");
}
return retained;
}
var imageInfo = new ImageCreateInfo
@@ -17719,67 +17697,20 @@ internal static unsafe class VulkanVideoPresenter
_vk.CmdEndRenderPass(_commandBuffer);
}
/// <summary>
/// Decodes the CB CLEAR_WORD0/1 pair into a float RGBA clear value
/// according to the surface pixel format. CLEAR_WORD holds the clear
/// colour packed in the surface's native layout, so the two 32-bit
/// words must be unpacked channel-by-channel; passing the raw word as
/// a single float channel clears to a garbage colour.
/// </summary>
private static ClearColorValue UnpackMetaClearValue(
uint format, uint cw0, uint cw1)
{
switch (format)
{
// Gen5 8_8_8_8 (R8G8B8A8): four UNORM bytes packed in WORD0,
// little-endian channel order R,G,B,A.
case Agc.AgcExports.Gen5TextureFormatR8G8B8A8Unorm:
return new ClearColorValue(
float32_0: ((cw0 >> 0) & 0xFF) / 255f,
float32_1: ((cw0 >> 8) & 0xFF) / 255f,
float32_2: ((cw0 >> 16) & 0xFF) / 255f,
float32_3: ((cw0 >> 24) & 0xFF) / 255f);
// Gen5 16_16_16_16 float (R16G16B16A16F): R,G as halfs in
// WORD0 and B,A as halfs in WORD1.
case Agc.AgcExports.Gen5TextureFormatR16G16B16A16Float:
return new ClearColorValue(
float32_0: HalfToFloat((ushort)(cw0 >> 0)),
float32_1: HalfToFloat((ushort)(cw0 >> 16)),
float32_2: HalfToFloat((ushort)(cw1 >> 0)),
float32_3: HalfToFloat((ushort)(cw1 >> 16)));
default:
// Unknown format: fall back to the common 8_8_8_8 layout.
return new ClearColorValue(
float32_0: ((cw0 >> 0) & 0xFF) / 255f,
float32_1: ((cw0 >> 8) & 0xFF) / 255f,
float32_2: ((cw0 >> 16) & 0xFF) / 255f,
float32_3: ((cw0 >> 24) & 0xFF) / 255f);
}
}
private static float HalfToFloat(ushort halfBits) =>
(float)BitConverter.UInt16BitsToHalf(halfBits);
private void BeginTranslatedRenderPass(
RenderPass renderPass,
Framebuffer framebuffer,
Extent2D extent,
int colorAttachmentCount = 1,
bool hasDepthAttachment = false,
float clearDepth = 1f,
ClearColorValue[]? colorClearValues = null)
float clearDepth = 1f)
{
colorAttachmentCount = Math.Max(colorAttachmentCount, 1);
var clearValueCount = colorAttachmentCount + (hasDepthAttachment ? 1 : 0);
var clearValues = stackalloc ClearValue[clearValueCount];
for (var index = 0; index < colorAttachmentCount; index++)
{
clearValues[index] = colorClearValues is not null &&
index < colorClearValues.Length
? new ClearValue { Color = colorClearValues[index] }
: default;
clearValues[index] = default;
}
// Reverse-Z is not assumed; clear depth to 1.0 (far) so a standard
// LessOrEqual/Less test keeps the nearest fragment.
@@ -144,35 +144,11 @@ public static partial class Gen5MslTranslator
// ---- float arithmetic ----
"VAddF32" => FloatResult(instruction, $"{F(instruction, 0)} + {F(instruction, 1)}"),
"VAddF16" => Float16Result(
instruction,
destination,
$"{F16(instruction, 0)} + {F16(instruction, 1)}"),
"VSubF32" => FloatResult(instruction, $"{F(instruction, 0)} - {F(instruction, 1)}"),
"VSubrevF32" => FloatResult(instruction, $"{F(instruction, 1)} - {F(instruction, 0)}"),
"VSubF16" => Float16Result(
instruction,
destination,
$"{F16(instruction, 0)} - {F16(instruction, 1)}"),
"VSubrevF16" => Float16Result(
instruction,
destination,
$"{F16(instruction, 1)} - {F16(instruction, 0)}"),
"VMulF32" => FloatResult(instruction, $"{F(instruction, 0)} * {F(instruction, 1)}"),
"VMulF16" => Float16Result(
instruction,
destination,
$"{F16(instruction, 0)} * {F16(instruction, 1)}"),
"VMinF32" => FloatResult(instruction, $"fmin({F(instruction, 0)}, {F(instruction, 1)})"),
"VMaxF32" => FloatResult(instruction, $"fmax({F(instruction, 0)}, {F(instruction, 1)})"),
"VMinF16" => Float16Result(
instruction,
destination,
$"fmin({F16(instruction, 0)}, {F16(instruction, 1)})"),
"VMaxF16" => Float16Result(
instruction,
destination,
$"fmax({F16(instruction, 0)}, {F16(instruction, 1)})"),
// The decoder normalizes mk/ak literal placement, so every MAD/FMA
// form is fma(src0, src1, src2) exactly like the SPIR-V translator.
"VFmaF32" or "VMadF32" or "VMadAkF32" or "VMadMkF32" or "VFmaAkF32" or "VFmaMkF32" =>
@@ -602,46 +578,23 @@ public static partial class Gen5MslTranslator
{
condition = EmitCompareClass(instruction);
}
else if (opcode is
"VCmpTruF32" or "VCmpxTruF32" or
"VCmpTruF16" or "VCmpxTruF16" or
"VCmpTI32" or "VCmpTU32")
else if (opcode is "VCmpTruF32" or "VCmpxTruF32" or "VCmpTI32" or "VCmpTU32")
{
condition = "true";
}
else if (opcode is
"VCmpFF32" or "VCmpxFF32" or
"VCmpFF16" or "VCmpxFF16" or
"VCmpFI32" or "VCmpFU32")
else if (opcode is "VCmpFF32" or "VCmpxFF32" or "VCmpFI32" or "VCmpFU32")
{
condition = "false";
}
else if (opcode is
"VCmpOF32" or "VCmpxOF32" or
"VCmpOF16" or "VCmpxOF16")
else if (opcode is "VCmpOF32" or "VCmpxOF32")
{
var left = opcode.EndsWith("F16", StringComparison.Ordinal)
? F16(instruction, 0)
: F(instruction, 0);
var right = opcode.EndsWith("F16", StringComparison.Ordinal)
? F16(instruction, 1)
: F(instruction, 1);
condition = $"(!isnan({left}) && !isnan({right}))";
condition = $"(!isnan({F(instruction, 0)}) && !isnan({F(instruction, 1)}))";
}
else if (opcode is
"VCmpUF32" or "VCmpxUF32" or
"VCmpUF16" or "VCmpxUF16")
else if (opcode is "VCmpUF32" or "VCmpxUF32")
{
var left = opcode.EndsWith("F16", StringComparison.Ordinal)
? F16(instruction, 0)
: F(instruction, 0);
var right = opcode.EndsWith("F16", StringComparison.Ordinal)
? F16(instruction, 1)
: F(instruction, 1);
condition = $"(isnan({left}) || isnan({right}))";
condition = $"(isnan({F(instruction, 0)}) || isnan({F(instruction, 1)}))";
}
else if (opcode.EndsWith("F32", StringComparison.Ordinal) ||
opcode.EndsWith("F16", StringComparison.Ordinal))
else if (opcode.EndsWith("F32", StringComparison.Ordinal))
{
// Ordered compares are the plain C operators (false on NaN);
// the Nxx forms are their unordered negations (true on NaN).
@@ -667,13 +620,7 @@ public static partial class Gen5MslTranslator
return false;
}
var left = opcode.EndsWith("F16", StringComparison.Ordinal)
? F16(instruction, 0)
: F(instruction, 0);
var right = opcode.EndsWith("F16", StringComparison.Ordinal)
? F16(instruction, 1)
: F(instruction, 1);
var comparison = $"({left} {op} {right})";
var comparison = $"({F(instruction, 0)} {op} {F(instruction, 1)})";
condition = unordered ? $"(!{comparison})" : comparison;
}
else
@@ -1623,80 +1570,6 @@ public static partial class Gen5MslTranslator
return expression;
}
/// <summary>Reads the selected 16-bit half as a widened float.</summary>
private string F16(Gen5ShaderInstruction instruction, int sourceIndex)
{
var operand = instruction.Sources[sourceIndex];
string expression;
if (operand.Kind == Gen5OperandKind.EncodedConstant &&
Gen5InlineConstants.TryDecode(operand.Value, out var inline))
{
expression = operand.Value switch
{
>= 128 and <= 192 => $"{operand.Value - 128}.0f",
>= 193 and <= 208 => $"(-{operand.Value - 192}.0f)",
_ => AsFloat(FormatUInt(inline)),
};
}
else
{
var raw = RawSource(
instruction,
sourceIndex,
applySdwaIntegerModifiers: false);
var shift = instruction.Control is Gen5Vop3Control control &&
(control.OperandSelect & (1u << sourceIndex)) != 0
? 16
: 0;
expression =
$"(float)as_type<half>((ushort)((({raw}) >> {shift}) & 0xFFFFu))";
}
var (absoluteMask, negateMask) = instruction.Control switch
{
Gen5Vop3Control control => (control.AbsoluteMask, control.NegateMask),
Gen5SdwaControl control => (control.AbsoluteMask, control.NegateMask),
Gen5DppControl control => (control.AbsoluteMask, control.NegateMask),
_ => (0u, 0u),
};
if ((absoluteMask & (1u << sourceIndex)) != 0)
{
expression = $"fabs({expression})";
}
if ((negateMask & (1u << sourceIndex)) != 0)
{
expression = $"(-{expression})";
}
return expression;
}
/// <summary>Rounds to f16 and preserves the unselected VGPR half.</summary>
private string Float16Result(
Gen5ShaderInstruction instruction,
uint destination,
string expression)
{
var control = instruction.Control as Gen5Vop3Control;
expression = (control?.OutputModifier ?? 0) switch
{
1 => $"(({expression}) * 2.0f)",
2 => $"(({expression}) * 4.0f)",
3 => $"(({expression}) * 0.5f)",
_ => expression,
};
if (control?.Clamp == true)
{
expression = $"clamp({expression}, 0.0f, 1.0f)";
}
var packed = $"(uint)as_type<ushort>(half({expression}))";
return ((control?.OperandSelect ?? 0) & 8) != 0
? $"((v[{destination}] & 0x0000FFFFu) | (({packed}) << 16))"
: $"((v[{destination}] & 0xFFFF0000u) | ({packed}))";
}
/// <summary>
/// Wraps a float expression with VOP3/SDWA output modifiers and clamp,
/// then bitcasts back to the register file's uint domain.
@@ -340,43 +340,21 @@ public static partial class Gen5SpirvTranslator
case "VAddF32":
result = EmitFloatBinary(instruction, SpirvOp.FAdd);
break;
case "VAddF16":
result = EmitFloat16Binary(instruction, destination, SpirvOp.FAdd);
break;
case "VSubF32":
result = EmitFloatBinary(instruction, SpirvOp.FSub);
break;
case "VSubrevF32":
result = EmitFloatBinary(instruction, SpirvOp.FSub, reverse: true);
break;
case "VSubF16":
result = EmitFloat16Binary(instruction, destination, SpirvOp.FSub);
break;
case "VSubrevF16":
result = EmitFloat16Binary(
instruction,
destination,
SpirvOp.FSub,
reverse: true);
break;
case "VMulF32":
result = EmitFloatBinary(instruction, SpirvOp.FMul);
break;
case "VMulF16":
result = EmitFloat16Binary(instruction, destination, SpirvOp.FMul);
break;
case "VMinF32":
result = EmitFloatExtBinary(instruction, 37);
break;
case "VMaxF32":
result = EmitFloatExtBinary(instruction, 40);
break;
case "VMinF16":
result = EmitFloat16ExtBinary(instruction, destination, 37);
break;
case "VMaxF16":
result = EmitFloat16ExtBinary(instruction, destination, 40);
break;
case "VMadF32":
case "VFmaF32":
case "VMadMkF32":
@@ -1631,72 +1609,29 @@ public static partial class Gen5SpirvTranslator
condition,
SignedClass(0x020, 0x040, zero));
}
else if (opcode is
"VCmpFF32" or "VCmpxFF32" or
"VCmpFF16" or "VCmpxFF16" or
"VCmpFI32" or "VCmpFU32")
else if (opcode is "VCmpFF32" or "VCmpxFF32" or "VCmpFI32" or "VCmpFU32")
{
condition = _module.ConstantBool(false);
}
else if (opcode is
"VCmpTruF32" or "VCmpxTruF32" or
"VCmpTruF16" or "VCmpxTruF16" or
"VCmpTI32" or "VCmpTU32")
else if (opcode is "VCmpTruF32" or "VCmpxTruF32" or "VCmpTI32" or "VCmpTU32")
{
condition = _module.ConstantBool(true);
}
else if (opcode is
"VCmpOF32" or "VCmpxOF32" or
"VCmpUF32" or "VCmpxUF32" or
"VCmpOF16" or "VCmpxOF16" or
"VCmpUF16" or "VCmpxUF16")
"VCmpUF32" or "VCmpxUF32")
{
var isHalf = opcode.EndsWith("F16", StringComparison.Ordinal);
var left = isHalf
? GetFloat16Source(instruction, 0)
: GetFloatSource(instruction, 0);
var right = isHalf
? GetFloat16Source(instruction, 1)
: GetFloatSource(instruction, 1);
var left = GetFloatSource(instruction, 0);
var right = GetFloatSource(instruction, 1);
var unordered = _module.AddInstruction(
SpirvOp.LogicalOr,
_boolType,
_module.AddInstruction(SpirvOp.IsNan, _boolType, left),
_module.AddInstruction(SpirvOp.IsNan, _boolType, right));
condition = opcode is
"VCmpUF32" or "VCmpxUF32" or
"VCmpUF16" or "VCmpxUF16"
condition = opcode is "VCmpUF32" or "VCmpxUF32"
? unordered
: _module.AddInstruction(SpirvOp.LogicalNot, _boolType, unordered);
}
else if (opcode.EndsWith("F16", StringComparison.Ordinal))
{
var left = GetFloat16Source(instruction, 0);
var right = GetFloat16Source(instruction, 1);
var operation = opcode switch
{
"VCmpLtF16" or "VCmpxLtF16" => SpirvOp.FOrdLessThan,
"VCmpEqF16" or "VCmpxEqF16" => SpirvOp.FOrdEqual,
"VCmpLeF16" or "VCmpxLeF16" => SpirvOp.FOrdLessThanEqual,
"VCmpGtF16" or "VCmpxGtF16" => SpirvOp.FOrdGreaterThan,
"VCmpLgF16" or "VCmpxLgF16" => SpirvOp.FOrdNotEqual,
"VCmpGeF16" or "VCmpxGeF16" => SpirvOp.FOrdGreaterThanEqual,
"VCmpNeqF16" or "VCmpxNeqF16" => SpirvOp.FUnordNotEqual,
"VCmpNltF16" or "VCmpxNltF16" => SpirvOp.FUnordGreaterThanEqual,
"VCmpNleF16" or "VCmpxNleF16" => SpirvOp.FUnordGreaterThan,
"VCmpNgtF16" or "VCmpxNgtF16" => SpirvOp.FUnordLessThanEqual,
"VCmpNgeF16" or "VCmpxNgeF16" => SpirvOp.FUnordLessThan,
"VCmpNlgF16" or "VCmpxNlgF16" => SpirvOp.FUnordEqual,
_ => SpirvOp.Nop,
};
if (operation == SpirvOp.Nop)
{
error = $"unsupported half compare {opcode}";
return false;
}
condition = _module.AddInstruction(operation, _boolType, left, right);
}
else if (opcode is not ("VCmpClassF32" or "VCmpxClassF32") &&
opcode.EndsWith("F32", StringComparison.Ordinal))
{
@@ -3173,70 +3108,6 @@ public static partial class Gen5SpirvTranslator
sourceAllowsWrite));
}
private uint GetFloat16Source(
Gen5ShaderInstruction instruction,
int sourceIndex)
{
var operand = instruction.Sources[sourceIndex];
uint value;
if (operand.Kind == Gen5OperandKind.EncodedConstant &&
operand.Value is >= 128 and <= 192)
{
value = Float(operand.Value - 128);
}
else if (operand.Kind == Gen5OperandKind.EncodedConstant &&
operand.Value is >= 193 and <= 208)
{
value = Float(-(operand.Value - 192));
}
else if (operand.Kind == Gen5OperandKind.EncodedConstant &&
Gen5InlineConstants.TryDecode(operand.Value, out var inline))
{
value = Bitcast(_floatType, UInt(inline));
}
else
{
var raw = GetRawSource(
instruction,
sourceIndex,
applySdwaIntegerModifiers: false);
if (instruction.Control is Gen5Vop3Control control &&
(control.OperandSelect & (1u << sourceIndex)) != 0)
{
raw = ShiftRightLogical(raw, UInt(16));
}
value = Bitcast(_floatType, EmitHalfToFloat(raw));
}
uint absoluteMask = 0;
uint negateMask = 0;
switch (instruction.Control)
{
case Gen5Vop3Control control:
absoluteMask = control.AbsoluteMask;
negateMask = control.NegateMask;
break;
case Gen5SdwaControl control:
absoluteMask = control.AbsoluteMask;
negateMask = control.NegateMask;
break;
case Gen5DppControl control:
absoluteMask = control.AbsoluteMask;
negateMask = control.NegateMask;
break;
}
if ((absoluteMask & (1u << sourceIndex)) != 0)
{
value = Ext(4, _floatType, value);
}
return (negateMask & (1u << sourceIndex)) != 0
? _module.AddInstruction(SpirvOp.FNegate, _floatType, value)
: value;
}
private uint GetFloatSource(
Gen5ShaderInstruction instruction,
int sourceIndex)
@@ -3361,33 +3232,6 @@ public static partial class Gen5SpirvTranslator
_module.AddInstruction(SpirvOp.UConvert, _uintType, high));
}
private uint EmitFloat16Binary(
Gen5ShaderInstruction instruction,
uint destination,
SpirvOp operation,
bool reverse = false)
{
var left = GetFloat16Source(instruction, reverse ? 1 : 0);
var right = GetFloat16Source(instruction, reverse ? 0 : 1);
return EmitFloat16Result(
instruction,
destination,
_module.AddInstruction(operation, _floatType, left, right));
}
private uint EmitFloat16ExtBinary(
Gen5ShaderInstruction instruction,
uint destination,
uint operation) =>
EmitFloat16Result(
instruction,
destination,
Ext(
operation,
_floatType,
GetFloat16Source(instruction, 0),
GetFloat16Source(instruction, 1)));
private uint EmitFloatBinary(
Gen5ShaderInstruction instruction,
SpirvOp operation,
@@ -3909,35 +3753,6 @@ public static partial class Gen5SpirvTranslator
UInt(0));
}
private uint EmitFloat16Result(
Gen5ShaderInstruction instruction,
uint destination,
uint value)
{
var control = instruction.Control as Gen5Vop3Control;
value = (control?.OutputModifier ?? 0) switch
{
1 => _module.AddInstruction(SpirvOp.FMul, _floatType, value, Float(2)),
2 => _module.AddInstruction(SpirvOp.FMul, _floatType, value, Float(4)),
3 => _module.AddInstruction(SpirvOp.FMul, _floatType, value, Float(0.5f)),
_ => value,
};
if (control?.Clamp == true)
{
value = Ext(43, _floatType, value, Float(0), Float(1));
}
var half = EmitFloatToHalf(Bitcast(_uintType, value));
var current = LoadV(destination);
return ((control?.OperandSelect ?? 0) & 8) != 0
? BitwiseOr(
BitwiseAnd(current, UInt(0x0000_FFFF)),
ShiftLeftLogical(half, UInt(16)))
: BitwiseOr(
BitwiseAnd(current, UInt(0xFFFF_0000)),
half);
}
private uint EmitFloatResult(
Gen5ShaderInstruction instruction,
uint value)
@@ -2366,16 +2366,17 @@ public static partial class Gen5SpirvTranslator
return;
}
// GLOBAL_STORE/LOAD_DWORD(x2/x3/x4) are dword-aligned by the GCN ISA, so read/write dwords directly instead of the per-byte loop.
for (uint index = 0; index < control.DwordCount; index++)
{
var indexedDwordAddress = index == 0
? dwordAddress
: IAdd(dwordAddress, UInt(index));
StoreBufferWord(
var address = index == 0
? byteAddress
: IAdd(byteAddress, UInt(index * sizeof(uint)));
StoreBufferBytes(
bindingIndex,
indexedDwordAddress,
LoadV(control.VectorData + index));
address,
LoadV(control.VectorData + index),
sizeof(uint),
0);
}
});
return true;
@@ -2403,12 +2404,12 @@ public static partial class Gen5SpirvTranslator
for (uint index = 0; index < control.DwordCount; index++)
{
var indexedDwordAddress = index == 0
? dwordAddress
: IAdd(dwordAddress, UInt(index));
var address = index == 0
? byteAddress
: IAdd(byteAddress, UInt(index * sizeof(uint)));
StoreV(
control.VectorData + index,
LoadBufferWord(bindingIndex, indexedDwordAddress));
LoadUnalignedBufferWord(bindingIndex, address));
}
return true;
@@ -2509,16 +2510,17 @@ public static partial class Gen5SpirvTranslator
return;
}
// BUFFER_STORE/LOAD_DWORD(x2/x3/x4) are dword-aligned by the GCN ISA, same as the GLOBAL case above — no per-byte reassembly needed.
for (uint index = 0; index < control.DwordCount; index++)
{
var indexedDwordAddress = index == 0
? dwordAddress
: IAdd(dwordAddress, UInt(index));
StoreBufferWord(
var address = index == 0
? byteAddress
: IAdd(byteAddress, UInt(index * sizeof(uint)));
StoreBufferBytes(
bindingIndex,
indexedDwordAddress,
LoadV(control.VectorData + index));
address,
LoadV(control.VectorData + index),
sizeof(uint),
0);
}
});
@@ -2574,12 +2576,12 @@ public static partial class Gen5SpirvTranslator
for (uint index = 0; index < control.DwordCount; index++)
{
var indexedDwordAddress = index == 0
? dwordAddress
: IAdd(dwordAddress, UInt(index));
var address = index == 0
? byteAddress
: IAdd(byteAddress, UInt(index * sizeof(uint)));
StoreV(
control.VectorData + index,
LoadBufferWord(bindingIndex, indexedDwordAddress));
LoadUnalignedBufferWord(bindingIndex, address));
}
return true;
@@ -1015,12 +1015,6 @@ public static class Gen5ShaderTranslator
0x2F => "VCvtPkrtzF16F32",
0x30 => "VCvtPkU16U32",
0x31 => "VCvtPkI16I32",
0x32 => "VAddF16",
0x33 => "VSubF16",
0x34 => "VSubrevF16",
0x35 => "VMulF16",
0x39 => "VMaxF16",
0x3A => "VMinF16",
_ => string.Empty,
};
@@ -1092,14 +1086,6 @@ public static class Gen5ShaderTranslator
0xC5 => "VCmpNeU32",
0xC6 => "VCmpGeU32",
0xC7 => "VCmpTU32",
0xC8 => "VCmpFF16",
0xC9 => "VCmpLtF16",
0xCA => "VCmpEqF16",
0xCB => "VCmpLeF16",
0xCC => "VCmpGtF16",
0xCD => "VCmpLgF16",
0xCE => "VCmpGeF16",
0xCF => "VCmpOF16",
0xD0 => "VCmpxFU32",
0xD1 => "VCmpxLtU32",
0xD2 => "VCmpxEqU32",
@@ -1108,30 +1094,6 @@ public static class Gen5ShaderTranslator
0xD5 => "VCmpxNeU32",
0xD6 => "VCmpxGeU32",
0xD7 => "VCmpxTU32",
0xD8 => "VCmpxFF16",
0xD9 => "VCmpxLtF16",
0xDA => "VCmpxEqF16",
0xDB => "VCmpxLeF16",
0xDC => "VCmpxGtF16",
0xDD => "VCmpxLgF16",
0xDE => "VCmpxGeF16",
0xDF => "VCmpxOF16",
0xE8 => "VCmpUF16",
0xE9 => "VCmpNgeF16",
0xEA => "VCmpNlgF16",
0xEB => "VCmpNgtF16",
0xEC => "VCmpNleF16",
0xED => "VCmpNeqF16",
0xEE => "VCmpNltF16",
0xEF => "VCmpTruF16",
0xF8 => "VCmpxUF16",
0xF9 => "VCmpxNgeF16",
0xFA => "VCmpxNlgF16",
0xFB => "VCmpxNgtF16",
0xFC => "VCmpxNleF16",
0xFD => "VCmpxNeqF16",
0xFE => "VCmpxNltF16",
0xFF => "VCmpxTruF16",
_ => string.Empty,
};
@@ -102,16 +102,17 @@ public sealed class SysAbiExportAnalyzer : DiagnosticAnalyzer
ConcurrentDictionary<string, IMethodSymbol> exportsByNid)
{
var method = (IMethodSymbol)context.Symbol;
var exportAttributes = ImmutableArray.CreateBuilder<AttributeData>();
AttributeData? exportAttribute = null;
foreach (var attribute in method.GetAttributes())
{
if (SysAbiExportShape.IsSysAbiExportAttribute(attribute.AttributeClass))
{
exportAttributes.Add(attribute);
exportAttribute = attribute;
break;
}
}
if (exportAttributes.Count == 0)
if (exportAttribute is null)
{
return;
}
@@ -134,28 +135,6 @@ public sealed class SysAbiExportAnalyzer : DiagnosticAnalyzer
SysAbiDiagnostics.HandlerNotAccessible, location, methodDisplay));
}
foreach (var exportAttribute in exportAttributes)
{
AnalyzeExportAttribute(
context,
catalogNames,
exportsByNid,
method,
exportAttribute,
location,
methodDisplay);
}
}
private static void AnalyzeExportAttribute(
SymbolAnalysisContext context,
HashSet<string>? catalogNames,
ConcurrentDictionary<string, IMethodSymbol> exportsByNid,
IMethodSymbol method,
AttributeData exportAttribute,
Location location,
string methodDisplay)
{
var arguments = SysAbiExportShape.ReadArguments(exportAttribute);
var hasNid = !string.IsNullOrWhiteSpace(arguments.Nid);
var hasName = !string.IsNullOrWhiteSpace(arguments.ExportName);
@@ -209,9 +188,9 @@ public sealed class SysAbiExportAnalyzer : DiagnosticAnalyzer
}
}
if (!exportsByNid.TryAdd(effectiveNid, method))
var existing = exportsByNid.GetOrAdd(effectiveNid, method);
if (!SymbolEqualityComparer.Default.Equals(existing, method))
{
var existing = exportsByNid[effectiveNid];
context.ReportDiagnostic(Diagnostic.Create(
SysAbiDiagnostics.DuplicateNid,
location,
@@ -27,16 +27,7 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
private sealed class ExportModel : IEquatable<ExportModel>
{
public ExportModel(
string containingType,
string methodName,
SysAbiExportShape.HandlerShape shape,
string typedParameterKinds,
string libraryName,
string nid,
string exportName,
int target,
bool preferLle)
public ExportModel(string containingType, string methodName, SysAbiExportShape.HandlerShape shape, string typedParameterKinds, string libraryName, string nid, string exportName, int target)
{
ContainingType = containingType;
MethodName = methodName;
@@ -46,7 +37,6 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
Nid = nid;
ExportName = exportName;
Target = target;
PreferLle = preferLle;
}
public string ContainingType { get; }
@@ -61,7 +51,6 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
public string Nid { get; }
public string ExportName { get; }
public int Target { get; }
public bool PreferLle { get; }
public bool Equals(ExportModel? other) =>
other is not null &&
@@ -72,8 +61,7 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
LibraryName == other.LibraryName &&
Nid == other.Nid &&
ExportName == other.ExportName &&
Target == other.Target &&
PreferLle == other.PreferLle;
Target == other.Target;
public override bool Equals(object? obj) => Equals(obj as ExportModel);
@@ -85,7 +73,6 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
hash = (hash * 31) + ContainingType.GetHashCode();
hash = (hash * 31) + MethodName.GetHashCode();
hash = (hash * 31) + Nid.GetHashCode();
hash = (hash * 31) + PreferLle.GetHashCode();
return hash;
}
}
@@ -93,90 +80,80 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var exportGroups = context.SyntaxProvider
var exports = context.SyntaxProvider
.ForAttributeWithMetadataName(
AttributeMetadataName,
static (node, _) => node is MethodDeclarationSyntax,
static (attributeContext, _) => CreateModels(attributeContext))
.Where(static models => !models.IsDefaultOrEmpty)
static (attributeContext, _) => CreateModel(attributeContext))
.Where(static model => model is not null)
.Collect();
var assemblyName = context.CompilationProvider
.Select(static (compilation, _) => compilation.AssemblyName ?? "Assembly");
context.RegisterSourceOutput(
exportGroups.Combine(assemblyName),
exports.Combine(assemblyName),
static (productionContext, source) => Emit(productionContext, source.Left!, source.Right));
}
private static ImmutableArray<ExportModel> CreateModels(GeneratorAttributeSyntaxContext context)
private static ExportModel? CreateModel(GeneratorAttributeSyntaxContext context)
{
if (context.TargetSymbol is not IMethodSymbol method ||
!SysAbiExportShape.IsAccessibleFromGeneratedCode(method))
{
return ImmutableArray<ExportModel>.Empty;
return null;
}
var shape = SysAbiExportShape.Classify(method, out var typedParameterKinds);
if (shape == SysAbiExportShape.HandlerShape.Invalid)
{
return ImmutableArray<ExportModel>.Empty;
return null;
}
var models = ImmutableArray.CreateBuilder<ExportModel>(context.Attributes.Length);
foreach (var attribute in context.Attributes)
var attribute = context.Attributes[0];
var arguments = SysAbiExportShape.ReadArguments(attribute);
var nid = arguments.Nid;
var exportName = arguments.ExportName;
// Mirror ModuleManager.ResolveExportInfo: a missing NID resolves from the export
// name (algorithmically — equivalent to the runtime catalog lookup, which was
// built with the same computation); a missing name falls back to the method name.
if (string.IsNullOrWhiteSpace(nid) && !string.IsNullOrWhiteSpace(exportName))
{
var arguments = SysAbiExportShape.ReadArguments(attribute);
var nid = arguments.Nid;
var exportName = arguments.ExportName;
// Mirror ModuleManager.ResolveExportInfo: a missing NID resolves from the
// export name. A missing name falls back to the method name.
if (string.IsNullOrWhiteSpace(nid) && !string.IsNullOrWhiteSpace(exportName))
{
nid = Ps5Nid.Compute(exportName);
}
if (string.IsNullOrWhiteSpace(nid))
{
continue;
}
if (string.IsNullOrWhiteSpace(exportName))
{
exportName = method.Name;
}
var libraryName = string.IsNullOrWhiteSpace(arguments.LibraryName) ? "libKernel" : arguments.LibraryName;
models.Add(new ExportModel(
method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
method.Name,
shape,
typedParameterKinds,
libraryName,
nid!,
exportName!,
arguments.Target,
arguments.PreferLle));
nid = Ps5Nid.Compute(exportName);
}
return models.ToImmutable();
if (string.IsNullOrWhiteSpace(nid))
{
return null;
}
if (string.IsNullOrWhiteSpace(exportName))
{
exportName = method.Name;
}
var libraryName = string.IsNullOrWhiteSpace(arguments.LibraryName) ? "libKernel" : arguments.LibraryName;
return new ExportModel(
method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
method.Name,
shape,
typedParameterKinds,
libraryName,
nid!,
exportName!,
arguments.Target);
}
private static void Emit(
SourceProductionContext context,
ImmutableArray<ImmutableArray<ExportModel>> exportGroups,
ImmutableArray<ExportModel?> exports,
string assemblyName)
{
// No exports, no registry: an assembly that merely references the analyzer
// (e.g. SharpEmu.HLE itself) must not mint a colliding
// SharpEmu.Generated.SysAbiExportRegistry type.
var exportCount = 0;
foreach (var group in exportGroups)
{
exportCount += group.Length;
}
if (exportCount == 0)
if (exports.IsDefaultOrEmpty)
{
return;
}
@@ -198,23 +175,24 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
builder.AppendLine(" public static global::System.Collections.Generic.IReadOnlyList<global::SharpEmu.HLE.ExportedFunction> CreateExports(");
builder.AppendLine(" global::SharpEmu.HLE.Generation registrationGeneration)");
builder.AppendLine(" {");
builder.AppendLine($" var exports = new global::System.Collections.Generic.List<global::SharpEmu.HLE.ExportedFunction>({exportCount});");
builder.AppendLine($" var exports = new global::System.Collections.Generic.List<global::SharpEmu.HLE.ExportedFunction>({exports.Length});");
foreach (var group in exportGroups)
foreach (var export in exports)
{
foreach (var export in group)
if (export is null)
{
var function = export.Shape switch
{
SysAbiExportShape.HandlerShape.ContextOnly => $"{export.ContainingType}.{export.MethodName}",
SysAbiExportShape.HandlerShape.Parameterless => $"static _ => {export.ContainingType}.{export.MethodName}()",
_ => TypedThunk(export),
};
builder.AppendLine(
$" Add(exports, registrationGeneration, {Literal(export.LibraryName)}, {Literal(export.Nid)}, " +
$"{Literal(export.ExportName)}, (global::SharpEmu.HLE.Generation){export.Target}, " +
$"{(export.PreferLle ? "true" : "false")}, {function});");
continue;
}
var function = export.Shape switch
{
SysAbiExportShape.HandlerShape.ContextOnly => $"{export.ContainingType}.{export.MethodName}",
SysAbiExportShape.HandlerShape.Parameterless => $"static _ => {export.ContainingType}.{export.MethodName}()",
_ => TypedThunk(export),
};
builder.AppendLine(
$" Add(exports, registrationGeneration, {Literal(export.LibraryName)}, {Literal(export.Nid)}, " +
$"{Literal(export.ExportName)}, (global::SharpEmu.HLE.Generation){export.Target}, {function});");
}
builder.AppendLine(" return exports;");
@@ -227,7 +205,6 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
builder.AppendLine(" string nid,");
builder.AppendLine(" string exportName,");
builder.AppendLine(" global::SharpEmu.HLE.Generation attributeTarget,");
builder.AppendLine(" bool preferLle,");
builder.AppendLine(" global::SharpEmu.HLE.SysAbiFunction function)");
builder.AppendLine(" {");
builder.AppendLine(" var target = attributeTarget == global::SharpEmu.HLE.Generation.None ? registrationGeneration : attributeTarget;");
@@ -236,7 +213,7 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
builder.AppendLine(" return;");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" exports.Add(new global::SharpEmu.HLE.ExportedFunction(libraryName, nid, exportName, target, function, preferLle));");
builder.AppendLine(" exports.Add(new global::SharpEmu.HLE.ExportedFunction(libraryName, nid, exportName, target, function));");
builder.AppendLine(" }");
builder.AppendLine("}");
@@ -18,20 +18,18 @@ public static class SysAbiExportShape
public readonly struct Arguments
{
public Arguments(string libraryName, string nid, string exportName, int target, bool preferLle)
public Arguments(string libraryName, string nid, string exportName, int target)
{
LibraryName = libraryName;
Nid = nid;
ExportName = exportName;
Target = target;
PreferLle = preferLle;
}
public string LibraryName { get; }
public string Nid { get; }
public string ExportName { get; }
public int Target { get; }
public bool PreferLle { get; }
}
/// <summary>
@@ -207,7 +205,6 @@ public static class SysAbiExportShape
var nid = string.Empty;
var exportName = string.Empty;
var target = 0;
var preferLle = false;
foreach (var argument in attribute.NamedArguments)
{
switch (argument.Key)
@@ -224,12 +221,9 @@ public static class SysAbiExportShape
case "Target":
target = argument.Value.Value is int value ? value : 0;
break;
case "PreferLle":
preferLle = argument.Value.Value is bool boolValue && boolValue;
break;
}
}
return new Arguments(libraryName, nid, exportName, target, preferLle);
return new Arguments(libraryName, nid, exportName, target);
}
}
@@ -74,7 +74,7 @@ public sealed class AgcVertexMetadataTests
}
[Fact]
public void MergeVertexInputs_OverlaysLayoutWithoutRebasingCapture()
public void MergeVertexInputs_OverlaysFormatWithoutRebasingCapture()
{
const ulong memoryBase = 0x1_0000_0000;
var memory = new FakeCpuMemory(memoryBase, 0x2000);
@@ -114,7 +114,7 @@ public sealed class AgcVertexMetadataTests
NumberFormat: 7,
BaseAddress: sharpBase,
Stride: 16,
OffsetBytes: 12,
OffsetBytes: 0,
Data: data,
DataLength: data.Length,
DataPooled: false),
@@ -135,137 +135,6 @@ public sealed class AgcVertexMetadataTests
Assert.Equal(0x40u, merged[0].Pc);
}
[Fact]
public void MergeVertexInputs_MetadataCorrectsStaleStride40()
{
const ulong memoryBase = 0x1_0000_0000;
var memory = new FakeCpuMemory(memoryBase, 0x2000);
var ctx = new CpuContext(memory, Generation.Gen5);
const ulong semanticsAddress = memoryBase + 0x100;
const ulong attribTable = memoryBase + 0x200;
const ulong bufferTable = memoryBase + 0x300;
const ulong sharpBase = memoryBase + 0x800;
WriteUInt32(memory, semanticsAddress, 0u | (0u << 8) | (4u << 16));
WriteUInt32(memory, attribTable, 0u | (56u << 5) | (12u << 14));
WriteUInt32(memory, bufferTable, (uint)(sharpBase & 0xFFFF_FFFFUL));
WriteUInt32(memory, bufferTable + 4, (uint)(sharpBase >> 32) | (40u << 16));
var scalars = new uint[32];
scalars[4] = (uint)(attribTable & 0xFFFF_FFFFUL);
scalars[5] = (uint)(attribTable >> 32);
scalars[6] = (uint)(bufferTable & 0xFFFF_FFFFUL);
scalars[7] = (uint)(bufferTable >> 32);
var tables = new AgcVertexMetadata.VertexTableRegisters(
VertexBufferReg: 6,
VertexAttribReg: 4,
InputSemanticsCount: 1,
InputSemanticsAddress: semanticsAddress);
var data = new byte[160];
var discovered = new[]
{
new Gen5VertexInputBinding(
0x40, 0, 4, 14, 7, sharpBase, 32, 12, data, data.Length, false),
};
var merged = AgcVertexMetadata.MergeVertexInputsFromMetadata(
ctx,
scalars,
tables,
discovered);
Assert.Single(merged);
Assert.Equal(40u, merged[0].Stride);
Assert.Equal(12u, merged[0].OffsetBytes);
Assert.Equal(sharpBase, merged[0].BaseAddress);
Assert.Same(data, merged[0].Data);
Assert.Equal(0x40u, merged[0].Pc);
}
[Fact]
public void MergeVertexInputs_ConflictingMetadataOffsetDoesNotMoveBinding()
{
const ulong memoryBase = 0x1_0000_0000;
var memory = new FakeCpuMemory(memoryBase, 0x2000);
var ctx = new CpuContext(memory, Generation.Gen5);
const ulong semanticsAddress = memoryBase + 0x100;
const ulong attribTable = memoryBase + 0x200;
const ulong bufferTable = memoryBase + 0x300;
const ulong sharpBase = memoryBase + 0x800;
WriteUInt32(memory, semanticsAddress, 0u | (0u << 8) | (4u << 16));
WriteUInt32(memory, attribTable, 0u | (56u << 5) | (12u << 14));
WriteUInt32(memory, bufferTable, (uint)(sharpBase & 0xFFFF_FFFFUL));
WriteUInt32(memory, bufferTable + 4, (uint)(sharpBase >> 32) | (40u << 16));
var scalars = new uint[32];
scalars[4] = (uint)(attribTable & 0xFFFF_FFFFUL);
scalars[5] = (uint)(attribTable >> 32);
scalars[6] = (uint)(bufferTable & 0xFFFF_FFFFUL);
scalars[7] = (uint)(bufferTable >> 32);
var tables = new AgcVertexMetadata.VertexTableRegisters(
VertexBufferReg: 6,
VertexAttribReg: 4,
InputSemanticsCount: 1,
InputSemanticsAddress: semanticsAddress);
var original = new Gen5VertexInputBinding(
0x40, 0, 4, 14, 7, sharpBase, 32, 0, new byte[160], 160, false);
var merged = AgcVertexMetadata.MergeVertexInputsFromMetadata(
ctx,
scalars,
tables,
[original]);
Assert.Same(original, Assert.Single(merged));
}
[Fact]
public void MergeVertexInputs_UsesOffsetRelativeToCapturedBase()
{
const ulong memoryBase = 0x1_0000_0000;
var memory = new FakeCpuMemory(memoryBase, 0x2000);
var ctx = new CpuContext(memory, Generation.Gen5);
const ulong semanticsAddress = memoryBase + 0x100;
const ulong attribTable = memoryBase + 0x200;
const ulong bufferTable = memoryBase + 0x300;
const ulong capturedBase = memoryBase + 0x7F8;
const ulong sharpBase = memoryBase + 0x800;
WriteUInt32(memory, semanticsAddress, 0u | (0u << 8) | (4u << 16));
WriteUInt32(memory, attribTable, 0u | (56u << 5) | (12u << 14));
WriteUInt32(memory, bufferTable, (uint)(sharpBase & 0xFFFF_FFFFUL));
WriteUInt32(memory, bufferTable + 4, (uint)(sharpBase >> 32) | (40u << 16));
var scalars = new uint[32];
scalars[4] = (uint)(attribTable & 0xFFFF_FFFFUL);
scalars[5] = (uint)(attribTable >> 32);
scalars[6] = (uint)(bufferTable & 0xFFFF_FFFFUL);
scalars[7] = (uint)(bufferTable >> 32);
var tables = new AgcVertexMetadata.VertexTableRegisters(
VertexBufferReg: 6,
VertexAttribReg: 4,
InputSemanticsCount: 1,
InputSemanticsAddress: semanticsAddress);
var data = new byte[160];
var merged = AgcVertexMetadata.MergeVertexInputsFromMetadata(
ctx,
scalars,
tables,
[new Gen5VertexInputBinding(
0x40, 0, 4, 14, 7, capturedBase, 32, 20, data, data.Length, false)]);
Assert.Equal(40u, Assert.Single(merged).Stride);
Assert.Equal(20u, merged[0].OffsetBytes);
Assert.Equal(capturedBase, merged[0].BaseAddress);
Assert.Same(data, merged[0].Data);
}
[Fact]
public void MergeVertexInputs_AcceptsVertexAttribFormatEnums()
{
@@ -1,60 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Cpu.Native;
using SharpEmu.HLE;
using Xunit;
namespace SharpEmu.Libs.Tests.Cpu;
public sealed class DirectExecutionBackendLlePreferenceTests
{
[Fact]
public void ExplicitLlePreference_AllowsNonKernelRegisteredExport()
{
var export = Export("libSceNpCppWebApi", preferLle: true);
Assert.True(DirectExecutionBackend.ShouldResolveRegisteredExportViaLle(
export,
preferLleForLibc: false));
}
[Fact]
public void ExplicitLlePreference_CannotOverrideKernelHleBoundary()
{
var export = Export("libKernel", preferLle: true);
Assert.False(DirectExecutionBackend.ShouldResolveRegisteredExportViaLle(
export,
preferLleForLibc: true));
}
[Fact]
public void RegisteredExportWithoutLlePreference_RemainsHle()
{
var export = Export("libSceNpCppWebApi", preferLle: false);
Assert.False(DirectExecutionBackend.ShouldResolveRegisteredExportViaLle(
export,
preferLleForLibc: false));
}
[Fact]
public void ExistingLibcPolicy_CanStillSelectRegisteredFirmwareExport()
{
var export = Export("libSceLibcInternal", preferLle: false);
Assert.True(DirectExecutionBackend.ShouldResolveRegisteredExportViaLle(
export,
preferLleForLibc: true));
}
private static ExportedFunction Export(string libraryName, bool preferLle) =>
new(
libraryName,
"Zxa0VhQVTsk",
"sceKernelWaitSema",
Generation.Gen5,
static _ => 0,
preferLle);
}
@@ -1,75 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Cpu.Native;
using Xunit;
namespace SharpEmu.Libs.Tests.Cpu;
public sealed class TlsLoadPatchBoundaryTests
{
[Fact]
public void RejectsGtaShortJumpDisplacementAsTlsPrefix()
{
byte[] code =
[
0x90,
0xEB, 0x66,
0x66, 0x64, 0x48, 0x8B, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00,
];
Assert.True(DirectExecutionBackend.IsTlsLoadCandidateInsideShortJump(code, candidateOffset: 2));
Assert.False(DirectExecutionBackend.IsTlsLoadCandidateInsideShortJump(code, candidateOffset: 3));
}
[Fact]
public void KeepsDreamingSarahTlsInstructionAfterBackwardJnz()
{
byte[] code =
[
0x48, 0x8B, 0x1C, 0xD0,
0x4C, 0x39, 0x2B,
0x0F, 0x84, 0x10, 0x01, 0x00, 0x00,
0x48, 0xFF, 0xC2,
0x48, 0x39, 0xD1,
0x75, 0xEB,
0x66, 0x66, 0x66, 0x64, 0x48, 0x8B, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00,
];
Assert.False(DirectExecutionBackend.IsTlsLoadCandidateInsideShortJump(code, candidateOffset: 21));
}
[Theory]
[InlineData(0x70)]
[InlineData(0x7F)]
[InlineData(0xE0)]
[InlineData(0xE3)]
[InlineData(0xEB)]
public void KeepsTlsInstructionAfterRel8ControlFlow(byte opcode)
{
byte[] code =
[
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
opcode, 0xEB,
0x66, 0x64, 0x48, 0x8B, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00,
];
Assert.False(DirectExecutionBackend.IsTlsLoadCandidateInsideShortJump(code, candidateOffset: 21));
}
[Fact]
public void Rel8OpcodeByteInsidePreviousInstructionDoesNotBypassGuard()
{
byte[] code =
[
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
0x6A, 0x75,
0xEB, 0x66,
0x66, 0x64, 0x48, 0x8B, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00,
];
Assert.True(DirectExecutionBackend.IsTlsLoadCandidateInsideShortJump(code, candidateOffset: 21));
}
}
@@ -14,7 +14,7 @@ public sealed class GuestMemoryAllocatorTests
public void FreedRangesAreReusedAndCoalesced()
{
using var memory = new PhysicalVirtualMemory(new FakeHostMemory());
const ulong usableArenaSize = 0x2000_0000 - 0x1000;
const ulong usableArenaSize = 0x0100_0000 - 0x1000;
Assert.True(memory.TryAllocateGuestMemory(0x4000, 0x1000, out var first));
Assert.True(memory.TryAllocateGuestMemory(0x8000, 0x1000, out var second));
@@ -34,16 +34,6 @@ public sealed class GuestMemoryAllocatorTests
Assert.Equal(first, coalesced);
}
[Fact]
public void ArenaSupportsAllocationsBeyondLegacySixteenMiBLimit()
{
using var memory = new PhysicalVirtualMemory(new FakeHostMemory());
Assert.True(memory.TryAllocateGuestMemory(0x0100_0000, 0x1000, out var first));
Assert.True(memory.TryAllocateGuestMemory(0x0020_0000, 0x1000, out var beyondLegacyLimit));
Assert.Equal(first + 0x0100_0000, beyondLegacyLimit);
}
[Fact]
public void SegmentProtectionIsAppliedInContiguousRuns()
{
@@ -1,85 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Metal;
using Xunit;
namespace SharpEmu.ShaderCompiler.Metal.Tests;
public sealed class Gen5MslF16CompareTests
{
public static TheoryData<string> Opcodes = new()
{
"VCmpFF16",
"VCmpLtF16",
"VCmpEqF16",
"VCmpLeF16",
"VCmpGtF16",
"VCmpLgF16",
"VCmpGeF16",
"VCmpOF16",
"VCmpxFF16",
"VCmpxLtF16",
"VCmpxEqF16",
"VCmpxLeF16",
"VCmpxGtF16",
"VCmpxLgF16",
"VCmpxGeF16",
"VCmpxOF16",
"VCmpUF16",
"VCmpNgeF16",
"VCmpNlgF16",
"VCmpNgtF16",
"VCmpNleF16",
"VCmpNeqF16",
"VCmpNltF16",
"VCmpTruF16",
"VCmpxUF16",
"VCmpxNgeF16",
"VCmpxNlgF16",
"VCmpxNgtF16",
"VCmpxNleF16",
"VCmpxNeqF16",
"VCmpxNltF16",
"VCmpxTruF16",
};
[Theory]
[MemberData(nameof(Opcodes))]
public void F16CompareOpcodeLowersToMsl(string opcode)
{
var compare = new Gen5ShaderInstruction(
0,
Gen5ShaderEncoding.Vopc,
opcode,
[0u],
[Gen5Operand.Vector(0), Gen5Operand.Vector(1)],
[],
null);
var state = new Gen5ShaderState(
new Gen5ShaderProgram(0x1000, [compare]),
[],
null);
var scalars = new uint[256];
var evaluation = new Gen5ShaderEvaluation(scalars, scalars, [], []);
Assert.True(
Gen5MslTranslator.TryCompileComputeShader(
state,
evaluation,
1,
1,
1,
out var shader,
out var error),
error);
Assert.NotEmpty(shader.Source);
if (opcode is not (
"VCmpFF16" or "VCmpxFF16" or
"VCmpTruF16" or "VCmpxTruF16"))
{
Assert.Contains("half", shader.Source, StringComparison.Ordinal);
}
}
}
@@ -1,34 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using Xunit;
namespace SharpEmu.ShaderCompiler.Metal.Tests;
public sealed class MslFloat16ArithmeticTests
{
[Fact]
public void CompactFloat16ArithmeticUsesHalfOperandsAndPreservesRegisterShape()
{
var fixture = new Gen5ComputeFixture(
"compact-f16-arithmetic",
[
0x64000501,
0x66060B04,
0x680C1107,
0x6A12170A,
0x72181D0D,
0x741E2310,
0xBF810000,
],
StoreScalarResourceBase: 0,
StoreBackingBytes: 0);
var shader = Gen5ComputeFixtures.CompileOrThrow(fixture);
Assert.Contains("as_type<half>", shader.Source, StringComparison.Ordinal);
Assert.Contains("fmin(", shader.Source, StringComparison.Ordinal);
Assert.Contains("fmax(", shader.Source, StringComparison.Ordinal);
Assert.Contains("& 0xFFFF0000u", shader.Source, StringComparison.Ordinal);
}
}
@@ -1,153 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.ShaderCompiler.Vulkan;
using Xunit;
namespace SharpEmu.ShaderCompiler.Tests;
public sealed class Gen5Float16ArithmeticTests
{
private const ulong ShaderAddress = 0x1_0000_0000;
private const uint SEndpgm = 0xBF810000;
[Fact]
public void CompactFloat16ArithmeticDecodesAndCompilesWithoutNativeFloat16()
{
var program = Decode(
[
0x64000501, // v_add_f16 v0, v1, v2
0x66060B04, // v_sub_f16 v3, v4, v5
0x680C1107, // v_subrev_f16 v6, v7, v8
0x6A12170A, // v_mul_f16 v9, v10, v11
0x72181D0D, // v_max_f16 v12, v13, v14
0x741E2310, // v_min_f16 v15, v16, v17
SEndpgm,
]);
Assert.Equal(
["VAddF16", "VSubF16", "VSubrevF16", "VMulF16", "VMaxF16", "VMinF16", "SEndpgm"],
program.Instructions.Select(instruction => instruction.Opcode));
var state = new Gen5ShaderState(program, [], null);
var scalarRegisters = new uint[256];
var evaluation = new Gen5ShaderEvaluation(
scalarRegisters,
scalarRegisters,
[],
[]);
Assert.True(
Gen5SpirvTranslator.TryCompileComputeShader(
state,
evaluation,
1,
1,
1,
out var shader,
out var error),
error);
var opcodes = ReadOpcodes(shader.Spirv);
Assert.Contains((ushort)SpirvOp.FAdd, opcodes);
Assert.Contains((ushort)SpirvOp.FSub, opcodes);
Assert.Contains((ushort)SpirvOp.FMul, opcodes);
Assert.True(opcodes.Count(opcode => opcode == (ushort)SpirvOp.ExtInst) >= 2);
Assert.DoesNotContain((ushort)SpirvCapability.Float16, ReadCapabilities(shader.Spirv));
}
private static Gen5ShaderProgram Decode(IReadOnlyList<uint> words)
{
var memory = new TestCpuMemory(ShaderAddress, words.Count * sizeof(uint));
var bytes = new byte[words.Count * sizeof(uint)];
for (var index = 0; index < words.Count; index++)
{
BinaryPrimitives.WriteUInt32LittleEndian(
bytes.AsSpan(index * sizeof(uint)),
words[index]);
}
Assert.True(memory.TryWrite(ShaderAddress, bytes));
var context = new CpuContext(memory, Generation.Gen5);
Assert.True(
Gen5ShaderTranslator.TryDecodeProgram(
context,
ShaderAddress,
out var program,
out var error),
error);
return program;
}
private static IReadOnlyList<ushort> ReadOpcodes(byte[] spirv) =>
ReadInstructions(spirv)
.Select(instruction => instruction.Opcode)
.ToArray();
private static IReadOnlyList<ushort> ReadCapabilities(byte[] spirv) =>
ReadInstructions(spirv)
.Where(instruction => instruction.Opcode == (ushort)SpirvOp.Capability)
.Select(instruction => (ushort)instruction.FirstOperand)
.ToArray();
private static IReadOnlyList<(ushort Opcode, uint FirstOperand)> ReadInstructions(
byte[] spirv)
{
Assert.Equal(0x07230203u, BinaryPrimitives.ReadUInt32LittleEndian(spirv));
var instructions = new List<(ushort Opcode, uint FirstOperand)>();
for (var offset = 5 * sizeof(uint); offset < spirv.Length;)
{
var header = BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset));
var wordCount = checked((int)(header >> 16));
Assert.InRange(wordCount, 1, (spirv.Length - offset) / sizeof(uint));
var firstOperand = wordCount > 1
? BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset + sizeof(uint)))
: 0;
instructions.Add(((ushort)header, firstOperand));
offset += wordCount * sizeof(uint);
}
return instructions;
}
private sealed class TestCpuMemory(ulong baseAddress, int size) : ICpuMemory
{
private readonly byte[] _storage = new byte[size];
public bool TryRead(ulong virtualAddress, Span<byte> destination)
{
if (!TryResolve(virtualAddress, destination.Length, out var offset))
{
return false;
}
_storage.AsSpan(offset, destination.Length).CopyTo(destination);
return true;
}
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
{
if (!TryResolve(virtualAddress, source.Length, out var offset))
{
return false;
}
source.CopyTo(_storage.AsSpan(offset, source.Length));
return true;
}
private bool TryResolve(ulong address, int length, out int offset)
{
offset = 0;
if (address < baseAddress || address - baseAddress > int.MaxValue)
{
return false;
}
offset = (int)(address - baseAddress);
return offset <= _storage.Length - length;
}
}
}
@@ -1,154 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Vulkan;
using Xunit;
namespace SharpEmu.ShaderCompiler.Tests;
public sealed class Gen5VopcF16Tests
{
private const ulong ShaderAddress = 0x1_0000_0000;
private const uint SEndpgm = 0xBF810000;
public static TheoryData<uint, string> Opcodes = new()
{
{ 0xC8, "VCmpFF16" },
{ 0xC9, "VCmpLtF16" },
{ 0xCA, "VCmpEqF16" },
{ 0xCB, "VCmpLeF16" },
{ 0xCC, "VCmpGtF16" },
{ 0xCD, "VCmpLgF16" },
{ 0xCE, "VCmpGeF16" },
{ 0xCF, "VCmpOF16" },
{ 0xD8, "VCmpxFF16" },
{ 0xD9, "VCmpxLtF16" },
{ 0xDA, "VCmpxEqF16" },
{ 0xDB, "VCmpxLeF16" },
{ 0xDC, "VCmpxGtF16" },
{ 0xDD, "VCmpxLgF16" },
{ 0xDE, "VCmpxGeF16" },
{ 0xDF, "VCmpxOF16" },
{ 0xE8, "VCmpUF16" },
{ 0xE9, "VCmpNgeF16" },
{ 0xEA, "VCmpNlgF16" },
{ 0xEB, "VCmpNgtF16" },
{ 0xEC, "VCmpNleF16" },
{ 0xED, "VCmpNeqF16" },
{ 0xEE, "VCmpNltF16" },
{ 0xEF, "VCmpTruF16" },
{ 0xF8, "VCmpxUF16" },
{ 0xF9, "VCmpxNgeF16" },
{ 0xFA, "VCmpxNlgF16" },
{ 0xFB, "VCmpxNgtF16" },
{ 0xFC, "VCmpxNleF16" },
{ 0xFD, "VCmpxNeqF16" },
{ 0xFE, "VCmpxNltF16" },
{ 0xFF, "VCmpxTruF16" },
};
[Theory]
[MemberData(nameof(Opcodes))]
public void F16CompareOpcodeDecodes(uint opcode, string expectedName)
{
var memory = new TestCpuMemory(ShaderAddress, 0x100);
Span<byte> shader = stackalloc byte[2 * sizeof(uint)];
var word = (0x3Eu << 25) | (opcode << 17) | (1u << 9);
BinaryPrimitives.WriteUInt32LittleEndian(shader, word);
BinaryPrimitives.WriteUInt32LittleEndian(shader[sizeof(uint)..], SEndpgm);
Assert.True(memory.TryWrite(ShaderAddress, shader));
var ctx = new CpuContext(memory, Generation.Gen5);
Assert.True(
Gen5ShaderTranslator.TryDecodeProgram(
ctx,
ShaderAddress,
out var program,
out var error),
error);
var instruction = Assert.Single(
program.Instructions,
candidate => candidate.Encoding == Gen5ShaderEncoding.Vopc);
Assert.Equal(expectedName, instruction.Opcode);
}
[Theory]
[MemberData(nameof(Opcodes))]
public void F16CompareOpcodeLowersToSpirv(uint _, string opcode)
{
var compare = new Gen5ShaderInstruction(
0,
Gen5ShaderEncoding.Vopc,
opcode,
[0u],
[Gen5Operand.Vector(0), Gen5Operand.Vector(1)],
[],
null);
var state = new Gen5ShaderState(
new Gen5ShaderProgram(ShaderAddress, [compare]),
[],
null);
var scalars = new uint[256];
var evaluation = new Gen5ShaderEvaluation(scalars, scalars, [], []);
Assert.True(
Gen5SpirvTranslator.TryCompileComputeShader(
state,
evaluation,
1,
1,
1,
out var shader,
out var error),
error);
Assert.NotEmpty(shader.Spirv);
}
private sealed class TestCpuMemory(ulong baseAddress, int size) : ICpuMemory
{
private readonly byte[] _storage = new byte[size];
public bool TryRead(ulong virtualAddress, Span<byte> destination)
{
if (!TryResolve(virtualAddress, destination.Length, out var offset))
{
return false;
}
_storage.AsSpan(offset, destination.Length).CopyTo(destination);
return true;
}
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
{
if (!TryResolve(virtualAddress, source.Length, out var offset))
{
return false;
}
source.CopyTo(_storage.AsSpan(offset, source.Length));
return true;
}
private bool TryResolve(ulong virtualAddress, int length, out int offset)
{
offset = 0;
if (virtualAddress < baseAddress)
{
return false;
}
var relative = virtualAddress - baseAddress;
if (relative + (ulong)length > (ulong)_storage.Length)
{
return false;
}
offset = (int)relative;
return true;
}
}
}
@@ -53,40 +53,6 @@ public sealed class SysAbiExportAnalyzerTests
AssertSingle(diagnostics, "SHEM001");
}
[Fact]
public void DuplicateNidOnTheSameMultiAttributeHandlerIsReported()
{
var diagnostics = Analyze("""
using SharpEmu.HLE;
public static class Exports
{
[SysAbiExport(Nid = "Zxa0VhQVTsk", ExportName = "sceKernelWaitSema")]
[SysAbiExport(Nid = "Zxa0VhQVTsk", ExportName = "sceKernelWaitSema")]
public static int Shared(CpuContext ctx) => 0;
}
""");
AssertSingle(diagnostics, "SHEM001");
}
[Fact]
public void EveryAttributeOnAMultiAttributeHandlerIsAnalyzed()
{
var diagnostics = Analyze("""
using SharpEmu.HLE;
public static class Exports
{
[SysAbiExport(Nid = "Zxa0VhQVTsk", ExportName = "sceKernelWaitSema")]
[SysAbiExport(Nid = "not_a_nid")]
public static int Shared(CpuContext ctx) => 0;
}
""");
AssertSingle(diagnostics, "SHEM002");
}
[Fact]
public void MalformedNidIsReported()
{
@@ -36,11 +36,6 @@ public sealed class SysAbiExportGeneratorTests
// Guest string marshalling: the thunk reads the pointer before the handler.
[SysAbiExport(Nid = "1G3lF1Gg1k8", ExportName = "sceKernelOpen")]
public static int KernelOpen(CpuContext ctx, [GuestCString(4096)] string path, int flags) => 0;
// A single fail-closed handler may back a catalog of LLE-preferred exports.
[SysAbiExport(Nid = "5fbPUzoA2fM", ExportName = "sceLleFirst", Target = Generation.Gen5, LibraryName = "libSceLle", PreferLle = true)]
[SysAbiExport(Nid = "L9NfM+f4f1Y", ExportName = "sceLleSecond", Target = Generation.Gen5, LibraryName = "libSceLle", PreferLle = true)]
public static int LleFallback(CpuContext ctx) => -1;
}
""";
@@ -128,22 +123,6 @@ public sealed class SysAbiExportGeneratorTests
Assert.Contains("(target & registrationGeneration) == 0", generated, StringComparison.Ordinal);
}
[Fact]
public void MultipleLlePreferredAttributesShareOneFailClosedHandler()
{
var (_, generated) = RoslynTestHost.RunGenerator(RoslynTestHost.Compile(HandlerSource));
Assert.Contains("\"5fbPUzoA2fM\"", generated, StringComparison.Ordinal);
Assert.Contains("\"L9NfM+f4f1Y\"", generated, StringComparison.Ordinal);
Assert.Equal(
2,
generated.Split("global::TestExports.SampleExports.LleFallback", StringSplitOptions.None).Length - 1);
Assert.Contains(
", true, global::TestExports.SampleExports.LleFallback",
generated,
StringComparison.Ordinal);
}
[Fact]
public void AssemblyWithoutExportsEmitsNoRegistry()
{
+1 -73
View File
@@ -17,9 +17,7 @@
// failure that must stay loud. Any unexpected outcome makes the tool exit
// non-zero, so it can gate scripts/CI.
//
// Usage:
// SharpEmu.Tools.ShaderDump [output-directory]
// SharpEmu.Tools.ShaderDump --inspect <shader.bin> [byte-count]
// Usage: SharpEmu.Tools.ShaderDump [output-directory]
using System.Buffers.Binary;
using SharpEmu.HLE;
@@ -28,76 +26,6 @@ using SharpEmu.ShaderCompiler.Vulkan;
const ulong ProgramAddress = 0x100000;
if (args.Length >= 1 && string.Equals(args[0], "--inspect", StringComparison.Ordinal))
{
if (args.Length is < 2 or > 3)
{
Console.Error.WriteLine(
"Usage: SharpEmu.Tools.ShaderDump --inspect <shader.bin> [byte-count]");
Environment.ExitCode = 2;
return;
}
var inputPath = Path.GetFullPath(args[1]);
var input = File.ReadAllBytes(inputPath);
var requestedByteCount = input.Length;
if (args.Length >= 3)
{
var value = args[2];
requestedByteCount = value.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
? Convert.ToInt32(value[2..], 16)
: Convert.ToInt32(value, 10);
}
if (requestedByteCount <= 0 ||
requestedByteCount > input.Length ||
requestedByteCount % sizeof(uint) != 0)
{
Console.Error.WriteLine(
$"Invalid byte count {requestedByteCount}; expected a positive, " +
$"4-byte-aligned value no larger than {input.Length}.");
Environment.ExitCode = 2;
return;
}
var words = new uint[requestedByteCount / sizeof(uint)];
for (var index = 0; index < words.Length; index++)
{
words[index] = BinaryPrimitives.ReadUInt32LittleEndian(
input.AsSpan(index * sizeof(uint), sizeof(uint)));
}
var memory = new FakeMemory();
memory.AddRegion(ProgramAddress, words);
var ctx = new CpuContext(memory, Generation.Gen5);
if (!Gen5ShaderTranslator.TryDecodeProgram(
ctx,
ProgramAddress,
out var program,
out var decodeError))
{
Console.Error.WriteLine($"Decode failed: {decodeError}");
Environment.ExitCode = 1;
return;
}
Console.WriteLine(
$"path={inputPath} bytes={requestedByteCount} " +
$"instructions={program!.Instructions.Count}");
foreach (var instruction in program.Instructions)
{
Console.WriteLine(
$"pc=0x{instruction.Pc:X} enc={instruction.Encoding} " +
$"op={instruction.Opcode} " +
$"words={string.Join(',', instruction.Words.Select(word => $"{word:X8}"))} " +
$"src={string.Join('/', instruction.Sources)} " +
$"dst={string.Join('/', instruction.Destinations)} " +
$"control={instruction.Control?.ToString() ?? "-"}");
}
return;
}
(string Name, bool ExpectTranslate, uint[] Words)[] testPrograms =
[
("fmac", true, [