mirror of
https://github.com/par274/sharpemu.git
synced 2026-08-28 12:20:43 +08:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b226dca3df | |||
| 51e5480049 | |||
| 600fcde637 | |||
| 807aad18b3 | |||
| f4f36b558f | |||
| 2b8ef7d8fa | |||
| f8a826ec1b | |||
| 4a7a45d1b3 | |||
| 3a744c991e | |||
| 35a28f0143 | |||
| e79a1cc70a | |||
| a2241d0e83 | |||
| fe6521f617 | |||
| 034ddcc092 | |||
| d9b599a1fd |
@@ -8,6 +8,7 @@ using System.Diagnostics;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using Iced.Intel;
|
||||||
using SharpEmu.Core.Cpu;
|
using SharpEmu.Core.Cpu;
|
||||||
using SharpEmu.Core.Cpu.Debugging;
|
using SharpEmu.Core.Cpu.Debugging;
|
||||||
using SharpEmu.Core.Loader;
|
using SharpEmu.Core.Loader;
|
||||||
@@ -1638,18 +1639,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
if (_moduleManager.TryGetExport(nid, out ExportedFunction export))
|
if (_moduleManager.TryGetExport(nid, out ExportedFunction export))
|
||||||
{
|
{
|
||||||
if (IsKernelLibrary(export.LibraryName))
|
var preferLleForLibc = IsLibcLibrary(export.LibraryName) && PreferLleForLibcExport(export.Name);
|
||||||
|
if (!ShouldResolveRegisteredExportViaLle(export, preferLleForLibc))
|
||||||
{
|
{
|
||||||
if (_logAllImports)
|
if (_logAllImports && IsKernelLibrary(export.LibraryName))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][DEBUG] TryResolveDirectImportTarget: {nid} ({export.LibraryName}:{export.Name}) -> HLE (kernel library)");
|
Console.Error.WriteLine($"[LOADER][DEBUG] TryResolveDirectImportTarget: {nid} ({export.LibraryName}:{export.Name}) -> HLE (kernel library)");
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!IsLibcLibrary(export.LibraryName) || !PreferLleForLibcExport(export.Name))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (TryResolveRuntimeSymbolAddress(nid, out var value2) && IsDirectImportTargetUsable(value2))
|
if (TryResolveRuntimeSymbolAddress(nid, out var value2) && IsDirectImportTargetUsable(value2))
|
||||||
{
|
{
|
||||||
targetAddress = value2;
|
targetAddress = value2;
|
||||||
@@ -1703,6 +1701,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
return false;
|
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)
|
private static bool IsHlePreferredNid(string nid)
|
||||||
{
|
{
|
||||||
return string.Equals(nid, "QrZZdJ8XsX0", StringComparison.Ordinal) ||
|
return string.Equals(nid, "QrZZdJ8XsX0", StringComparison.Ordinal) ||
|
||||||
@@ -3200,7 +3206,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
{
|
{
|
||||||
nint address = (nint)(ptr + i);
|
nint address = (nint)(ptr + i);
|
||||||
int remainingBytes = scanBytes - i;
|
int remainingBytes = scanBytes - i;
|
||||||
if (TryPatchTlsLoadInstruction(address, ptr + i, remainingBytes))
|
if (TryPatchTlsLoadInstruction(address, ptr + i, remainingBytes, i))
|
||||||
{
|
{
|
||||||
num3++;
|
num3++;
|
||||||
}
|
}
|
||||||
@@ -3343,13 +3349,19 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe bool TryPatchTlsLoadInstruction(nint address, byte* source, int availableLength)
|
private unsafe bool TryPatchTlsLoadInstruction(nint address, byte* source, int availableLength, int regionOffset)
|
||||||
{
|
{
|
||||||
if (availableLength < MinTlsPatchInstructionBytes)
|
if (availableLength < MinTlsPatchInstructionBytes)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var region = new ReadOnlySpan<byte>(source - regionOffset, regionOffset + availableLength);
|
||||||
|
if (IsTlsLoadCandidateInsideShortJump(region, regionOffset))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var offset = 0;
|
var offset = 0;
|
||||||
while (offset < availableLength && source[offset] == 0x66)
|
while (offset < availableLength && source[offset] == 0x66)
|
||||||
{
|
{
|
||||||
@@ -3402,6 +3414,76 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
return PatchTlsLoadInstruction(address, instructionLength, destinationRegister);
|
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)
|
private unsafe bool PatchTlsLoadInstruction(nint address, int instructionLength, int destinationRegister)
|
||||||
{
|
{
|
||||||
uint flNewProtect = default(uint);
|
uint flNewProtect = default(uint);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using SharpEmu.Core.Loader;
|
using SharpEmu.Core.Loader;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
@@ -18,7 +19,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
private readonly object _allocationSearchHintGate = new();
|
private readonly object _allocationSearchHintGate = new();
|
||||||
private readonly List<MemoryRegion> _regions = new();
|
private readonly List<MemoryRegion> _regions = new();
|
||||||
private readonly Dictionary<(ulong DesiredAddress, ulong Alignment, bool Executable), ulong> _allocationSearchHints = new();
|
private readonly Dictionary<(ulong DesiredAddress, ulong Alignment, bool Executable), ulong> _allocationSearchHints = new();
|
||||||
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
|
private readonly ConcurrentDictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
@@ -28,7 +29,13 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
private const ulong PageSize = 0x1000;
|
private const ulong PageSize = 0x1000;
|
||||||
private const ulong HostAllocationGranularity = 0x10000;
|
private const ulong HostAllocationGranularity = 0x10000;
|
||||||
private const ulong GuestAllocationArenaAddress = 0x00006000_0000_0000;
|
private const ulong GuestAllocationArenaAddress = 0x00006000_0000_0000;
|
||||||
private const ulong GuestAllocationArenaSize = 0x0100_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 GuestAllocationArenaStartOffset = PageSize;
|
private const ulong GuestAllocationArenaStartOffset = PageSize;
|
||||||
private const ulong LargeDataReserveThreshold = 0x4000_0000UL; // 1 GiB
|
private const ulong LargeDataReserveThreshold = 0x4000_0000UL; // 1 GiB
|
||||||
private const ulong FullCommitRegionLimit = 4UL << 30;
|
private const ulong FullCommitRegionLimit = 4UL << 30;
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
{
|
||||||
|
"_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."
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
{
|
||||||
|
"_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,7 +5,13 @@ namespace SharpEmu.HLE;
|
|||||||
|
|
||||||
public sealed class ExportedFunction
|
public sealed class ExportedFunction
|
||||||
{
|
{
|
||||||
public ExportedFunction(string libraryName, string nid, string name, Generation target, SysAbiFunction function)
|
public ExportedFunction(
|
||||||
|
string libraryName,
|
||||||
|
string nid,
|
||||||
|
string name,
|
||||||
|
Generation target,
|
||||||
|
SysAbiFunction function,
|
||||||
|
bool preferLle = false)
|
||||||
{
|
{
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(libraryName);
|
ArgumentException.ThrowIfNullOrWhiteSpace(libraryName);
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
|
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
|
||||||
@@ -17,6 +23,7 @@ public sealed class ExportedFunction
|
|||||||
Name = name;
|
Name = name;
|
||||||
Target = target;
|
Target = target;
|
||||||
Function = function;
|
Function = function;
|
||||||
|
PreferLle = preferLle;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string LibraryName { get; }
|
public string LibraryName { get; }
|
||||||
@@ -28,4 +35,10 @@ public sealed class ExportedFunction
|
|||||||
public Generation Target { get; }
|
public Generation Target { get; }
|
||||||
|
|
||||||
public SysAbiFunction Function { 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;
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
|
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
||||||
public sealed class SysAbiExportAttribute : Attribute
|
public sealed class SysAbiExportAttribute : Attribute
|
||||||
{
|
{
|
||||||
public string LibraryName { get; set; } = "libKernel";
|
public string LibraryName { get; set; } = "libKernel";
|
||||||
@@ -13,4 +13,11 @@ public sealed class SysAbiExportAttribute : Attribute
|
|||||||
public string ExportName { get; set; } = string.Empty;
|
public string ExportName { get; set; } = string.Empty;
|
||||||
|
|
||||||
public Generation Target { get; set; } = Generation.None;
|
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,6 +118,29 @@ public static partial class AgcExports
|
|||||||
// Multiple producers can share one target label; last-writer-wins would
|
// Multiple producers can share one target label; last-writer-wins would
|
||||||
// starve waits on the others.
|
// starve waits on the others.
|
||||||
private static readonly Dictionary<ulong, List<ulong>> _cbReleaseMemTargets = new();
|
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.
|
// header -> {ring base, write cursor} of the last submitted slice.
|
||||||
// Submissions stay cursor-bounded since rings aren't zeroed. Lap
|
// Submissions stay cursor-bounded since rings aren't zeroed. Lap
|
||||||
// distinguishes a stale cursor from a previous pass over the same base.
|
// distinguishes a stale cursor from a previous pass over the same base.
|
||||||
@@ -1095,15 +1118,20 @@ public static partial class AgcExports
|
|||||||
private const uint CbColor0Base = 0x318;
|
private const uint CbColor0Base = 0x318;
|
||||||
private const uint CbColorRegisterStride = 15;
|
private const uint CbColorRegisterStride = 15;
|
||||||
private const uint CbColor0Info = 0x31C;
|
private const uint CbColor0Info = 0x31C;
|
||||||
|
private const uint CbColor0Cmask = 0x31F;
|
||||||
private const uint CbColor0ClearWord0 = 0x323;
|
private const uint CbColor0ClearWord0 = 0x323;
|
||||||
private const uint CbColor0ClearWord1 = 0x324;
|
private const uint CbColor0ClearWord1 = 0x324;
|
||||||
|
private const uint CbColor0DccBase = 0x325;
|
||||||
private const uint CbColor0BaseExt = 0x390;
|
private const uint CbColor0BaseExt = 0x390;
|
||||||
|
private const uint CbColor0CmaskBaseExt = 0x398;
|
||||||
|
private const uint CbColor0DccBaseExt = 0x3A8;
|
||||||
private const uint CbColor0Attrib2 = 0x3B0;
|
private const uint CbColor0Attrib2 = 0x3B0;
|
||||||
private const uint CbColor0Attrib3 = 0x3B8;
|
private const uint CbColor0Attrib3 = 0x3B8;
|
||||||
// CB_COLORn_INFO.DCC_ENABLE (gc_10_1_0_sh_mask.h). On GFX10 the legacy
|
// 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,
|
// FAST_CLEAR and COMPRESSION bits stay clear because DCC, not CMASK,
|
||||||
// carries the compression.
|
// carries the compression.
|
||||||
private const uint CbColorInfoDccEnableMask = 1u << 28;
|
private const uint CbColorInfoDccEnableMask = 1u << 28;
|
||||||
|
private const uint CbColorInfoFastClearEnableMask = 1u << 12;
|
||||||
private const uint CbBlend0Control = 0x1E0;
|
private const uint CbBlend0Control = 0x1E0;
|
||||||
private const uint PaScModeCntl0 = 0x292;
|
private const uint PaScModeCntl0 = 0x292;
|
||||||
// GFX10 DB context registers (register byte address minus 0x28000, / 4).
|
// GFX10 DB context registers (register byte address minus 0x28000, / 4).
|
||||||
@@ -1123,8 +1151,8 @@ public static partial class AgcExports
|
|||||||
private const uint EsUserDataRegister = 0xCC;
|
private const uint EsUserDataRegister = 0xCC;
|
||||||
private const uint ComputeUserDataRegister = 0x240;
|
private const uint ComputeUserDataRegister = 0x240;
|
||||||
private const uint NggUserDataScalarRegisterBase = 8;
|
private const uint NggUserDataScalarRegisterBase = 8;
|
||||||
private const uint Gen5TextureFormatR8G8B8A8Unorm = 10;
|
internal const uint Gen5TextureFormatR8G8B8A8Unorm = 10;
|
||||||
private const uint Gen5TextureFormatR16G16B16A16Float = 12;
|
internal const uint Gen5TextureFormatR16G16B16A16Float = 12;
|
||||||
private const uint Gen5TextureType1D = 8;
|
private const uint Gen5TextureType1D = 8;
|
||||||
private const uint Gen5TextureType2D = 9;
|
private const uint Gen5TextureType2D = 9;
|
||||||
private const uint Gen5TextureType3D = 10;
|
private const uint Gen5TextureType3D = 10;
|
||||||
@@ -5083,6 +5111,10 @@ public static partial class AgcExports
|
|||||||
|
|
||||||
if (op == ItNop && register == RDmaData && length >= 7)
|
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(
|
ApplySubmittedDmaData(
|
||||||
ctx,
|
ctx,
|
||||||
gpuState,
|
gpuState,
|
||||||
@@ -5243,6 +5275,7 @@ public static partial class AgcExports
|
|||||||
{
|
{
|
||||||
TraceFramePacketSummary(state);
|
TraceFramePacketSummary(state);
|
||||||
SyncCpuWrittenGuestImages(ctx);
|
SyncCpuWrittenGuestImages(ctx);
|
||||||
|
GpuWaitRegistry.AdvanceFrame();
|
||||||
if (!TryReadUInt32(ctx, currentAddress + 4, out var videoOutHandle) ||
|
if (!TryReadUInt32(ctx, currentAddress + 4, out var videoOutHandle) ||
|
||||||
!TryReadUInt32(ctx, currentAddress + 8, out var displayBufferIndexRaw) ||
|
!TryReadUInt32(ctx, currentAddress + 8, out var displayBufferIndexRaw) ||
|
||||||
!TryReadUInt32(ctx, currentAddress + 12, out var flipMode) ||
|
!TryReadUInt32(ctx, currentAddress + 12, out var flipMode) ||
|
||||||
@@ -6264,6 +6297,13 @@ public static partial class AgcExports
|
|||||||
ulong byteCount,
|
ulong byteCount,
|
||||||
uint? fillValue)
|
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(
|
var hasImage = GuestGpu.Current.TryGetGuestImageExtent(
|
||||||
destinationAddress,
|
destinationAddress,
|
||||||
out var width,
|
out var width,
|
||||||
@@ -6478,6 +6518,25 @@ public static partial class AgcExports
|
|||||||
var targetAddress = destinationAddress +
|
var targetAddress = destinationAddress +
|
||||||
(incrementAddress ? (ulong)index * sizeof(uint) : 0);
|
(incrementAddress ? (ulong)index * sizeof(uint) : 0);
|
||||||
wroteData = TryWriteUInt32(ctx, targetAddress, values[index]);
|
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)
|
if (tracePacket)
|
||||||
@@ -7053,7 +7112,13 @@ public static partial class AgcExports
|
|||||||
|
|
||||||
if (hasCurrent && GpuWaitRegistry.Compare(waiter, currentValue))
|
if (hasCurrent && GpuWaitRegistry.Compare(waiter, currentValue))
|
||||||
{
|
{
|
||||||
return false; // already satisfied — keep parsing
|
// 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_gpuWaitSuspendEnabled)
|
if (!_gpuWaitSuspendEnabled)
|
||||||
@@ -8044,7 +8109,8 @@ public static partial class AgcExports
|
|||||||
var hasPsInputEna = state.CxRegisters.TryGetValue(SpiPsInputEna, out var psInputEna);
|
var hasPsInputEna = state.CxRegisters.TryGetValue(SpiPsInputEna, out var psInputEna);
|
||||||
var hasPsInputAddr = state.CxRegisters.TryGetValue(SpiPsInputAddr, out var psInputAddr);
|
var hasPsInputAddr = state.CxRegisters.TryGetValue(SpiPsInputAddr, out var psInputAddr);
|
||||||
state.UcRegisters.TryGetValue(VgtPrimitiveType, out var primitiveType);
|
state.UcRegisters.TryGetValue(VgtPrimitiveType, out var primitiveType);
|
||||||
var renderTargets = GetRenderTargets(state.CxRegisters);
|
var renderTargets = GetRenderTargets(state.CxRegisters);
|
||||||
|
TrackCmaskAddresses(state.CxRegisters, renderTargets);
|
||||||
var drawSequence = ++gpuState.WorkSequence;
|
var drawSequence = ++gpuState.WorkSequence;
|
||||||
if (state.PendingTargetlessDraw is { } stalePendingDraw)
|
if (state.PendingTargetlessDraw is { } stalePendingDraw)
|
||||||
{
|
{
|
||||||
@@ -8064,6 +8130,31 @@ public static partial class AgcExports
|
|||||||
if (TryGetCbColorControlMode(state.CxRegisters, out var cbMode) &&
|
if (TryGetCbColorControlMode(state.CxRegisters, out var cbMode) &&
|
||||||
IsCbMetadataColorMode(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))
|
if (_traceAgcShader || ShouldTraceHotPath(ref _cbMetadataSkipTraceCount))
|
||||||
{
|
{
|
||||||
TraceAgcShader(
|
TraceAgcShader(
|
||||||
@@ -8278,6 +8369,34 @@ public static partial class AgcExports
|
|||||||
return;
|
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();
|
var firstTarget = translatedDraw.RenderTargets.FirstOrDefault();
|
||||||
if (firstTarget.Address != 0)
|
if (firstTarget.Address != 0)
|
||||||
{
|
{
|
||||||
@@ -9570,6 +9689,193 @@ public static partial class AgcExports
|
|||||||
CoversClipSpace(vertexInputs, vertexCount);
|
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>
|
/// <summary>
|
||||||
/// True when the draw's float32x3 position stream spans the full clip
|
/// True when the draw's float32x3 position stream spans the full clip
|
||||||
/// rectangle, i.e. x and y both reach -1 and +1.
|
/// rectangle, i.e. x and y both reach -1 and +1.
|
||||||
@@ -9982,7 +10288,7 @@ public static partial class AgcExports
|
|||||||
private static readonly HashSet<ulong> _sampledRenderTargets = new();
|
private static readonly HashSet<ulong> _sampledRenderTargets = new();
|
||||||
private static readonly object _renderTargetProbeGate = new();
|
private static readonly object _renderTargetProbeGate = new();
|
||||||
private static long _renderTargetSampleTraceCount;
|
private static long _renderTargetSampleTraceCount;
|
||||||
private static long _indirectDrawProbeCount;
|
private static long _indirectDrawProbeCount;
|
||||||
private static long _indirectDrawEmitCount;
|
private static long _indirectDrawEmitCount;
|
||||||
private static long _indirectDrawEmitRejectCount;
|
private static long _indirectDrawEmitRejectCount;
|
||||||
private static long _indirectMultiProbeCount;
|
private static long _indirectMultiProbeCount;
|
||||||
@@ -12174,15 +12480,18 @@ public static partial class AgcExports
|
|||||||
|
|
||||||
if (dispatchEndX == 0 || dispatchEndY == 0 || dispatchEndZ == 0)
|
if (dispatchEndX == 0 || dispatchEndY == 0 || dispatchEndZ == 0)
|
||||||
{
|
{
|
||||||
// Indirect dispatches read their dimensions from a guest buffer a
|
// For indirect dispatches (both absolute and base), zero dimensions are a valid outcome
|
||||||
// prior GPU dispatch fills. Zero here means that producer has not run
|
// of GPU culling passes (0 workgroups). VulkanVideoPresenter handles groupCount = 0 as a clean no-op.
|
||||||
// yet — signal the caller to suspend on the dims buffer and retry,
|
if (opcode == ItDispatchIndirect || dispatchSource is "absolute-indirect" or "base-indirect")
|
||||||
// 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)
|
|
||||||
{
|
{
|
||||||
indirectDimsRetryAddress = dimensionsAddress;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
return RejectComputeDispatch(
|
return RejectComputeDispatch(
|
||||||
@@ -12465,6 +12774,10 @@ public static partial class AgcExports
|
|||||||
shaderAddress,
|
shaderAddress,
|
||||||
binding.Opcode);
|
binding.Opcode);
|
||||||
|
|
||||||
|
// Check if this compute shader writes to a CMASK address
|
||||||
|
// (shadPS4's IsComputeMetaClear logic)
|
||||||
|
CheckCmaskWrite(texture.Address, gpuState);
|
||||||
|
|
||||||
TraceAgcShader(
|
TraceAgcShader(
|
||||||
$"agc.compute_writer addr=0x{texture.Address:X16} " +
|
$"agc.compute_writer addr=0x{texture.Address:X16} " +
|
||||||
$"fmt={texture.Format} num={texture.NumberType} tile={texture.TileMode} " +
|
$"fmt={texture.Format} num={texture.NumberType} tile={texture.TileMode} " +
|
||||||
@@ -13060,11 +13373,14 @@ public static partial class AgcExports
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
GuestImageWriteTracker.Track(
|
GuestImageWriteTracker.Track(
|
||||||
destinationAddress,
|
destinationAddress,
|
||||||
(ulong)output.Length,
|
(ulong)output.Length,
|
||||||
VulkanVideoPresenter.CurrentGuestWorkSequenceForDiagnostics,
|
VulkanVideoPresenter.CurrentGuestWorkSequenceForDiagnostics,
|
||||||
"agc.constant-fill");
|
"agc.constant-fill");
|
||||||
|
|
||||||
|
VulkanVideoPresenter.RequestGuestColorClear(destinationAddress);
|
||||||
|
|
||||||
},
|
},
|
||||||
$"constant_fill dst=0x{destinationAddress:X16} bytes={output.Length}");
|
$"constant_fill dst=0x{destinationAddress:X16} bytes={output.Length}");
|
||||||
description =
|
description =
|
||||||
|
|||||||
@@ -238,10 +238,12 @@ internal static class AgcVertexMetadata
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Patch IR-discovered fetches from the attrib table onto the V# format/offset.
|
/// Patch IR-discovered fetches from the attrib table onto the V# layout.
|
||||||
/// Prefer 1:1 Location pairing when counts match on one interleaved stream
|
/// Prefer 1:1 Location pairing when counts match on one interleaved stream
|
||||||
/// (GTA UI glyphs). Otherwise match by stride + byte offset. Never rebases
|
/// (GTA UI glyphs). Otherwise match by the effective captured byte offset.
|
||||||
/// BaseAddress/Data/Location/Pc/PerInstance.
|
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static IReadOnlyList<Gen5VertexInputBinding> MergeVertexInputsFromMetadata(
|
internal static IReadOnlyList<Gen5VertexInputBinding> MergeVertexInputsFromMetadata(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
@@ -269,13 +271,13 @@ internal static class AgcVertexMetadata
|
|||||||
var changed = false;
|
var changed = false;
|
||||||
foreach (var input in discovered)
|
foreach (var input in discovered)
|
||||||
{
|
{
|
||||||
if (!TryMatchMetadataResource(input, resources, usedResources, out var resource, out var fillOffset))
|
if (!TryMatchMetadataResource(input, resources, usedResources, out var resource))
|
||||||
{
|
{
|
||||||
merged.Add(input);
|
merged.Add(input);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var refined = ApplyMetadataFormat(input, resource, fillOffset);
|
var refined = ApplyMetadataFormat(input, resource);
|
||||||
changed |= refined != input;
|
changed |= refined != input;
|
||||||
merged.Add(refined);
|
merged.Add(refined);
|
||||||
}
|
}
|
||||||
@@ -286,7 +288,7 @@ internal static class AgcVertexMetadata
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// When discovery and metadata describe the same interleaved stream with
|
/// When discovery and metadata describe the same interleaved stream with
|
||||||
/// equal attribute counts, pair by sorted Location (semantic order).
|
/// equal attribute counts, pair by sorted Location (semantic order).
|
||||||
/// Keeps each binding's Pc/Location for SPIR-V; overlays format + offset.
|
/// Keeps each binding's Pc/Location/address for SPIR-V and overlays layout.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static bool TryMergeByLocationPairing(
|
private static bool TryMergeByLocationPairing(
|
||||||
IReadOnlyList<Gen5VertexInputBinding> discovered,
|
IReadOnlyList<Gen5VertexInputBinding> discovered,
|
||||||
@@ -299,34 +301,36 @@ internal static class AgcVertexMetadata
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var orderedInputs = discovered.OrderBy(static input => input.Location).ToArray();
|
var orderedInputs = discovered
|
||||||
|
.Select(static (input, originalIndex) => (Input: input, OriginalIndex: originalIndex))
|
||||||
|
.OrderBy(static entry => entry.Input.Location)
|
||||||
|
.ThenBy(static entry => entry.OriginalIndex)
|
||||||
|
.ToArray();
|
||||||
var orderedResources = resources.OrderBy(static resource => resource.Location).ToArray();
|
var orderedResources = resources.OrderBy(static resource => resource.Location).ToArray();
|
||||||
var streamBase = orderedResources[0].SharpBase;
|
var streamBase = orderedResources[0].SharpBase;
|
||||||
var streamStride = orderedResources[0].Stride;
|
var streamStride = orderedResources[0].Stride;
|
||||||
for (var index = 0; index < orderedResources.Length; index++)
|
for (var index = 0; index < orderedResources.Length; index++)
|
||||||
{
|
{
|
||||||
var resource = orderedResources[index];
|
var resource = orderedResources[index];
|
||||||
var input = orderedInputs[index];
|
var input = orderedInputs[index].Input;
|
||||||
if (resource.SharpBase != streamBase ||
|
if (resource.SharpBase != streamBase ||
|
||||||
resource.Stride != streamStride ||
|
resource.Stride != streamStride ||
|
||||||
(input.Stride != 0 && input.Stride != streamStride) ||
|
!TryGetMetadataOffset(input, resource, out var resolvedOffset) ||
|
||||||
!IsSameVertexStream(input, resource))
|
resolvedOffset != input.OffsetBytes)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var byPc = new Dictionary<uint, Gen5VertexInputBinding>(discovered.Count);
|
var result = discovered.ToArray();
|
||||||
var changed = false;
|
var changed = false;
|
||||||
for (var index = 0; index < orderedInputs.Length; index++)
|
for (var index = 0; index < orderedInputs.Length; index++)
|
||||||
{
|
{
|
||||||
var input = orderedInputs[index];
|
var input = orderedInputs[index].Input;
|
||||||
var resource = orderedResources[index];
|
var resource = orderedResources[index];
|
||||||
var fillOffset = input.BaseAddress == resource.SharpBase ||
|
var refined = ApplyMetadataFormat(input, resource);
|
||||||
IsAddressInsideCapturedSpan(input, resource.SharpBase);
|
|
||||||
var refined = ApplyMetadataFormat(input, resource, fillOffset);
|
|
||||||
changed |= refined != input;
|
changed |= refined != input;
|
||||||
byPc[input.Pc] = refined;
|
result[orderedInputs[index].OriginalIndex] = refined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!changed)
|
if (!changed)
|
||||||
@@ -334,20 +338,13 @@ internal static class AgcVertexMetadata
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = new Gen5VertexInputBinding[discovered.Count];
|
|
||||||
for (var index = 0; index < discovered.Count; index++)
|
|
||||||
{
|
|
||||||
result[index] = byPc[discovered[index].Pc];
|
|
||||||
}
|
|
||||||
|
|
||||||
merged = result;
|
merged = result;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Gen5VertexInputBinding ApplyMetadataFormat(
|
private static Gen5VertexInputBinding ApplyMetadataFormat(
|
||||||
Gen5VertexInputBinding input,
|
Gen5VertexInputBinding input,
|
||||||
MetadataVertexResource resource,
|
MetadataVertexResource resource)
|
||||||
bool fillOffsetBytes)
|
|
||||||
{
|
{
|
||||||
var components = input.ComponentCount != 0 &&
|
var components = input.ComponentCount != 0 &&
|
||||||
input.ComponentCount < resource.ComponentCount
|
input.ComponentCount < resource.ComponentCount
|
||||||
@@ -359,7 +356,8 @@ internal static class AgcVertexMetadata
|
|||||||
DataFormat = resource.DataFormat,
|
DataFormat = resource.DataFormat,
|
||||||
NumberFormat = resource.NumberFormat,
|
NumberFormat = resource.NumberFormat,
|
||||||
ComponentCount = components,
|
ComponentCount = components,
|
||||||
OffsetBytes = fillOffsetBytes ? resource.OffsetBytes : input.OffsetBytes,
|
Stride = resource.Stride,
|
||||||
|
PerInstance = resource.PerInstance,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,14 +432,11 @@ internal static class AgcVertexMetadata
|
|||||||
Gen5VertexInputBinding input,
|
Gen5VertexInputBinding input,
|
||||||
IReadOnlyList<MetadataVertexResource> resources,
|
IReadOnlyList<MetadataVertexResource> resources,
|
||||||
bool[] usedResources,
|
bool[] usedResources,
|
||||||
out MetadataVertexResource resource,
|
out MetadataVertexResource resource)
|
||||||
out bool fillOffsetBytes)
|
|
||||||
{
|
{
|
||||||
resource = default;
|
resource = default;
|
||||||
fillOffsetBytes = false;
|
|
||||||
var bestScore = int.MinValue;
|
var bestScore = int.MinValue;
|
||||||
var bestIndex = -1;
|
var bestIndex = -1;
|
||||||
var bestFillOffset = false;
|
|
||||||
for (var index = 0; index < resources.Count; index++)
|
for (var index = 0; index < resources.Count; index++)
|
||||||
{
|
{
|
||||||
if (usedResources[index])
|
if (usedResources[index])
|
||||||
@@ -450,100 +445,64 @@ internal static class AgcVertexMetadata
|
|||||||
}
|
}
|
||||||
|
|
||||||
var candidate = resources[index];
|
var candidate = resources[index];
|
||||||
if (candidate.Stride != 0 &&
|
if (!TryGetMetadataOffset(input, candidate, out var resolvedOffset) ||
|
||||||
input.Stride != 0 &&
|
resolvedOffset != input.OffsetBytes)
|
||||||
candidate.Stride != input.Stride)
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IsSameVertexStream(input, candidate))
|
// The effective captured offset (including any base rebasing done
|
||||||
{
|
// while coalescing adjacent vertex streams) is the primary key.
|
||||||
continue;
|
var score = 400;
|
||||||
}
|
|
||||||
|
|
||||||
var attrAddress = candidate.SharpBase + candidate.OffsetBytes;
|
// Discovery can carry a stale inferred stride (notably 32 for a
|
||||||
var score = int.MinValue;
|
// real stride-40 interleaved layout). Prefer a matching stride
|
||||||
var fillOffset = false;
|
// when candidates are otherwise equivalent, but do not reject an
|
||||||
|
// unambiguous metadata match: the V# descriptor is authoritative.
|
||||||
// Post-capture interleaved: shared BaseAddress, distinct OffsetBytes.
|
if (input.Stride == candidate.Stride)
|
||||||
if (input.OffsetBytes == candidate.OffsetBytes &&
|
|
||||||
(input.BaseAddress == candidate.SharpBase ||
|
|
||||||
IsAddressInsideCapturedSpan(input, candidate.SharpBase)))
|
|
||||||
{
|
{
|
||||||
score = 400;
|
score += 25;
|
||||||
}
|
|
||||||
// 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)
|
if (score > bestScore)
|
||||||
{
|
{
|
||||||
bestScore = score;
|
bestScore = score;
|
||||||
bestIndex = index;
|
bestIndex = index;
|
||||||
bestFillOffset = fillOffset;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Require an offset-aware match. Bare SharpBase ties (score 250) are
|
if (bestIndex < 0)
|
||||||
// only accepted when a single unused resource remains for that stream.
|
|
||||||
if (bestIndex < 0 || bestScore < 300)
|
|
||||||
{
|
{
|
||||||
if (bestIndex < 0 || bestScore < 250)
|
return false;
|
||||||
{
|
|
||||||
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;
|
usedResources[bestIndex] = true;
|
||||||
resource = resources[bestIndex];
|
resource = resources[bestIndex];
|
||||||
fillOffsetBytes = bestFillOffset;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsSameVertexStream(
|
private static bool TryGetMetadataOffset(
|
||||||
Gen5VertexInputBinding input,
|
Gen5VertexInputBinding input,
|
||||||
MetadataVertexResource resource)
|
MetadataVertexResource resource,
|
||||||
|
out uint offsetBytes)
|
||||||
{
|
{
|
||||||
if (input.BaseAddress == resource.SharpBase ||
|
offsetBytes = input.OffsetBytes;
|
||||||
input.BaseAddress == resource.SharpBase + resource.OffsetBytes)
|
if (resource.SharpBase < input.BaseAddress ||
|
||||||
|
(!IsAddressInsideCapturedSpan(input, resource.SharpBase) &&
|
||||||
|
resource.SharpBase != input.BaseAddress))
|
||||||
{
|
{
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return IsAddressInsideCapturedSpan(input, resource.SharpBase);
|
var relativeBase = resource.SharpBase - input.BaseAddress;
|
||||||
|
var resolvedOffset = relativeBase + resource.OffsetBytes;
|
||||||
|
if (resolvedOffset > uint.MaxValue)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
offsetBytes = (uint)resolvedOffset;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsAddressInsideCapturedSpan(
|
private static bool IsAddressInsideCapturedSpan(
|
||||||
@@ -553,28 +512,6 @@ internal static class AgcVertexMetadata
|
|||||||
address >= input.BaseAddress &&
|
address >= input.BaseAddress &&
|
||||||
address < input.BaseAddress + (ulong)input.DataLength;
|
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>
|
/// <summary>
|
||||||
/// Attrib-table format
|
/// Attrib-table format
|
||||||
/// fields are VertexAttribFormat; V# / Vulkan paths need BufferFormat.
|
/// fields are VertexAttribFormat; V# / Vulkan paths need BufferFormat.
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ internal static class GpuWaitRegistry
|
|||||||
// cycle forever even though a real producer did signal it. Keyed by (memory,
|
// cycle forever even though a real producer did signal it. Keyed by (memory,
|
||||||
// address) so distinct guest processes never alias.
|
// address) so distinct guest processes never alias.
|
||||||
private static readonly Dictionary<(object, ulong), ulong> _lastProduced = new();
|
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)
|
private static object? Canonicalize(object? memory)
|
||||||
@@ -71,6 +75,34 @@ internal static class GpuWaitRegistry
|
|||||||
return memory;
|
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
|
public static int Count
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -576,6 +608,7 @@ internal static class GpuWaitRegistry
|
|||||||
}
|
}
|
||||||
|
|
||||||
_lastProduced[(memory, address)] = value;
|
_lastProduced[(memory, address)] = value;
|
||||||
|
_labelFrameIds[(memory, address)] = System.Threading.Volatile.Read(ref _currentFrameId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return LatchSatisfiedByValue(memory, address, value);
|
return LatchSatisfiedByValue(memory, address, value);
|
||||||
|
|||||||
@@ -5884,6 +5884,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
private void ExecuteOrderedGuestFlip(VulkanOrderedGuestFlip work)
|
private void ExecuteOrderedGuestFlip(VulkanOrderedGuestFlip work)
|
||||||
{
|
{
|
||||||
|
Agc.AgcExports.MarkAllSurfacesCleared();
|
||||||
FlushBatchedGuestCommands();
|
FlushBatchedGuestCommands();
|
||||||
_guestImages.TryGetValue(work.Address, out var source);
|
_guestImages.TryGetValue(work.Address, out var source);
|
||||||
if (_deviceLost ||
|
if (_deviceLost ||
|
||||||
@@ -12770,6 +12771,18 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
targets[index].Initialized = false;
|
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 &&
|
if (work.Targets[index].Address != 0 &&
|
||||||
TakeGuestImageInitialData(work.Targets[index].Address) is { } initialData &&
|
TakeGuestImageInitialData(work.Targets[index].Address) is { } initialData &&
|
||||||
!targets[index].Initialized &&
|
!targets[index].Initialized &&
|
||||||
@@ -13031,13 +13044,31 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
&toDepthAttachment);
|
&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(
|
BeginTranslatedRenderPass(
|
||||||
renderPass,
|
renderPass,
|
||||||
framebuffer,
|
framebuffer,
|
||||||
extent,
|
extent,
|
||||||
colorAttachmentCount: targets.Length,
|
colorAttachmentCount: targets.Length,
|
||||||
hasDepthAttachment: depth is not null && !clearDepthSeparately,
|
hasDepthAttachment: depth is not null && !clearDepthSeparately,
|
||||||
clearDepth: depth?.ClearDepth ?? 1f);
|
clearDepth: depth?.ClearDepth ?? 1f,
|
||||||
|
colorClearValues: metaClearValues);
|
||||||
RecordTranslatedDrawInPass(resources, extent);
|
RecordTranslatedDrawInPass(resources, extent);
|
||||||
_vk.CmdEndRenderPass(_commandBuffer);
|
_vk.CmdEndRenderPass(_commandBuffer);
|
||||||
|
|
||||||
@@ -13828,16 +13859,10 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
existing.LogicalDepth == depth &&
|
existing.LogicalDepth == depth &&
|
||||||
existing.Type == type &&
|
existing.Type == type &&
|
||||||
existing.MipLevels == mipLevels &&
|
existing.MipLevels == mipLevels &&
|
||||||
|
(!requiresStorage || existing.SupportsStorageUsage) &&
|
||||||
(exactFormatMatch ||
|
(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.IsCpuBacked = false;
|
||||||
existing.CpuContentFingerprint = 0;
|
existing.CpuContentFingerprint = 0;
|
||||||
if (existing.RenderPass.Handle == 0 &&
|
if (existing.RenderPass.Handle == 0 &&
|
||||||
@@ -13870,14 +13895,9 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
if (existing.Width == target.Width &&
|
if (existing.Width == target.Width &&
|
||||||
existing.Height == target.Height &&
|
existing.Height == target.Height &&
|
||||||
existing.MipLevels == mipLevels &&
|
existing.MipLevels == mipLevels &&
|
||||||
|
(!requiresStorage || existing.SupportsStorageUsage) &&
|
||||||
IsCompatibleViewFormat(existing.Format, format))
|
IsCompatibleViewFormat(existing.Format, format))
|
||||||
{
|
{
|
||||||
if (requiresStorage && !existing.SupportsStorageUsage)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"Guest image 0x{target.Address:X16} was created without storage usage.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_traceGuestImageEvents)
|
if (_traceGuestImageEvents)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
@@ -13952,50 +13972,52 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
{
|
{
|
||||||
if (requiresStorage && !retained.SupportsStorageUsage)
|
if (requiresStorage && !retained.SupportsStorageUsage)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
// Do not reuse retained image if it lacks required storage usage
|
||||||
$"Retained guest image 0x{target.Address:X16} was created without storage usage.");
|
DestroyGuestImage(retained);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
retained.IsCpuBacked = false;
|
|
||||||
retained.CpuContentFingerprint = 0;
|
|
||||||
_guestImages.Add(target.Address, retained);
|
|
||||||
var retainedByteCount = GetTextureByteCount(
|
|
||||||
target.Format,
|
|
||||||
target.Width,
|
|
||||||
target.Height,
|
|
||||||
depth);
|
|
||||||
lock (_gate)
|
|
||||||
{
|
{
|
||||||
_cpuBackedUploadGenerations.Remove(target.Address);
|
retained.IsCpuBacked = false;
|
||||||
_guestImageExtents[target.Address] = (
|
retained.CpuContentFingerprint = 0;
|
||||||
|
_guestImages.Add(target.Address, retained);
|
||||||
|
var retainedByteCount = GetTextureByteCount(
|
||||||
|
target.Format,
|
||||||
target.Width,
|
target.Width,
|
||||||
target.Height,
|
target.Height,
|
||||||
retainedByteCount);
|
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
|
// Arm the exact extent the flip/acquire sync path would read
|
||||||
// back, budgeted by bytes rather than by resolution: the old
|
// back, budgeted by bytes rather than by resolution: the old
|
||||||
// 1920x1080 cap left every 4K surface permanently
|
// 1920x1080 cap left every 4K surface permanently
|
||||||
// un-invalidated, so a guest CPU rewrite of one was never
|
// un-invalidated, so a guest CPU rewrite of one was never
|
||||||
// reflected and the sample served stale bytes.
|
// reflected and the sample served stale bytes.
|
||||||
if (ShouldTrackGuestImageWrites(retainedByteCount))
|
if (ShouldTrackGuestImageWrites(retainedByteCount))
|
||||||
{
|
{
|
||||||
SharpEmu.HLE.GuestImageWriteTracker.Track(
|
SharpEmu.HLE.GuestImageWriteTracker.Track(
|
||||||
target.Address,
|
target.Address,
|
||||||
retainedByteCount,
|
retainedByteCount,
|
||||||
CurrentGuestWorkSequenceForDiagnostics,
|
CurrentGuestWorkSequenceForDiagnostics,
|
||||||
"vulkan.render-target");
|
"vulkan.render-target");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_traceGuestImageEvents)
|
if (_traceGuestImageEvents)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[GIMG] retained addr=0x{target.Address:X} " +
|
$"[GIMG] retained addr=0x{target.Address:X} " +
|
||||||
$"{target.Width}x{target.Height} fmt={format} " +
|
$"{target.Width}x{target.Height} fmt={format} " +
|
||||||
$"initialized={retained.Initialized}");
|
$"initialized={retained.Initialized}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return retained;
|
return retained;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var imageInfo = new ImageCreateInfo
|
var imageInfo = new ImageCreateInfo
|
||||||
@@ -17697,20 +17719,67 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_vk.CmdEndRenderPass(_commandBuffer);
|
_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(
|
private void BeginTranslatedRenderPass(
|
||||||
RenderPass renderPass,
|
RenderPass renderPass,
|
||||||
Framebuffer framebuffer,
|
Framebuffer framebuffer,
|
||||||
Extent2D extent,
|
Extent2D extent,
|
||||||
int colorAttachmentCount = 1,
|
int colorAttachmentCount = 1,
|
||||||
bool hasDepthAttachment = false,
|
bool hasDepthAttachment = false,
|
||||||
float clearDepth = 1f)
|
float clearDepth = 1f,
|
||||||
|
ClearColorValue[]? colorClearValues = null)
|
||||||
{
|
{
|
||||||
colorAttachmentCount = Math.Max(colorAttachmentCount, 1);
|
colorAttachmentCount = Math.Max(colorAttachmentCount, 1);
|
||||||
var clearValueCount = colorAttachmentCount + (hasDepthAttachment ? 1 : 0);
|
var clearValueCount = colorAttachmentCount + (hasDepthAttachment ? 1 : 0);
|
||||||
var clearValues = stackalloc ClearValue[clearValueCount];
|
var clearValues = stackalloc ClearValue[clearValueCount];
|
||||||
for (var index = 0; index < colorAttachmentCount; index++)
|
for (var index = 0; index < colorAttachmentCount; index++)
|
||||||
{
|
{
|
||||||
clearValues[index] = default;
|
clearValues[index] = colorClearValues is not null &&
|
||||||
|
index < colorClearValues.Length
|
||||||
|
? new ClearValue { Color = colorClearValues[index] }
|
||||||
|
: default;
|
||||||
}
|
}
|
||||||
// Reverse-Z is not assumed; clear depth to 1.0 (far) so a standard
|
// Reverse-Z is not assumed; clear depth to 1.0 (far) so a standard
|
||||||
// LessOrEqual/Less test keeps the nearest fragment.
|
// LessOrEqual/Less test keeps the nearest fragment.
|
||||||
|
|||||||
@@ -144,11 +144,35 @@ public static partial class Gen5MslTranslator
|
|||||||
|
|
||||||
// ---- float arithmetic ----
|
// ---- float arithmetic ----
|
||||||
"VAddF32" => FloatResult(instruction, $"{F(instruction, 0)} + {F(instruction, 1)}"),
|
"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)}"),
|
"VSubF32" => FloatResult(instruction, $"{F(instruction, 0)} - {F(instruction, 1)}"),
|
||||||
"VSubrevF32" => FloatResult(instruction, $"{F(instruction, 1)} - {F(instruction, 0)}"),
|
"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)}"),
|
"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)})"),
|
"VMinF32" => FloatResult(instruction, $"fmin({F(instruction, 0)}, {F(instruction, 1)})"),
|
||||||
"VMaxF32" => FloatResult(instruction, $"fmax({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
|
// The decoder normalizes mk/ak literal placement, so every MAD/FMA
|
||||||
// form is fma(src0, src1, src2) exactly like the SPIR-V translator.
|
// form is fma(src0, src1, src2) exactly like the SPIR-V translator.
|
||||||
"VFmaF32" or "VMadF32" or "VMadAkF32" or "VMadMkF32" or "VFmaAkF32" or "VFmaMkF32" =>
|
"VFmaF32" or "VMadF32" or "VMadAkF32" or "VMadMkF32" or "VFmaAkF32" or "VFmaMkF32" =>
|
||||||
@@ -578,23 +602,46 @@ public static partial class Gen5MslTranslator
|
|||||||
{
|
{
|
||||||
condition = EmitCompareClass(instruction);
|
condition = EmitCompareClass(instruction);
|
||||||
}
|
}
|
||||||
else if (opcode is "VCmpTruF32" or "VCmpxTruF32" or "VCmpTI32" or "VCmpTU32")
|
else if (opcode is
|
||||||
|
"VCmpTruF32" or "VCmpxTruF32" or
|
||||||
|
"VCmpTruF16" or "VCmpxTruF16" or
|
||||||
|
"VCmpTI32" or "VCmpTU32")
|
||||||
{
|
{
|
||||||
condition = "true";
|
condition = "true";
|
||||||
}
|
}
|
||||||
else if (opcode is "VCmpFF32" or "VCmpxFF32" or "VCmpFI32" or "VCmpFU32")
|
else if (opcode is
|
||||||
|
"VCmpFF32" or "VCmpxFF32" or
|
||||||
|
"VCmpFF16" or "VCmpxFF16" or
|
||||||
|
"VCmpFI32" or "VCmpFU32")
|
||||||
{
|
{
|
||||||
condition = "false";
|
condition = "false";
|
||||||
}
|
}
|
||||||
else if (opcode is "VCmpOF32" or "VCmpxOF32")
|
else if (opcode is
|
||||||
|
"VCmpOF32" or "VCmpxOF32" or
|
||||||
|
"VCmpOF16" or "VCmpxOF16")
|
||||||
{
|
{
|
||||||
condition = $"(!isnan({F(instruction, 0)}) && !isnan({F(instruction, 1)}))";
|
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}))";
|
||||||
}
|
}
|
||||||
else if (opcode is "VCmpUF32" or "VCmpxUF32")
|
else if (opcode is
|
||||||
|
"VCmpUF32" or "VCmpxUF32" or
|
||||||
|
"VCmpUF16" or "VCmpxUF16")
|
||||||
{
|
{
|
||||||
condition = $"(isnan({F(instruction, 0)}) || isnan({F(instruction, 1)}))";
|
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}))";
|
||||||
}
|
}
|
||||||
else if (opcode.EndsWith("F32", StringComparison.Ordinal))
|
else if (opcode.EndsWith("F32", StringComparison.Ordinal) ||
|
||||||
|
opcode.EndsWith("F16", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
// Ordered compares are the plain C operators (false on NaN);
|
// Ordered compares are the plain C operators (false on NaN);
|
||||||
// the Nxx forms are their unordered negations (true on NaN).
|
// the Nxx forms are their unordered negations (true on NaN).
|
||||||
@@ -620,7 +667,13 @@ public static partial class Gen5MslTranslator
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var comparison = $"({F(instruction, 0)} {op} {F(instruction, 1)})";
|
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})";
|
||||||
condition = unordered ? $"(!{comparison})" : comparison;
|
condition = unordered ? $"(!{comparison})" : comparison;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -1570,6 +1623,80 @@ public static partial class Gen5MslTranslator
|
|||||||
return expression;
|
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>
|
/// <summary>
|
||||||
/// Wraps a float expression with VOP3/SDWA output modifiers and clamp,
|
/// Wraps a float expression with VOP3/SDWA output modifiers and clamp,
|
||||||
/// then bitcasts back to the register file's uint domain.
|
/// then bitcasts back to the register file's uint domain.
|
||||||
|
|||||||
@@ -340,21 +340,43 @@ public static partial class Gen5SpirvTranslator
|
|||||||
case "VAddF32":
|
case "VAddF32":
|
||||||
result = EmitFloatBinary(instruction, SpirvOp.FAdd);
|
result = EmitFloatBinary(instruction, SpirvOp.FAdd);
|
||||||
break;
|
break;
|
||||||
|
case "VAddF16":
|
||||||
|
result = EmitFloat16Binary(instruction, destination, SpirvOp.FAdd);
|
||||||
|
break;
|
||||||
case "VSubF32":
|
case "VSubF32":
|
||||||
result = EmitFloatBinary(instruction, SpirvOp.FSub);
|
result = EmitFloatBinary(instruction, SpirvOp.FSub);
|
||||||
break;
|
break;
|
||||||
case "VSubrevF32":
|
case "VSubrevF32":
|
||||||
result = EmitFloatBinary(instruction, SpirvOp.FSub, reverse: true);
|
result = EmitFloatBinary(instruction, SpirvOp.FSub, reverse: true);
|
||||||
break;
|
break;
|
||||||
|
case "VSubF16":
|
||||||
|
result = EmitFloat16Binary(instruction, destination, SpirvOp.FSub);
|
||||||
|
break;
|
||||||
|
case "VSubrevF16":
|
||||||
|
result = EmitFloat16Binary(
|
||||||
|
instruction,
|
||||||
|
destination,
|
||||||
|
SpirvOp.FSub,
|
||||||
|
reverse: true);
|
||||||
|
break;
|
||||||
case "VMulF32":
|
case "VMulF32":
|
||||||
result = EmitFloatBinary(instruction, SpirvOp.FMul);
|
result = EmitFloatBinary(instruction, SpirvOp.FMul);
|
||||||
break;
|
break;
|
||||||
|
case "VMulF16":
|
||||||
|
result = EmitFloat16Binary(instruction, destination, SpirvOp.FMul);
|
||||||
|
break;
|
||||||
case "VMinF32":
|
case "VMinF32":
|
||||||
result = EmitFloatExtBinary(instruction, 37);
|
result = EmitFloatExtBinary(instruction, 37);
|
||||||
break;
|
break;
|
||||||
case "VMaxF32":
|
case "VMaxF32":
|
||||||
result = EmitFloatExtBinary(instruction, 40);
|
result = EmitFloatExtBinary(instruction, 40);
|
||||||
break;
|
break;
|
||||||
|
case "VMinF16":
|
||||||
|
result = EmitFloat16ExtBinary(instruction, destination, 37);
|
||||||
|
break;
|
||||||
|
case "VMaxF16":
|
||||||
|
result = EmitFloat16ExtBinary(instruction, destination, 40);
|
||||||
|
break;
|
||||||
case "VMadF32":
|
case "VMadF32":
|
||||||
case "VFmaF32":
|
case "VFmaF32":
|
||||||
case "VMadMkF32":
|
case "VMadMkF32":
|
||||||
@@ -1609,29 +1631,72 @@ public static partial class Gen5SpirvTranslator
|
|||||||
condition,
|
condition,
|
||||||
SignedClass(0x020, 0x040, zero));
|
SignedClass(0x020, 0x040, zero));
|
||||||
}
|
}
|
||||||
else if (opcode is "VCmpFF32" or "VCmpxFF32" or "VCmpFI32" or "VCmpFU32")
|
else if (opcode is
|
||||||
|
"VCmpFF32" or "VCmpxFF32" or
|
||||||
|
"VCmpFF16" or "VCmpxFF16" or
|
||||||
|
"VCmpFI32" or "VCmpFU32")
|
||||||
{
|
{
|
||||||
condition = _module.ConstantBool(false);
|
condition = _module.ConstantBool(false);
|
||||||
}
|
}
|
||||||
else if (opcode is "VCmpTruF32" or "VCmpxTruF32" or "VCmpTI32" or "VCmpTU32")
|
else if (opcode is
|
||||||
|
"VCmpTruF32" or "VCmpxTruF32" or
|
||||||
|
"VCmpTruF16" or "VCmpxTruF16" or
|
||||||
|
"VCmpTI32" or "VCmpTU32")
|
||||||
{
|
{
|
||||||
condition = _module.ConstantBool(true);
|
condition = _module.ConstantBool(true);
|
||||||
}
|
}
|
||||||
else if (opcode is
|
else if (opcode is
|
||||||
"VCmpOF32" or "VCmpxOF32" or
|
"VCmpOF32" or "VCmpxOF32" or
|
||||||
"VCmpUF32" or "VCmpxUF32")
|
"VCmpUF32" or "VCmpxUF32" or
|
||||||
|
"VCmpOF16" or "VCmpxOF16" or
|
||||||
|
"VCmpUF16" or "VCmpxUF16")
|
||||||
{
|
{
|
||||||
var left = GetFloatSource(instruction, 0);
|
var isHalf = opcode.EndsWith("F16", StringComparison.Ordinal);
|
||||||
var right = GetFloatSource(instruction, 1);
|
var left = isHalf
|
||||||
|
? GetFloat16Source(instruction, 0)
|
||||||
|
: GetFloatSource(instruction, 0);
|
||||||
|
var right = isHalf
|
||||||
|
? GetFloat16Source(instruction, 1)
|
||||||
|
: GetFloatSource(instruction, 1);
|
||||||
var unordered = _module.AddInstruction(
|
var unordered = _module.AddInstruction(
|
||||||
SpirvOp.LogicalOr,
|
SpirvOp.LogicalOr,
|
||||||
_boolType,
|
_boolType,
|
||||||
_module.AddInstruction(SpirvOp.IsNan, _boolType, left),
|
_module.AddInstruction(SpirvOp.IsNan, _boolType, left),
|
||||||
_module.AddInstruction(SpirvOp.IsNan, _boolType, right));
|
_module.AddInstruction(SpirvOp.IsNan, _boolType, right));
|
||||||
condition = opcode is "VCmpUF32" or "VCmpxUF32"
|
condition = opcode is
|
||||||
|
"VCmpUF32" or "VCmpxUF32" or
|
||||||
|
"VCmpUF16" or "VCmpxUF16"
|
||||||
? unordered
|
? unordered
|
||||||
: _module.AddInstruction(SpirvOp.LogicalNot, _boolType, 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") &&
|
else if (opcode is not ("VCmpClassF32" or "VCmpxClassF32") &&
|
||||||
opcode.EndsWith("F32", StringComparison.Ordinal))
|
opcode.EndsWith("F32", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
@@ -3108,6 +3173,70 @@ public static partial class Gen5SpirvTranslator
|
|||||||
sourceAllowsWrite));
|
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(
|
private uint GetFloatSource(
|
||||||
Gen5ShaderInstruction instruction,
|
Gen5ShaderInstruction instruction,
|
||||||
int sourceIndex)
|
int sourceIndex)
|
||||||
@@ -3232,6 +3361,33 @@ public static partial class Gen5SpirvTranslator
|
|||||||
_module.AddInstruction(SpirvOp.UConvert, _uintType, high));
|
_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(
|
private uint EmitFloatBinary(
|
||||||
Gen5ShaderInstruction instruction,
|
Gen5ShaderInstruction instruction,
|
||||||
SpirvOp operation,
|
SpirvOp operation,
|
||||||
@@ -3753,6 +3909,35 @@ public static partial class Gen5SpirvTranslator
|
|||||||
UInt(0));
|
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(
|
private uint EmitFloatResult(
|
||||||
Gen5ShaderInstruction instruction,
|
Gen5ShaderInstruction instruction,
|
||||||
uint value)
|
uint value)
|
||||||
|
|||||||
@@ -2366,17 +2366,16 @@ public static partial class Gen5SpirvTranslator
|
|||||||
return;
|
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++)
|
for (uint index = 0; index < control.DwordCount; index++)
|
||||||
{
|
{
|
||||||
var address = index == 0
|
var indexedDwordAddress = index == 0
|
||||||
? byteAddress
|
? dwordAddress
|
||||||
: IAdd(byteAddress, UInt(index * sizeof(uint)));
|
: IAdd(dwordAddress, UInt(index));
|
||||||
StoreBufferBytes(
|
StoreBufferWord(
|
||||||
bindingIndex,
|
bindingIndex,
|
||||||
address,
|
indexedDwordAddress,
|
||||||
LoadV(control.VectorData + index),
|
LoadV(control.VectorData + index));
|
||||||
sizeof(uint),
|
|
||||||
0);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
@@ -2404,12 +2403,12 @@ public static partial class Gen5SpirvTranslator
|
|||||||
|
|
||||||
for (uint index = 0; index < control.DwordCount; index++)
|
for (uint index = 0; index < control.DwordCount; index++)
|
||||||
{
|
{
|
||||||
var address = index == 0
|
var indexedDwordAddress = index == 0
|
||||||
? byteAddress
|
? dwordAddress
|
||||||
: IAdd(byteAddress, UInt(index * sizeof(uint)));
|
: IAdd(dwordAddress, UInt(index));
|
||||||
StoreV(
|
StoreV(
|
||||||
control.VectorData + index,
|
control.VectorData + index,
|
||||||
LoadUnalignedBufferWord(bindingIndex, address));
|
LoadBufferWord(bindingIndex, indexedDwordAddress));
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -2510,17 +2509,16 @@ public static partial class Gen5SpirvTranslator
|
|||||||
return;
|
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++)
|
for (uint index = 0; index < control.DwordCount; index++)
|
||||||
{
|
{
|
||||||
var address = index == 0
|
var indexedDwordAddress = index == 0
|
||||||
? byteAddress
|
? dwordAddress
|
||||||
: IAdd(byteAddress, UInt(index * sizeof(uint)));
|
: IAdd(dwordAddress, UInt(index));
|
||||||
StoreBufferBytes(
|
StoreBufferWord(
|
||||||
bindingIndex,
|
bindingIndex,
|
||||||
address,
|
indexedDwordAddress,
|
||||||
LoadV(control.VectorData + index),
|
LoadV(control.VectorData + index));
|
||||||
sizeof(uint),
|
|
||||||
0);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2576,12 +2574,12 @@ public static partial class Gen5SpirvTranslator
|
|||||||
|
|
||||||
for (uint index = 0; index < control.DwordCount; index++)
|
for (uint index = 0; index < control.DwordCount; index++)
|
||||||
{
|
{
|
||||||
var address = index == 0
|
var indexedDwordAddress = index == 0
|
||||||
? byteAddress
|
? dwordAddress
|
||||||
: IAdd(byteAddress, UInt(index * sizeof(uint)));
|
: IAdd(dwordAddress, UInt(index));
|
||||||
StoreV(
|
StoreV(
|
||||||
control.VectorData + index,
|
control.VectorData + index,
|
||||||
LoadUnalignedBufferWord(bindingIndex, address));
|
LoadBufferWord(bindingIndex, indexedDwordAddress));
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1015,6 +1015,12 @@ public static class Gen5ShaderTranslator
|
|||||||
0x2F => "VCvtPkrtzF16F32",
|
0x2F => "VCvtPkrtzF16F32",
|
||||||
0x30 => "VCvtPkU16U32",
|
0x30 => "VCvtPkU16U32",
|
||||||
0x31 => "VCvtPkI16I32",
|
0x31 => "VCvtPkI16I32",
|
||||||
|
0x32 => "VAddF16",
|
||||||
|
0x33 => "VSubF16",
|
||||||
|
0x34 => "VSubrevF16",
|
||||||
|
0x35 => "VMulF16",
|
||||||
|
0x39 => "VMaxF16",
|
||||||
|
0x3A => "VMinF16",
|
||||||
_ => string.Empty,
|
_ => string.Empty,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1086,6 +1092,14 @@ public static class Gen5ShaderTranslator
|
|||||||
0xC5 => "VCmpNeU32",
|
0xC5 => "VCmpNeU32",
|
||||||
0xC6 => "VCmpGeU32",
|
0xC6 => "VCmpGeU32",
|
||||||
0xC7 => "VCmpTU32",
|
0xC7 => "VCmpTU32",
|
||||||
|
0xC8 => "VCmpFF16",
|
||||||
|
0xC9 => "VCmpLtF16",
|
||||||
|
0xCA => "VCmpEqF16",
|
||||||
|
0xCB => "VCmpLeF16",
|
||||||
|
0xCC => "VCmpGtF16",
|
||||||
|
0xCD => "VCmpLgF16",
|
||||||
|
0xCE => "VCmpGeF16",
|
||||||
|
0xCF => "VCmpOF16",
|
||||||
0xD0 => "VCmpxFU32",
|
0xD0 => "VCmpxFU32",
|
||||||
0xD1 => "VCmpxLtU32",
|
0xD1 => "VCmpxLtU32",
|
||||||
0xD2 => "VCmpxEqU32",
|
0xD2 => "VCmpxEqU32",
|
||||||
@@ -1094,6 +1108,30 @@ public static class Gen5ShaderTranslator
|
|||||||
0xD5 => "VCmpxNeU32",
|
0xD5 => "VCmpxNeU32",
|
||||||
0xD6 => "VCmpxGeU32",
|
0xD6 => "VCmpxGeU32",
|
||||||
0xD7 => "VCmpxTU32",
|
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,
|
_ => string.Empty,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -102,17 +102,16 @@ public sealed class SysAbiExportAnalyzer : DiagnosticAnalyzer
|
|||||||
ConcurrentDictionary<string, IMethodSymbol> exportsByNid)
|
ConcurrentDictionary<string, IMethodSymbol> exportsByNid)
|
||||||
{
|
{
|
||||||
var method = (IMethodSymbol)context.Symbol;
|
var method = (IMethodSymbol)context.Symbol;
|
||||||
AttributeData? exportAttribute = null;
|
var exportAttributes = ImmutableArray.CreateBuilder<AttributeData>();
|
||||||
foreach (var attribute in method.GetAttributes())
|
foreach (var attribute in method.GetAttributes())
|
||||||
{
|
{
|
||||||
if (SysAbiExportShape.IsSysAbiExportAttribute(attribute.AttributeClass))
|
if (SysAbiExportShape.IsSysAbiExportAttribute(attribute.AttributeClass))
|
||||||
{
|
{
|
||||||
exportAttribute = attribute;
|
exportAttributes.Add(attribute);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exportAttribute is null)
|
if (exportAttributes.Count == 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -135,6 +134,28 @@ public sealed class SysAbiExportAnalyzer : DiagnosticAnalyzer
|
|||||||
SysAbiDiagnostics.HandlerNotAccessible, location, methodDisplay));
|
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 arguments = SysAbiExportShape.ReadArguments(exportAttribute);
|
||||||
var hasNid = !string.IsNullOrWhiteSpace(arguments.Nid);
|
var hasNid = !string.IsNullOrWhiteSpace(arguments.Nid);
|
||||||
var hasName = !string.IsNullOrWhiteSpace(arguments.ExportName);
|
var hasName = !string.IsNullOrWhiteSpace(arguments.ExportName);
|
||||||
@@ -188,9 +209,9 @@ public sealed class SysAbiExportAnalyzer : DiagnosticAnalyzer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var existing = exportsByNid.GetOrAdd(effectiveNid, method);
|
if (!exportsByNid.TryAdd(effectiveNid, method))
|
||||||
if (!SymbolEqualityComparer.Default.Equals(existing, method))
|
|
||||||
{
|
{
|
||||||
|
var existing = exportsByNid[effectiveNid];
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
context.ReportDiagnostic(Diagnostic.Create(
|
||||||
SysAbiDiagnostics.DuplicateNid,
|
SysAbiDiagnostics.DuplicateNid,
|
||||||
location,
|
location,
|
||||||
|
|||||||
@@ -27,7 +27,16 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
|
|||||||
|
|
||||||
private sealed class ExportModel : IEquatable<ExportModel>
|
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)
|
public ExportModel(
|
||||||
|
string containingType,
|
||||||
|
string methodName,
|
||||||
|
SysAbiExportShape.HandlerShape shape,
|
||||||
|
string typedParameterKinds,
|
||||||
|
string libraryName,
|
||||||
|
string nid,
|
||||||
|
string exportName,
|
||||||
|
int target,
|
||||||
|
bool preferLle)
|
||||||
{
|
{
|
||||||
ContainingType = containingType;
|
ContainingType = containingType;
|
||||||
MethodName = methodName;
|
MethodName = methodName;
|
||||||
@@ -37,6 +46,7 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
|
|||||||
Nid = nid;
|
Nid = nid;
|
||||||
ExportName = exportName;
|
ExportName = exportName;
|
||||||
Target = target;
|
Target = target;
|
||||||
|
PreferLle = preferLle;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ContainingType { get; }
|
public string ContainingType { get; }
|
||||||
@@ -51,6 +61,7 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
|
|||||||
public string Nid { get; }
|
public string Nid { get; }
|
||||||
public string ExportName { get; }
|
public string ExportName { get; }
|
||||||
public int Target { get; }
|
public int Target { get; }
|
||||||
|
public bool PreferLle { get; }
|
||||||
|
|
||||||
public bool Equals(ExportModel? other) =>
|
public bool Equals(ExportModel? other) =>
|
||||||
other is not null &&
|
other is not null &&
|
||||||
@@ -61,7 +72,8 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
|
|||||||
LibraryName == other.LibraryName &&
|
LibraryName == other.LibraryName &&
|
||||||
Nid == other.Nid &&
|
Nid == other.Nid &&
|
||||||
ExportName == other.ExportName &&
|
ExportName == other.ExportName &&
|
||||||
Target == other.Target;
|
Target == other.Target &&
|
||||||
|
PreferLle == other.PreferLle;
|
||||||
|
|
||||||
public override bool Equals(object? obj) => Equals(obj as ExportModel);
|
public override bool Equals(object? obj) => Equals(obj as ExportModel);
|
||||||
|
|
||||||
@@ -73,6 +85,7 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
|
|||||||
hash = (hash * 31) + ContainingType.GetHashCode();
|
hash = (hash * 31) + ContainingType.GetHashCode();
|
||||||
hash = (hash * 31) + MethodName.GetHashCode();
|
hash = (hash * 31) + MethodName.GetHashCode();
|
||||||
hash = (hash * 31) + Nid.GetHashCode();
|
hash = (hash * 31) + Nid.GetHashCode();
|
||||||
|
hash = (hash * 31) + PreferLle.GetHashCode();
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,80 +93,90 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
|
|||||||
|
|
||||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||||
{
|
{
|
||||||
var exports = context.SyntaxProvider
|
var exportGroups = context.SyntaxProvider
|
||||||
.ForAttributeWithMetadataName(
|
.ForAttributeWithMetadataName(
|
||||||
AttributeMetadataName,
|
AttributeMetadataName,
|
||||||
static (node, _) => node is MethodDeclarationSyntax,
|
static (node, _) => node is MethodDeclarationSyntax,
|
||||||
static (attributeContext, _) => CreateModel(attributeContext))
|
static (attributeContext, _) => CreateModels(attributeContext))
|
||||||
.Where(static model => model is not null)
|
.Where(static models => !models.IsDefaultOrEmpty)
|
||||||
.Collect();
|
.Collect();
|
||||||
|
|
||||||
var assemblyName = context.CompilationProvider
|
var assemblyName = context.CompilationProvider
|
||||||
.Select(static (compilation, _) => compilation.AssemblyName ?? "Assembly");
|
.Select(static (compilation, _) => compilation.AssemblyName ?? "Assembly");
|
||||||
|
|
||||||
context.RegisterSourceOutput(
|
context.RegisterSourceOutput(
|
||||||
exports.Combine(assemblyName),
|
exportGroups.Combine(assemblyName),
|
||||||
static (productionContext, source) => Emit(productionContext, source.Left!, source.Right));
|
static (productionContext, source) => Emit(productionContext, source.Left!, source.Right));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ExportModel? CreateModel(GeneratorAttributeSyntaxContext context)
|
private static ImmutableArray<ExportModel> CreateModels(GeneratorAttributeSyntaxContext context)
|
||||||
{
|
{
|
||||||
if (context.TargetSymbol is not IMethodSymbol method ||
|
if (context.TargetSymbol is not IMethodSymbol method ||
|
||||||
!SysAbiExportShape.IsAccessibleFromGeneratedCode(method))
|
!SysAbiExportShape.IsAccessibleFromGeneratedCode(method))
|
||||||
{
|
{
|
||||||
return null;
|
return ImmutableArray<ExportModel>.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
var shape = SysAbiExportShape.Classify(method, out var typedParameterKinds);
|
var shape = SysAbiExportShape.Classify(method, out var typedParameterKinds);
|
||||||
if (shape == SysAbiExportShape.HandlerShape.Invalid)
|
if (shape == SysAbiExportShape.HandlerShape.Invalid)
|
||||||
{
|
{
|
||||||
return null;
|
return ImmutableArray<ExportModel>.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
var attribute = context.Attributes[0];
|
var models = ImmutableArray.CreateBuilder<ExportModel>(context.Attributes.Length);
|
||||||
var arguments = SysAbiExportShape.ReadArguments(attribute);
|
foreach (var attribute in context.Attributes)
|
||||||
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))
|
|
||||||
{
|
{
|
||||||
nid = Ps5Nid.Compute(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));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(nid))
|
return models.ToImmutable();
|
||||||
{
|
|
||||||
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(
|
private static void Emit(
|
||||||
SourceProductionContext context,
|
SourceProductionContext context,
|
||||||
ImmutableArray<ExportModel?> exports,
|
ImmutableArray<ImmutableArray<ExportModel>> exportGroups,
|
||||||
string assemblyName)
|
string assemblyName)
|
||||||
{
|
{
|
||||||
// No exports, no registry: an assembly that merely references the analyzer
|
// No exports, no registry: an assembly that merely references the analyzer
|
||||||
// (e.g. SharpEmu.HLE itself) must not mint a colliding
|
// (e.g. SharpEmu.HLE itself) must not mint a colliding
|
||||||
// SharpEmu.Generated.SysAbiExportRegistry type.
|
// SharpEmu.Generated.SysAbiExportRegistry type.
|
||||||
if (exports.IsDefaultOrEmpty)
|
var exportCount = 0;
|
||||||
|
foreach (var group in exportGroups)
|
||||||
|
{
|
||||||
|
exportCount += group.Length;
|
||||||
|
}
|
||||||
|
if (exportCount == 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -175,24 +198,23 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
|
|||||||
builder.AppendLine(" public static global::System.Collections.Generic.IReadOnlyList<global::SharpEmu.HLE.ExportedFunction> CreateExports(");
|
builder.AppendLine(" public static global::System.Collections.Generic.IReadOnlyList<global::SharpEmu.HLE.ExportedFunction> CreateExports(");
|
||||||
builder.AppendLine(" global::SharpEmu.HLE.Generation registrationGeneration)");
|
builder.AppendLine(" global::SharpEmu.HLE.Generation registrationGeneration)");
|
||||||
builder.AppendLine(" {");
|
builder.AppendLine(" {");
|
||||||
builder.AppendLine($" var exports = new global::System.Collections.Generic.List<global::SharpEmu.HLE.ExportedFunction>({exports.Length});");
|
builder.AppendLine($" var exports = new global::System.Collections.Generic.List<global::SharpEmu.HLE.ExportedFunction>({exportCount});");
|
||||||
|
|
||||||
foreach (var export in exports)
|
foreach (var group in exportGroups)
|
||||||
{
|
{
|
||||||
if (export is null)
|
foreach (var export in group)
|
||||||
{
|
{
|
||||||
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}, " +
|
||||||
|
$"{(export.PreferLle ? "true" : "false")}, {function});");
|
||||||
}
|
}
|
||||||
|
|
||||||
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;");
|
builder.AppendLine(" return exports;");
|
||||||
@@ -205,6 +227,7 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
|
|||||||
builder.AppendLine(" string nid,");
|
builder.AppendLine(" string nid,");
|
||||||
builder.AppendLine(" string exportName,");
|
builder.AppendLine(" string exportName,");
|
||||||
builder.AppendLine(" global::SharpEmu.HLE.Generation attributeTarget,");
|
builder.AppendLine(" global::SharpEmu.HLE.Generation attributeTarget,");
|
||||||
|
builder.AppendLine(" bool preferLle,");
|
||||||
builder.AppendLine(" global::SharpEmu.HLE.SysAbiFunction function)");
|
builder.AppendLine(" global::SharpEmu.HLE.SysAbiFunction function)");
|
||||||
builder.AppendLine(" {");
|
builder.AppendLine(" {");
|
||||||
builder.AppendLine(" var target = attributeTarget == global::SharpEmu.HLE.Generation.None ? registrationGeneration : attributeTarget;");
|
builder.AppendLine(" var target = attributeTarget == global::SharpEmu.HLE.Generation.None ? registrationGeneration : attributeTarget;");
|
||||||
@@ -213,7 +236,7 @@ public sealed class SysAbiExportGenerator : IIncrementalGenerator
|
|||||||
builder.AppendLine(" return;");
|
builder.AppendLine(" return;");
|
||||||
builder.AppendLine(" }");
|
builder.AppendLine(" }");
|
||||||
builder.AppendLine();
|
builder.AppendLine();
|
||||||
builder.AppendLine(" exports.Add(new global::SharpEmu.HLE.ExportedFunction(libraryName, nid, exportName, target, function));");
|
builder.AppendLine(" exports.Add(new global::SharpEmu.HLE.ExportedFunction(libraryName, nid, exportName, target, function, preferLle));");
|
||||||
builder.AppendLine(" }");
|
builder.AppendLine(" }");
|
||||||
builder.AppendLine("}");
|
builder.AppendLine("}");
|
||||||
|
|
||||||
|
|||||||
@@ -18,18 +18,20 @@ public static class SysAbiExportShape
|
|||||||
|
|
||||||
public readonly struct Arguments
|
public readonly struct Arguments
|
||||||
{
|
{
|
||||||
public Arguments(string libraryName, string nid, string exportName, int target)
|
public Arguments(string libraryName, string nid, string exportName, int target, bool preferLle)
|
||||||
{
|
{
|
||||||
LibraryName = libraryName;
|
LibraryName = libraryName;
|
||||||
Nid = nid;
|
Nid = nid;
|
||||||
ExportName = exportName;
|
ExportName = exportName;
|
||||||
Target = target;
|
Target = target;
|
||||||
|
PreferLle = preferLle;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string LibraryName { get; }
|
public string LibraryName { get; }
|
||||||
public string Nid { get; }
|
public string Nid { get; }
|
||||||
public string ExportName { get; }
|
public string ExportName { get; }
|
||||||
public int Target { get; }
|
public int Target { get; }
|
||||||
|
public bool PreferLle { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -205,6 +207,7 @@ public static class SysAbiExportShape
|
|||||||
var nid = string.Empty;
|
var nid = string.Empty;
|
||||||
var exportName = string.Empty;
|
var exportName = string.Empty;
|
||||||
var target = 0;
|
var target = 0;
|
||||||
|
var preferLle = false;
|
||||||
foreach (var argument in attribute.NamedArguments)
|
foreach (var argument in attribute.NamedArguments)
|
||||||
{
|
{
|
||||||
switch (argument.Key)
|
switch (argument.Key)
|
||||||
@@ -221,9 +224,12 @@ public static class SysAbiExportShape
|
|||||||
case "Target":
|
case "Target":
|
||||||
target = argument.Value.Value is int value ? value : 0;
|
target = argument.Value.Value is int value ? value : 0;
|
||||||
break;
|
break;
|
||||||
|
case "PreferLle":
|
||||||
|
preferLle = argument.Value.Value is bool boolValue && boolValue;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Arguments(libraryName, nid, exportName, target);
|
return new Arguments(libraryName, nid, exportName, target, preferLle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ public sealed class AgcVertexMetadataTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void MergeVertexInputs_OverlaysFormatWithoutRebasingCapture()
|
public void MergeVertexInputs_OverlaysLayoutWithoutRebasingCapture()
|
||||||
{
|
{
|
||||||
const ulong memoryBase = 0x1_0000_0000;
|
const ulong memoryBase = 0x1_0000_0000;
|
||||||
var memory = new FakeCpuMemory(memoryBase, 0x2000);
|
var memory = new FakeCpuMemory(memoryBase, 0x2000);
|
||||||
@@ -114,7 +114,7 @@ public sealed class AgcVertexMetadataTests
|
|||||||
NumberFormat: 7,
|
NumberFormat: 7,
|
||||||
BaseAddress: sharpBase,
|
BaseAddress: sharpBase,
|
||||||
Stride: 16,
|
Stride: 16,
|
||||||
OffsetBytes: 0,
|
OffsetBytes: 12,
|
||||||
Data: data,
|
Data: data,
|
||||||
DataLength: data.Length,
|
DataLength: data.Length,
|
||||||
DataPooled: false),
|
DataPooled: false),
|
||||||
@@ -135,6 +135,137 @@ public sealed class AgcVertexMetadataTests
|
|||||||
Assert.Equal(0x40u, merged[0].Pc);
|
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]
|
[Fact]
|
||||||
public void MergeVertexInputs_AcceptsVertexAttribFormatEnums()
|
public void MergeVertexInputs_AcceptsVertexAttribFormatEnums()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
// 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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// 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()
|
public void FreedRangesAreReusedAndCoalesced()
|
||||||
{
|
{
|
||||||
using var memory = new PhysicalVirtualMemory(new FakeHostMemory());
|
using var memory = new PhysicalVirtualMemory(new FakeHostMemory());
|
||||||
const ulong usableArenaSize = 0x0100_0000 - 0x1000;
|
const ulong usableArenaSize = 0x2000_0000 - 0x1000;
|
||||||
|
|
||||||
Assert.True(memory.TryAllocateGuestMemory(0x4000, 0x1000, out var first));
|
Assert.True(memory.TryAllocateGuestMemory(0x4000, 0x1000, out var first));
|
||||||
Assert.True(memory.TryAllocateGuestMemory(0x8000, 0x1000, out var second));
|
Assert.True(memory.TryAllocateGuestMemory(0x8000, 0x1000, out var second));
|
||||||
@@ -34,6 +34,16 @@ public sealed class GuestMemoryAllocatorTests
|
|||||||
Assert.Equal(first, coalesced);
|
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]
|
[Fact]
|
||||||
public void SegmentProtectionIsAppliedInContiguousRuns()
|
public void SegmentProtectionIsAppliedInContiguousRuns()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
// 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,6 +53,40 @@ public sealed class SysAbiExportAnalyzerTests
|
|||||||
AssertSingle(diagnostics, "SHEM001");
|
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]
|
[Fact]
|
||||||
public void MalformedNidIsReported()
|
public void MalformedNidIsReported()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ public sealed class SysAbiExportGeneratorTests
|
|||||||
// Guest string marshalling: the thunk reads the pointer before the handler.
|
// Guest string marshalling: the thunk reads the pointer before the handler.
|
||||||
[SysAbiExport(Nid = "1G3lF1Gg1k8", ExportName = "sceKernelOpen")]
|
[SysAbiExport(Nid = "1G3lF1Gg1k8", ExportName = "sceKernelOpen")]
|
||||||
public static int KernelOpen(CpuContext ctx, [GuestCString(4096)] string path, int flags) => 0;
|
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;
|
||||||
}
|
}
|
||||||
""";
|
""";
|
||||||
|
|
||||||
@@ -123,6 +128,22 @@ public sealed class SysAbiExportGeneratorTests
|
|||||||
Assert.Contains("(target & registrationGeneration) == 0", generated, StringComparison.Ordinal);
|
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]
|
[Fact]
|
||||||
public void AssemblyWithoutExportsEmitsNoRegistry()
|
public void AssemblyWithoutExportsEmitsNoRegistry()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
// failure that must stay loud. Any unexpected outcome makes the tool exit
|
// failure that must stay loud. Any unexpected outcome makes the tool exit
|
||||||
// non-zero, so it can gate scripts/CI.
|
// non-zero, so it can gate scripts/CI.
|
||||||
//
|
//
|
||||||
// Usage: SharpEmu.Tools.ShaderDump [output-directory]
|
// Usage:
|
||||||
|
// SharpEmu.Tools.ShaderDump [output-directory]
|
||||||
|
// SharpEmu.Tools.ShaderDump --inspect <shader.bin> [byte-count]
|
||||||
|
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
@@ -26,6 +28,76 @@ using SharpEmu.ShaderCompiler.Vulkan;
|
|||||||
|
|
||||||
const ulong ProgramAddress = 0x100000;
|
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 =
|
(string Name, bool ExpectTranslate, uint[] Words)[] testPrograms =
|
||||||
[
|
[
|
||||||
("fmac", true, [
|
("fmac", true, [
|
||||||
|
|||||||
Reference in New Issue
Block a user