mirror of
https://github.com/par274/sharpemu.git
synced 2026-08-28 12:20:43 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f857bef20 |
@@ -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;
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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, [
|
||||
|
||||
Reference in New Issue
Block a user