mirror of
https://github.com/par274/sharpemu.git
synced 2026-08-03 16:39:51 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da36a45bf3 |
@@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SharpEmuVersion>0.0.3-release.2</SharpEmuVersion>
|
||||
<SharpEmuVersion>0.0.3-hotfix-2</SharpEmuVersion>
|
||||
<Version>$(SharpEmuVersion)</Version>
|
||||
|
||||
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
|
||||
|
||||
@@ -48,8 +48,6 @@ internal static partial class Program
|
||||
{
|
||||
ConfigureManagedPluginResolution();
|
||||
|
||||
SharpEmu.Libs.VideoOut.RenderDocCapture.Initialize();
|
||||
|
||||
try
|
||||
{
|
||||
return Run(args);
|
||||
|
||||
@@ -26,7 +26,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
private long _mappingGeneration;
|
||||
private const ulong PageSize = 0x1000;
|
||||
private const ulong HostAllocationGranularity = 0x10000;
|
||||
private const ulong GuestAllocationArenaAddress = 0x00006000_0000_0000;
|
||||
private const ulong GuestAllocationArenaSize = 0x0100_0000;
|
||||
private const ulong GuestAllocationArenaStartOffset = PageSize;
|
||||
@@ -118,9 +117,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
private const uint PAGE_READONLY = 0x02;
|
||||
|
||||
private readonly IHostMemory _hostMemory;
|
||||
|
||||
private readonly object _fixedAllocationGate = new();
|
||||
private readonly HashSet<ulong> _fixedGranuleReservationBases = new();
|
||||
private ulong _guestAllocationArenaBase;
|
||||
private readonly SortedDictionary<ulong, ulong> _guestAllocationFreeRanges = new();
|
||||
private readonly Dictionary<ulong, (ulong Offset, ulong Size)> _guestAllocations = new();
|
||||
@@ -251,12 +247,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
// reserve-only + lazy commit only when a huge non-exec commit fails —
|
||||
// that is the Poppy / large-reservation path #608 was aiming for.
|
||||
var reservedOnly = false;
|
||||
var result = TryAllocateFixedThroughGranules(desiredAddress, alignedSize, hostProtection, traceReject: false);
|
||||
if (result == 0)
|
||||
{
|
||||
result = _hostMemory.Allocate(desiredAddress, alignedSize, hostProtection);
|
||||
}
|
||||
|
||||
var result = _hostMemory.Allocate(desiredAddress, alignedSize, hostProtection);
|
||||
if (result == 0 && allowLazyReserve)
|
||||
{
|
||||
result = _hostMemory.Reserve(desiredAddress, alignedSize, HostPageProtection.ReadWrite);
|
||||
@@ -338,16 +329,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
// Prefer a full commit. Only fall back to reserve-only when a large
|
||||
// non-executable commit cannot be satisfied (see TryAllocateAtExact).
|
||||
ulong result = 0;
|
||||
if (desiredAddress != 0)
|
||||
{
|
||||
result = TryAllocateFixedThroughGranules(desiredAddress, alignedSize, hostProtection, traceReject: false);
|
||||
}
|
||||
|
||||
if (result == 0)
|
||||
{
|
||||
result = _hostMemory.Allocate(desiredAddress, alignedSize, hostProtection);
|
||||
}
|
||||
ulong result = _hostMemory.Allocate(desiredAddress, alignedSize, hostProtection);
|
||||
|
||||
if (result == 0)
|
||||
{
|
||||
@@ -454,183 +436,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
return $"fail:{primeBytes:X}";
|
||||
}
|
||||
|
||||
private ulong TryAllocateFixedThroughGranules(
|
||||
ulong desiredAddress,
|
||||
ulong alignedSize,
|
||||
HostPageProtection hostProtection,
|
||||
bool traceReject = true)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows() || desiredAddress == 0 || alignedSize == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var requestStart = AlignDown(desiredAddress, PageSize);
|
||||
ulong requestEnd;
|
||||
ulong granuleEnd;
|
||||
try
|
||||
{
|
||||
requestEnd = AlignUp(desiredAddress + alignedSize, PageSize);
|
||||
granuleEnd = AlignUp(requestEnd, HostAllocationGranularity);
|
||||
}
|
||||
catch (OverflowException)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var granuleStart = AlignDown(requestStart, HostAllocationGranularity);
|
||||
|
||||
lock (_fixedAllocationGate)
|
||||
{
|
||||
var newReservations = new List<ulong>();
|
||||
|
||||
void Reject(ulong segmentAddress, string reason)
|
||||
{
|
||||
if (traceReject)
|
||||
{
|
||||
Log.Warn(
|
||||
$"fixed-alloc reject: want=0x{desiredAddress:X16}+0x{alignedSize:X} segment=0x{segmentAddress:X16} {reason}");
|
||||
}
|
||||
foreach (var reservationBase in newReservations)
|
||||
{
|
||||
_hostMemory.Free(reservationBase);
|
||||
_fixedGranuleReservationBases.Remove(reservationBase);
|
||||
}
|
||||
}
|
||||
|
||||
var cursor = granuleStart;
|
||||
while (cursor < granuleEnd)
|
||||
{
|
||||
if (!_hostMemory.Query(cursor, out var info))
|
||||
{
|
||||
Reject(cursor, "query-failed");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var segmentEnd = info.RegionSize > ulong.MaxValue - info.BaseAddress
|
||||
? ulong.MaxValue
|
||||
: info.BaseAddress + info.RegionSize;
|
||||
segmentEnd = Math.Min(segmentEnd, granuleEnd);
|
||||
if (segmentEnd <= cursor)
|
||||
{
|
||||
Reject(cursor, "query-no-progress");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (info.State == HostRegionState.Free)
|
||||
{
|
||||
var alignedReserveBase = AlignUp(cursor, HostAllocationGranularity);
|
||||
var unreservableEnd = Math.Min(segmentEnd, alignedReserveBase);
|
||||
if (unreservableEnd > cursor && cursor < requestEnd && unreservableEnd > requestStart)
|
||||
{
|
||||
Reject(cursor, $"free-but-unreservable head (granule base 0x{AlignDown(cursor, HostAllocationGranularity):X16} owned elsewhere)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (alignedReserveBase < segmentEnd)
|
||||
{
|
||||
var reserved = _hostMemory.Reserve(alignedReserveBase, segmentEnd - alignedReserveBase, HostPageProtection.ReadWrite);
|
||||
if (reserved != alignedReserveBase)
|
||||
{
|
||||
if (reserved != 0)
|
||||
{
|
||||
_hostMemory.Free(reserved);
|
||||
}
|
||||
|
||||
Reject(alignedReserveBase, "reserve-failed");
|
||||
return 0;
|
||||
}
|
||||
|
||||
_fixedGranuleReservationBases.Add(alignedReserveBase);
|
||||
newReservations.Add(alignedReserveBase);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var trusted = _fixedGranuleReservationBases.Contains(info.AllocationBase) ||
|
||||
IsTrackedRegionBase(info.AllocationBase);
|
||||
if (!trusted && cursor < requestEnd && segmentEnd > requestStart)
|
||||
{
|
||||
Reject(cursor, $"foreign {info.State} allocBase=0x{info.AllocationBase:X16} prot=0x{info.RawProtection:X}");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
cursor = segmentEnd;
|
||||
}
|
||||
|
||||
var commitCursor = requestStart;
|
||||
while (commitCursor < requestEnd)
|
||||
{
|
||||
if (!_hostMemory.Query(commitCursor, out var info))
|
||||
{
|
||||
Reject(commitCursor, "commit-query-failed");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var segmentEnd = info.RegionSize > ulong.MaxValue - info.BaseAddress
|
||||
? ulong.MaxValue
|
||||
: info.BaseAddress + info.RegionSize;
|
||||
segmentEnd = Math.Min(segmentEnd, requestEnd);
|
||||
if (segmentEnd <= commitCursor)
|
||||
{
|
||||
Reject(commitCursor, "commit-no-progress");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (info.State != HostRegionState.Committed &&
|
||||
!_hostMemory.Commit(commitCursor, segmentEnd - commitCursor, hostProtection))
|
||||
{
|
||||
Reject(commitCursor, "commit-failed");
|
||||
return 0;
|
||||
}
|
||||
|
||||
commitCursor = segmentEnd;
|
||||
}
|
||||
|
||||
if (newReservations.Count == 0)
|
||||
{
|
||||
TraceVmem($"Fixed alloc committed into existing granule reservations: 0x{desiredAddress:X16}+0x{alignedSize:X}");
|
||||
}
|
||||
|
||||
return desiredAddress;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsTrackedRegionBase(ulong allocationBase)
|
||||
{
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
var low = 0;
|
||||
var high = _regions.Count - 1;
|
||||
while (low <= high)
|
||||
{
|
||||
var middle = low + ((high - low) >> 1);
|
||||
var address = _regions[middle].VirtualAddress;
|
||||
if (address == allocationBase)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (address < allocationBase)
|
||||
{
|
||||
low = middle + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
high = middle - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryBackFixedRange(ulong address, ulong size, bool executable)
|
||||
{
|
||||
if (size == 0)
|
||||
@@ -658,7 +463,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
// MemoryRegions are inserted only once every gap in the range has been
|
||||
// backed. If any gap fails to back, every earlier host allocation is freed
|
||||
// and no region is inserted, so the address space is left untouched.
|
||||
var stagedAllocations = new List<(ulong Address, ulong Size, bool GranuleTracked)>();
|
||||
var stagedAllocations = new List<(ulong Address, ulong Size)>();
|
||||
|
||||
var cursor = start;
|
||||
while (cursor < end)
|
||||
@@ -677,21 +482,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
goto Rollback;
|
||||
}
|
||||
|
||||
var needsGranuleAwareBacking = OperatingSystem.IsWindows() &&
|
||||
(info.State == HostRegionState.Free || info.State == HostRegionState.Reserved);
|
||||
|
||||
if (needsGranuleAwareBacking)
|
||||
{
|
||||
var runSize = runEnd - cursor;
|
||||
if (TryAllocateFixedThroughGranules(cursor, runSize, hostProtection, traceReject: false) != cursor)
|
||||
{
|
||||
goto Rollback;
|
||||
}
|
||||
|
||||
stagedAllocations.Add((cursor, runSize, true));
|
||||
TraceVmem($"Backed fixed range gap: 0x{cursor:X16} - 0x{runEnd:X16} ({runSize} bytes)");
|
||||
}
|
||||
else if (info.State == HostRegionState.Free)
|
||||
if (info.State == HostRegionState.Free)
|
||||
{
|
||||
var runSize = runEnd - cursor;
|
||||
var allocated = _hostMemory.Allocate(cursor, runSize, hostProtection);
|
||||
@@ -705,11 +496,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
goto Rollback;
|
||||
}
|
||||
|
||||
stagedAllocations.Add((cursor, runSize, false));
|
||||
stagedAllocations.Add((cursor, runSize));
|
||||
TraceVmem($"Backed fixed range gap: 0x{cursor:X16} - 0x{runEnd:X16} ({runSize} bytes)");
|
||||
}
|
||||
|
||||
|
||||
cursor = runEnd;
|
||||
}
|
||||
|
||||
@@ -723,7 +513,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
foreach (var (gapAddress, gapSize, _) in stagedAllocations)
|
||||
foreach (var (gapAddress, gapSize) in stagedAllocations)
|
||||
{
|
||||
InsertRegionSorted(new MemoryRegion
|
||||
{
|
||||
@@ -743,12 +533,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
return true;
|
||||
|
||||
Rollback:
|
||||
foreach (var (gapAddress, _, granuleTracked) in stagedAllocations)
|
||||
foreach (var (gapAddress, _) in stagedAllocations)
|
||||
{
|
||||
if (!granuleTracked)
|
||||
{
|
||||
_hostMemory.Free(gapAddress);
|
||||
}
|
||||
_hostMemory.Free(gapAddress);
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -1004,41 +791,24 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
{
|
||||
lock (_guestAllocationGate)
|
||||
{
|
||||
lock (_fixedAllocationGate)
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
var freedBases = new HashSet<ulong>();
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
if (freedBases.Add(region.VirtualAddress))
|
||||
{
|
||||
_hostMemory.Free(region.VirtualAddress);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var reservationBase in _fixedGranuleReservationBases)
|
||||
{
|
||||
if (freedBases.Add(reservationBase))
|
||||
{
|
||||
_hostMemory.Free(reservationBase);
|
||||
}
|
||||
}
|
||||
|
||||
_fixedGranuleReservationBases.Clear();
|
||||
_regions.Clear();
|
||||
_pageProtections.Clear();
|
||||
lock (_allocationSearchHintGate)
|
||||
{
|
||||
_allocationSearchHints.Clear();
|
||||
}
|
||||
Interlocked.Increment(ref _mappingGeneration);
|
||||
_hostMemory.Free(region.VirtualAddress);
|
||||
}
|
||||
finally
|
||||
_regions.Clear();
|
||||
_pageProtections.Clear();
|
||||
lock (_allocationSearchHintGate)
|
||||
{
|
||||
_gate.ExitWriteLock();
|
||||
_allocationSearchHints.Clear();
|
||||
}
|
||||
Interlocked.Increment(ref _mappingGeneration);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitWriteLock();
|
||||
}
|
||||
|
||||
_guestAllocationArenaBase = 0;
|
||||
@@ -1632,42 +1402,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsWindows() && !region.IsReservedOnly)
|
||||
{
|
||||
var previous = low > 0 ? _regions[low - 1] : null;
|
||||
var next = low < _regions.Count ? _regions[low] : null;
|
||||
var mergePrevious = previous is not null &&
|
||||
!previous.IsReservedOnly &&
|
||||
previous.IsExecutable == region.IsExecutable &&
|
||||
previous.Protection == region.Protection &&
|
||||
previous.VirtualAddress + previous.Size == region.VirtualAddress;
|
||||
var mergeNext = next is not null &&
|
||||
!next.IsReservedOnly &&
|
||||
next.IsExecutable == region.IsExecutable &&
|
||||
next.Protection == region.Protection &&
|
||||
region.VirtualAddress + region.Size == next.VirtualAddress;
|
||||
|
||||
if (mergePrevious && mergeNext)
|
||||
{
|
||||
previous!.Size += region.Size + next!.Size;
|
||||
_regions.RemoveAt(low);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mergePrevious)
|
||||
{
|
||||
previous!.Size += region.Size;
|
||||
return;
|
||||
}
|
||||
|
||||
if (mergeNext)
|
||||
{
|
||||
next!.VirtualAddress = region.VirtualAddress;
|
||||
next.Size += region.Size;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_regions.Insert(low, region);
|
||||
}
|
||||
|
||||
|
||||
@@ -145,9 +145,6 @@
|
||||
"Options.Env.LogDirectMemory.Desc": "تسجيل تخصيصات الذاكرة المباشرة وإخفاقاتها في وحدة التحكم.\nاستخدمه عندما تنهار لعبة أو تُغلق أثناء الإقلاع.",
|
||||
"Options.Env.LogIo.Desc": "تسجيل فتح الملفات وقراءتها وحلّ المسارات في وحدة التحكم.\nاستخدمه عندما لا تجد لعبة ملفات بياناتها أثناء الإقلاع.",
|
||||
"Options.Env.LogNp.Desc": "تسجيل نداءات مكتبة NP (شبكة PlayStation) في وحدة التحكم.",
|
||||
"Options.Env.Group.Debug": "تصحيح الأخطاء",
|
||||
"Options.Env.Group.General": "عام",
|
||||
"Options.Env.RenderDoc.Desc": "يحمّل واجهة RenderDoc داخل التطبيق حتى يمكن التقاط الإطارات من داخل المحاكي.\nاضغط F10 أثناء تشغيل اللعبة لالتقاط إطار واحد؛ تُحفظ اللقطات في user/logs/capture_logs/<TITLE_ID>.\nيتطلب تثبيت RenderDoc. يبطئ وحدة معالجة الرسوميات ويسبب تعليق بعض الألعاب، لذا اتركه معطلاً ما لم تكن تصحح الأخطاء.",
|
||||
"Options.Env.GuestImageCpuSync.Desc": "إعادة رفع أسطح الضيف التي تعيد كتابتها شيفرة المعالج الخاصة باللعبة.\nاتركه مغلقًا عادة. شغّله للألعاب التي لا تصل أسطحها المرسومة بالمعالج إلى الشاشة.\nيكلّف أداءً ويسبب مشاكل في بعض الألعاب مثل GTA V.",
|
||||
"Common.Save": "حفظ",
|
||||
"Common.Cancel": "إلغاء",
|
||||
|
||||
@@ -39,9 +39,6 @@
|
||||
"Options.Env.DumpSpirv.Desc": "Extrai os shaders AGC e suas traduções SPIR-V para a pasta shader-dumps.\nUse ao reportar bugs de shader ou renderização.",
|
||||
"Options.Env.LogDirectMemory.Desc": "Registra alocações de memória direta e falhas no console.\nUse quando um jogo aborta ou fecha durante a inicialização (boot).",
|
||||
"Options.Env.LogNp.Desc": "Registra chamadas da biblioteca NP (PlayStation Network) no console.",
|
||||
"Options.Env.Group.Debug": "Depuração",
|
||||
"Options.Env.Group.General": "Geral",
|
||||
"Options.Env.RenderDoc.Desc": "Carrega a API in-application do RenderDoc para capturar frames de dentro do emulador.\nPressione F10 enquanto o jogo roda para capturar um frame; as capturas vão para user/logs/capture_logs/<TITLE_ID>.\nExige o RenderDoc instalado. Deixa a GPU mais lenta e trava alguns títulos, então mantenha desativado a menos que esteja depurando.",
|
||||
"Options.Env.GuestImageCpuSync.Desc": "Recarrega as superfícies do convidado que o próprio código de CPU do jogo reescreve.\nDeixe desativado normalmente. Ative para títulos cujas superfícies desenhadas pela CPU nunca chegam à tela.\nCusta desempenho e causa regressões em alguns títulos, como GTA V.",
|
||||
"Options.Section.Emulation": "EMULAÇÃO",
|
||||
"Options.Section.Logging": "LOGS",
|
||||
|
||||
@@ -145,9 +145,6 @@
|
||||
"Options.Env.LogDirectMemory.Desc": "Direkte Speicherzuweisungen und Fehler in der Konsole protokollieren.\nVerwenden, wenn ein Spiel beim Start abbricht oder sich beendet.",
|
||||
"Options.Env.LogIo.Desc": "Datei-Öffnen, -Lesen und Pfadauflösung in der Konsole protokollieren.\nVerwenden, wenn ein Spiel beim Start seine Datendateien nicht findet.",
|
||||
"Options.Env.LogNp.Desc": "NP-Bibliotheksaufrufe (PlayStation Network) in der Konsole protokollieren.",
|
||||
"Options.Env.Group.Debug": "Debug",
|
||||
"Options.Env.Group.General": "Allgemein",
|
||||
"Options.Env.RenderDoc.Desc": "Lädt die RenderDoc-In-Application-API, damit Frames aus dem Emulator heraus aufgezeichnet werden können.\nDrücke F10 während das Spiel läuft, um ein Frame aufzuzeichnen; Aufzeichnungen landen in user/logs/capture_logs/<TITLE_ID>.\nErfordert eine RenderDoc-Installation. Verlangsamt die GPU und lässt manche Titel hängen, lass es also aus, wenn du nicht debuggst.",
|
||||
"Options.Env.GuestImageCpuSync.Desc": "Gast-Oberflächen neu hochladen, die der eigene CPU-Code des Spiels überschreibt.\nNormalerweise aus lassen. Für Titel aktivieren, deren CPU-gezeichnete Oberflächen nie auf dem Bildschirm erscheinen.\nKostet Leistung und verursacht bei einigen Titeln wie GTA V Regressionen.",
|
||||
"Common.Save": "Speichern",
|
||||
"Common.Cancel": "Abbrechen",
|
||||
|
||||
@@ -145,9 +145,6 @@
|
||||
"Options.Env.LogDirectMemory.Desc": "Log direkte hukommelsestildelinger og fejl til konsollen.\nBrug dette, når et spil afbryder eller lukker under opstart.",
|
||||
"Options.Env.LogIo.Desc": "Log åbning og læsning af filer samt stiopslag til konsollen.\nBrug dette, når et spil ikke kan finde sine datafiler under opstart.",
|
||||
"Options.Env.LogNp.Desc": "Log NP-bibliotekskald (PlayStation Network) til konsollen.",
|
||||
"Options.Env.Group.Debug": "Fejlfinding",
|
||||
"Options.Env.Group.General": "Generelt",
|
||||
"Options.Env.RenderDoc.Desc": "Indlæser RenderDocs in-application-API, så frames kan optages inde fra emulatoren.\nTryk på F10 mens spillet kører for at optage ét frame; optagelser havner i user/logs/capture_logs/<TITLE_ID>.\nKræver at RenderDoc er installeret. Gør GPU'en langsommere og får nogle titler til at hænge, så lad den være slået fra, medmindre du fejlfinder.",
|
||||
"Options.Env.GuestImageCpuSync.Desc": "Genindlæs gæsteoverflader, som spillets egen CPU-kode omskriver.\nLad den være slået fra normalt. Slå til for titler, hvis CPU-tegnede overflader aldrig når skærmen.\nKoster ydeevne og giver regressioner i nogle titler, såsom GTA V.",
|
||||
"Common.Save": "Gem",
|
||||
"Common.Cancel": "Annuller",
|
||||
|
||||
@@ -44,9 +44,6 @@
|
||||
"Options.Env.LogDirectMemory.Desc": "Log direct memory allocations and failures to the console.\nUse when a game aborts or exits during boot.",
|
||||
"Options.Env.LogIo.Desc": "Log file open, read, and path-resolve activity to the console.\nUse when a game cannot find its data files during boot.",
|
||||
"Options.Env.LogNp.Desc": "Log NP (PlayStation Network) library calls to the console.",
|
||||
"Options.Env.Group.Debug": "Debug",
|
||||
"Options.Env.Group.General": "General",
|
||||
"Options.Env.RenderDoc.Desc": "Load the RenderDoc in-application API so frames can be captured from inside the emulator.\nPress F10 while the game runs to capture one frame; captures land in user/logs/capture_logs/<TITLE_ID>.\nRequires RenderDoc to be installed. Slows the GPU down and hangs some titles, so leave it off unless you are debugging.",
|
||||
"Options.Env.GuestImageCpuSync.Desc": "Re-upload guest surfaces the game's own CPU code rewrites.\nLeave off normally. Turn on for titles whose CPU-drawn surfaces never reach the screen.\nCosts performance and regresses some titles, such as GTA V.",
|
||||
"Options.DefaultProfile.Label": "Default profile name",
|
||||
"Options.DefaultProfile.Desc": "Name used when a game asks for text input. Defaults to Sharp.",
|
||||
|
||||
@@ -155,9 +155,6 @@
|
||||
"Options.Env.LogDirectMemory.Desc": "Registrar en la consola las asignaciones de memoria directa y sus fallos.\nÚsalo cuando un juego se aborte o se cierre durante el arranque.",
|
||||
"Options.Env.LogIo.Desc": "Registrar en la consola la apertura y lectura de archivos y la resolución de rutas.\nÚsalo cuando un juego no encuentre sus archivos de datos durante el arranque.",
|
||||
"Options.Env.LogNp.Desc": "Registrar en la consola las llamadas a la biblioteca NP (PlayStation Network).",
|
||||
"Options.Env.Group.Debug": "Depuración",
|
||||
"Options.Env.Group.General": "General",
|
||||
"Options.Env.RenderDoc.Desc": "Carga la API in-application de RenderDoc para poder capturar fotogramas desde el emulador.\nPulsa F10 mientras el juego se ejecuta para capturar un fotograma; las capturas se guardan en user/logs/capture_logs/<TITLE_ID>.\nRequiere tener RenderDoc instalado. Ralentiza la GPU y bloquea algunos títulos, así que déjalo desactivado salvo que estés depurando.",
|
||||
"Options.Env.GuestImageCpuSync.Desc": "Volver a subir las superficies del invitado que reescribe el propio código de CPU del juego.\nDejar desactivado normalmente. Activar en títulos cuyas superficies dibujadas por CPU nunca llegan a la pantalla.\nCuesta rendimiento y causa regresiones en algunos títulos, como GTA V.",
|
||||
"Common.Save": "Guardar",
|
||||
"Common.Cancel": "Cancelar",
|
||||
|
||||
@@ -39,9 +39,6 @@
|
||||
"Options.Env.DumpSpirv.Desc": "Exporter les shaders AGC et leurs traductions SPIR-V dans le dossier shader-dumps.\nÀ utiliser pour signaler des bugs de shader ou de rendu.",
|
||||
"Options.Env.LogDirectMemory.Desc": "Journaliser les allocations de mémoire directe et les échecs dans la console.\nÀ utiliser quand un jeu plante ou se ferme pendant le démarrage.",
|
||||
"Options.Env.LogNp.Desc": "Journaliser les appels de la bibliothèque NP (PlayStation Network) dans la console.",
|
||||
"Options.Env.Group.Debug": "Débogage",
|
||||
"Options.Env.Group.General": "Général",
|
||||
"Options.Env.RenderDoc.Desc": "Charge l'API in-application de RenderDoc afin de capturer des images depuis l'émulateur.\nAppuyez sur F10 pendant le jeu pour capturer une image ; les captures sont écrites dans user/logs/capture_logs/<TITLE_ID>.\nNécessite l'installation de RenderDoc. Ralentit le GPU et bloque certains jeux : laissez cette option désactivée sauf en cas de débogage.",
|
||||
"Options.Env.GuestImageCpuSync.Desc": "Recharger les surfaces invité que le code CPU du jeu réécrit lui-même.\nLaisser désactivé normalement. Activer pour les titres dont les surfaces dessinées par le CPU n'atteignent jamais l'écran.\nCoûte des performances et provoque des régressions sur certains titres, comme GTA V.",
|
||||
"Options.Section.Emulation": "ÉMULATION",
|
||||
"Options.Section.Logging": "JOURNALISATION",
|
||||
|
||||
@@ -39,9 +39,6 @@
|
||||
"Options.Env.DumpSpirv.Desc": "Az AGC-shaderek és azok SPIR-V-fordításainak mentése a shader-dumps mappába.\nHasználd shader- vagy renderelési hibák jelentésekor.",
|
||||
"Options.Env.LogDirectMemory.Desc": "A közvetlen memóriaallokációk és hibák naplózása a konzolra.\nHasználd, ha egy játék a rendszerindítás során megszakad vagy kilép.",
|
||||
"Options.Env.LogNp.Desc": "Az NP (PlayStation Network) könyvtárhívásokat naplózza a konzolra.",
|
||||
"Options.Env.Group.Debug": "Hibakeresés",
|
||||
"Options.Env.Group.General": "Általános",
|
||||
"Options.Env.RenderDoc.Desc": "Betölti a RenderDoc alkalmazáson belüli API-ját, így képkockák rögzíthetők az emulátorból.\nNyomd meg az F10-et futó játék közben egy képkocka rögzítéséhez; a felvételek a user/logs/capture_logs/<TITLE_ID> mappába kerülnek.\nTelepített RenderDocot igényel. Lassítja a GPU-t és egyes játékokat lefagyaszt, ezért hagyd kikapcsolva, hacsak nem hibakeresel.",
|
||||
"Options.Env.GuestImageCpuSync.Desc": "Újratölti azokat a vendégfelületeket, amelyeket a játék saját CPU-kódja ír felül.\nNormál esetben hagyd kikapcsolva. Kapcsold be azoknál a címeknél, amelyek CPU-val rajzolt felületei sosem jutnak ki a képernyőre.\nTeljesítménybe kerül, és egyes címeknél, például a GTA V-nél regressziót okoz.",
|
||||
"Options.Section.Emulation": "EMULÁCIÓ",
|
||||
"Options.Section.Logging": "LOGOLÁS",
|
||||
|
||||
@@ -150,9 +150,6 @@
|
||||
"Options.Env.LogDirectMemory.Desc": "Registra in console le allocazioni di memoria diretta e i relativi errori.\nUsalo quando un gioco si interrompe o si chiude durante l'avvio.",
|
||||
"Options.Env.LogIo.Desc": "Registra in console l'apertura e la lettura dei file e la risoluzione dei percorsi.\nUsalo quando un gioco non trova i propri file di dati durante l'avvio.",
|
||||
"Options.Env.LogNp.Desc": "Registra in console le chiamate alla libreria NP (PlayStation Network).",
|
||||
"Options.Env.Group.Debug": "Debug",
|
||||
"Options.Env.Group.General": "Generale",
|
||||
"Options.Env.RenderDoc.Desc": "Carica l'API in-application di RenderDoc per catturare i frame dall'interno dell'emulatore.\nPremi F10 mentre il gioco è in esecuzione per catturare un frame; le catture finiscono in user/logs/capture_logs/<TITLE_ID>.\nRichiede RenderDoc installato. Rallenta la GPU e blocca alcuni titoli, quindi lascialo disattivato se non stai facendo debug.",
|
||||
"Options.Env.GuestImageCpuSync.Desc": "Ricarica le superfici guest riscritte dal codice CPU del gioco.\nLasciare disattivato normalmente. Attivare per i titoli le cui superfici disegnate dalla CPU non raggiungono mai lo schermo.\nCosta prestazioni e causa regressioni in alcuni titoli, come GTA V.",
|
||||
"Common.Save": "Salva",
|
||||
"Common.Cancel": "Annulla",
|
||||
|
||||
@@ -145,9 +145,6 @@
|
||||
"Options.Env.LogDirectMemory.Desc": "ダイレクトメモリの割り当てと失敗をコンソールに記録します。\nゲームが起動中に中断・終了する場合に使用してください。",
|
||||
"Options.Env.LogIo.Desc": "ファイルのオープン・読み込み・パス解決の動作をコンソールに記録します。\nゲームが起動中にデータファイルを見つけられない場合に使用してください。",
|
||||
"Options.Env.LogNp.Desc": "NP(PlayStation Network)ライブラリの呼び出しをコンソールに記録します。",
|
||||
"Options.Env.Group.Debug": "デバッグ",
|
||||
"Options.Env.Group.General": "一般",
|
||||
"Options.Env.RenderDoc.Desc": "RenderDoc のアプリ内 API を読み込み、エミュレーター内からフレームをキャプチャできるようにします。\nゲーム実行中に F10 を押すと 1 フレームをキャプチャします。保存先は user/logs/capture_logs/<TITLE_ID> です。\nRenderDoc のインストールが必要です。GPU が遅くなり一部のタイトルはハングするため、デバッグ時以外はオフのままにしてください。",
|
||||
"Options.Env.GuestImageCpuSync.Desc": "ゲーム自身の CPU コードが書き換えるゲスト表面を再アップロードします。\n通常はオフのままにしてください。CPU で描画した表面が画面に反映されないタイトルで有効にします。\n性能を犠牲にし、GTA V など一部のタイトルでは不具合が生じます。",
|
||||
"Common.Save": "保存",
|
||||
"Common.Cancel": "キャンセル",
|
||||
|
||||
@@ -145,9 +145,6 @@
|
||||
"Options.Env.LogDirectMemory.Desc": "다이렉트 메모리 할당과 실패를 콘솔에 기록합니다.\n게임이 부팅 중 중단되거나 종료될 때 사용하세요.",
|
||||
"Options.Env.LogIo.Desc": "파일 열기, 읽기, 경로 확인 동작을 콘솔에 기록합니다.\n게임이 부팅 중 데이터 파일을 찾지 못할 때 사용하세요.",
|
||||
"Options.Env.LogNp.Desc": "NP(PlayStation Network) 라이브러리 호출을 콘솔에 기록합니다.",
|
||||
"Options.Env.Group.Debug": "디버그",
|
||||
"Options.Env.Group.General": "일반",
|
||||
"Options.Env.RenderDoc.Desc": "RenderDoc의 인앱 API를 로드하여 에뮬레이터 내부에서 프레임을 캡처할 수 있게 합니다.\n게임 실행 중 F10을 누르면 한 프레임을 캡처하며, 캡처 파일은 user/logs/capture_logs/<TITLE_ID>에 저장됩니다.\nRenderDoc이 설치되어 있어야 합니다. GPU 속도가 느려지고 일부 타이틀은 멈추므로 디버깅할 때가 아니면 꺼 두세요.",
|
||||
"Options.Env.GuestImageCpuSync.Desc": "게임의 자체 CPU 코드가 다시 쓰는 게스트 표면을 다시 업로드합니다.\n평소에는 꺼 두세요. CPU로 그린 표면이 화면에 나타나지 않는 타이틀에서 켜세요.\n성능을 소모하며 GTA V 등 일부 타이틀에서는 문제가 생깁니다.",
|
||||
"Common.Save": "저장",
|
||||
"Common.Cancel": "취소",
|
||||
|
||||
@@ -145,9 +145,6 @@
|
||||
"Options.Env.LogDirectMemory.Desc": "Log directe geheugentoewijzingen en fouten naar de console.\nGebruik dit wanneer een game tijdens het opstarten afbreekt of afsluit.",
|
||||
"Options.Env.LogIo.Desc": "Log het openen en lezen van bestanden en het oplossen van paden naar de console.\nGebruik dit wanneer een game zijn databestanden niet kan vinden tijdens het opstarten.",
|
||||
"Options.Env.LogNp.Desc": "Log NP-bibliotheekaanroepen (PlayStation Network) naar de console.",
|
||||
"Options.Env.Group.Debug": "Debuggen",
|
||||
"Options.Env.Group.General": "Algemeen",
|
||||
"Options.Env.RenderDoc.Desc": "Laadt de RenderDoc in-application-API zodat frames vanuit de emulator kunnen worden vastgelegd.\nDruk op F10 terwijl het spel draait om één frame vast te leggen; opnamen komen in user/logs/capture_logs/<TITLE_ID>.\nVereist een geïnstalleerde RenderDoc. Vertraagt de GPU en laat sommige games vastlopen, dus laat dit uit tenzij je aan het debuggen bent.",
|
||||
"Options.Env.GuestImageCpuSync.Desc": "Gastoppervlakken opnieuw uploaden die de eigen CPU-code van de game herschrijft.\nNormaal uit laten. Inschakelen voor titels waarvan de door de CPU getekende oppervlakken nooit het scherm bereiken.\nKost prestaties en veroorzaakt regressies in sommige titels, zoals GTA V.",
|
||||
"Common.Save": "Opslaan",
|
||||
"Common.Cancel": "Annuleren",
|
||||
|
||||
@@ -39,9 +39,6 @@
|
||||
"Options.Env.DumpSpirv.Desc": "Extrai os shaders AGC e as respetivas traduções SPIR-V para a pasta shader-dumps.\nUtilize ao reportar problemas de shaders ou renderização.",
|
||||
"Options.Env.LogDirectMemory.Desc": "Regista alocações de memória direta e falhas na consola.\nUtilize quando um jogo aborta ou fecha durante o arranque.",
|
||||
"Options.Env.LogNp.Desc": "Regista chamadas da biblioteca NP (PlayStation Network) na consola.",
|
||||
"Options.Env.Group.Debug": "Depuração",
|
||||
"Options.Env.Group.General": "Geral",
|
||||
"Options.Env.RenderDoc.Desc": "Carrega a API in-application do RenderDoc para capturar frames a partir do emulador.\nPrime F10 enquanto o jogo corre para capturar um frame; as capturas vão para user/logs/capture_logs/<TITLE_ID>.\nRequer o RenderDoc instalado. Torna a GPU mais lenta e bloqueia alguns títulos, por isso deixa desativado a menos que estejas a depurar.",
|
||||
"Options.Env.GuestImageCpuSync.Desc": "Recarrega as superfícies do convidado que o próprio código de CPU do jogo reescreve.\nDeixar desativado normalmente. Ativar para títulos cujas superfícies desenhadas pela CPU nunca chegam ao ecrã.\nCusta desempenho e causa regressões em alguns títulos, como GTA V.",
|
||||
"Options.Section.Emulation": "EMULAÇÃO",
|
||||
"Options.Section.Logging": "REGISTOS",
|
||||
|
||||
@@ -42,9 +42,6 @@
|
||||
"Options.Env.LogDirectMemory.Desc": "Выводить в консоль выделения прямой памяти и ошибки выделения.\nИспользуйте, если игра аварийно завершает работу или закрывается при запуске.",
|
||||
"Options.Env.LogIo.Desc": "Выводить в консоль операции открытия и чтения файлов, а также разрешение путей.\nИспользуйте, если игра не может найти файлы данных при запуске.",
|
||||
"Options.Env.LogNp.Desc": "Выводить в консоль вызовы библиотеки NP (PlayStation Network).",
|
||||
"Options.Env.Group.Debug": "Отладка",
|
||||
"Options.Env.Group.General": "Общие",
|
||||
"Options.Env.RenderDoc.Desc": "Загружает внутренний API RenderDoc, чтобы кадры можно было захватывать из эмулятора.\nНажмите F10 во время игры, чтобы захватить один кадр; захваты сохраняются в user/logs/capture_logs/<TITLE_ID>.\nТребует установленного RenderDoc. Замедляет GPU и подвешивает некоторые игры, поэтому оставьте выключенным, если не занимаетесь отладкой.",
|
||||
"Options.Env.GuestImageCpuSync.Desc": "Повторно загружать гостевые поверхности, которые переписывает собственный код ЦП игры.\nОбычно оставляйте выключенным. Включайте для игр, чьи отрисованные ЦП поверхности не попадают на экран.\nСнижает производительность и вызывает регрессии в некоторых играх, например в GTA V.",
|
||||
"Options.Section.Emulation": "ЭМУЛЯЦИЯ",
|
||||
"Options.Section.Logging": "ЛОГГИРОВАНИЕ",
|
||||
|
||||
@@ -179,9 +179,6 @@
|
||||
"Options.Env.LogDirectMemory.Desc": "Doğrudan bellek tahsislerini ve hatalarını konsola günlükle.\nBir oyun açılış sırasında çöküyor veya kapanıyorsa kullanın.",
|
||||
"Options.Env.LogIo.Desc": "Dosya açma, okuma ve yol çözümleme etkinliğini konsola günlükle.\nBir oyun açılışta veri dosyalarını bulamıyorsa kullanın.",
|
||||
"Options.Env.LogNp.Desc": "NP (PlayStation Network) kütüphane çağrılarını konsola günlükle.",
|
||||
"Options.Env.Group.Debug": "Hata Ayıklama",
|
||||
"Options.Env.Group.General": "Genel",
|
||||
"Options.Env.RenderDoc.Desc": "RenderDoc uygulama içi API'sini yükler, böylece kareler emülatörün içinden yakalanabilir.\nOyun çalışırken F10'a basınca bir kare yakalanır; kayıtlar user/logs/capture_logs/<TITLE_ID> altına düşer.\nRenderDoc'un kurulu olmasını gerektirir. GPU'yu yavaşlatır ve bazı oyunları kilitler, hata ayıklamıyorsanız kapalı bırakın.",
|
||||
"Options.Env.GuestImageCpuSync.Desc": "Oyunun kendi CPU kodunun yeniden yazdığı misafir yüzeyleri tekrar yükler.\nNormalde kapalı bırakın. CPU ile çizilen yüzeyleri ekrana ulaşmayan oyunlarda açın.\nPerformansa mal olur ve GTA V gibi bazı oyunlarda soruna yol açar.",
|
||||
"Options.DefaultProfile.Label": "Varsayilan profil adi",
|
||||
"Options.DefaultProfile.Desc": "Oyun metin girisi istediginde kullanilacak ad. Varsayilan deger Sharp'tir.",
|
||||
|
||||
@@ -22,7 +22,6 @@ public partial class MainWindow
|
||||
"SHARPEMU_LOG_IO",
|
||||
"SHARPEMU_LOG_NP",
|
||||
"SHARPEMU_GUEST_IMAGE_CPU_SYNC",
|
||||
"SHARPEMU_RENDERDOC",
|
||||
];
|
||||
|
||||
private readonly List<string> _gameEnvironmentPassthrough = new();
|
||||
@@ -483,7 +482,6 @@ public partial class MainWindow
|
||||
("SHARPEMU_LOG_IO", GameEnvLogIoToggle),
|
||||
("SHARPEMU_LOG_NP", GameEnvLogNpToggle),
|
||||
("SHARPEMU_GUEST_IMAGE_CPU_SYNC", GameEnvGuestImageCpuSyncToggle),
|
||||
("SHARPEMU_RENDERDOC", GameEnvRenderDocToggle),
|
||||
];
|
||||
|
||||
private static void SetGameOptionsOpenClass(Control control, bool active) =>
|
||||
|
||||
@@ -892,13 +892,28 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<StackPanel>
|
||||
<Border Classes="optionsGroup">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Classes="optionsGroupHeader" Text="{Binding [Options.Env.Group.Debug], Source={x:Static local:Localization.Instance}, x:CompileBindings=False}" />
|
||||
<local:SettingRow Classes="optionRow"
|
||||
Label="SHARPEMU_RENDERDOC"
|
||||
Description="{Binding [Options.Env.RenderDoc.Desc],
|
||||
Label="SHARPEMU_BTHID_UNAVAILABLE"
|
||||
Description="{Binding [Options.Env.Bthid.Desc],
|
||||
Source={x:Static local:Localization.Instance},
|
||||
x:CompileBindings=False}">
|
||||
<ToggleSwitch x:Name="GameEnvRenderDocToggle"
|
||||
<ToggleSwitch x:Name="GameEnvBthidToggle"
|
||||
Classes="optionToggle" />
|
||||
</local:SettingRow>
|
||||
<local:SettingRow Classes="optionRow"
|
||||
Label="SHARPEMU_DISABLE_IMPORT_LOOP_GUARD"
|
||||
Description="{Binding [Options.Env.LoopGuard.Desc],
|
||||
Source={x:Static local:Localization.Instance},
|
||||
x:CompileBindings=False}">
|
||||
<ToggleSwitch x:Name="GameEnvLoopGuardToggle"
|
||||
Classes="optionToggle" />
|
||||
</local:SettingRow>
|
||||
<local:SettingRow Classes="optionRow"
|
||||
Label="SHARPEMU_WRITABLE_APP0"
|
||||
Description="{Binding [Options.Env.WritableApp0.Desc],
|
||||
Source={x:Static local:Localization.Instance},
|
||||
x:CompileBindings=False}">
|
||||
<ToggleSwitch x:Name="GameEnvWritableApp0Toggle"
|
||||
Classes="optionToggle" />
|
||||
</local:SettingRow>
|
||||
<local:SettingRow Classes="optionRow"
|
||||
@@ -941,34 +956,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<ToggleSwitch x:Name="GameEnvLogNpToggle"
|
||||
Classes="optionToggle" />
|
||||
</local:SettingRow>
|
||||
|
||||
<Border Classes="optionsGroupDivider" />
|
||||
|
||||
<TextBlock Classes="optionsGroupHeader" Text="{Binding [Options.Env.Group.General], Source={x:Static local:Localization.Instance}, x:CompileBindings=False}" />
|
||||
<local:SettingRow Classes="optionRow"
|
||||
Label="SHARPEMU_BTHID_UNAVAILABLE"
|
||||
Description="{Binding [Options.Env.Bthid.Desc],
|
||||
Source={x:Static local:Localization.Instance},
|
||||
x:CompileBindings=False}">
|
||||
<ToggleSwitch x:Name="GameEnvBthidToggle"
|
||||
Classes="optionToggle" />
|
||||
</local:SettingRow>
|
||||
<local:SettingRow Classes="optionRow"
|
||||
Label="SHARPEMU_DISABLE_IMPORT_LOOP_GUARD"
|
||||
Description="{Binding [Options.Env.LoopGuard.Desc],
|
||||
Source={x:Static local:Localization.Instance},
|
||||
x:CompileBindings=False}">
|
||||
<ToggleSwitch x:Name="GameEnvLoopGuardToggle"
|
||||
Classes="optionToggle" />
|
||||
</local:SettingRow>
|
||||
<local:SettingRow Classes="optionRow"
|
||||
Label="SHARPEMU_WRITABLE_APP0"
|
||||
Description="{Binding [Options.Env.WritableApp0.Desc],
|
||||
Source={x:Static local:Localization.Instance},
|
||||
x:CompileBindings=False}">
|
||||
<ToggleSwitch x:Name="GameEnvWritableApp0Toggle"
|
||||
Classes="optionToggle" />
|
||||
</local:SettingRow>
|
||||
<local:SettingRow Classes="optionRow"
|
||||
Label="SHARPEMU_GUEST_IMAGE_CPU_SYNC"
|
||||
Description="{Binding [Options.Env.GuestImageCpuSync.Desc],
|
||||
@@ -1233,13 +1220,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<StackPanel Spacing="8">
|
||||
<!--Latest commit info-->
|
||||
<Border Classes="optionsInfoRow">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="18">
|
||||
<Image Grid.Column="0"
|
||||
Source="avares://SharpEmu.GUI/Assets/commit-icon.png"
|
||||
Width="20" Height="20"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,12,0" />
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="18">
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<TextBlock x:Name="LatestCommitLabel"
|
||||
Text="{Binding [About.Github.LatestCommitLabel], Source={x:Static local:Localization.Instance}}"
|
||||
FontSize="14"
|
||||
@@ -1250,7 +1232,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
Foreground="{StaticResource SettingsDescriptionBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="2"
|
||||
<Button Grid.Column="1"
|
||||
x:Name="LatestCommitHashText"
|
||||
Classes="optionAction"
|
||||
Content="Loading…"
|
||||
@@ -1263,13 +1245,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
<!--Update-->
|
||||
<Border Classes="optionsInfoRow">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="18">
|
||||
<Image Grid.Column="0"
|
||||
Source="avares://SharpEmu.GUI/Assets/update-icon.png"
|
||||
Width="20" Height="20"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,12,0" />
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="18">
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<TextBlock x:Name="UpdateLabel"
|
||||
Text="{Binding [Updater.Label], Source={x:Static local:Localization.Instance}}"
|
||||
FontSize="14"
|
||||
@@ -1280,7 +1257,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
Foreground="{StaticResource SettingsDescriptionBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="2"
|
||||
<Button Grid.Column="1"
|
||||
x:Name="UpdateButton"
|
||||
Classes="optionAction"
|
||||
Content="Check for updates"
|
||||
@@ -1290,13 +1267,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
<!--Github-->
|
||||
<Border Classes="optionsInfoRow">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="18">
|
||||
<Image Grid.Column="0"
|
||||
Source="avares://SharpEmu.GUI/Assets/github.png"
|
||||
Width="20" Height="20"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,12,0" />
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="18">
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<TextBlock x:Name="GithubLabel"
|
||||
Text="{Binding [About.Github.Label], Source={x:Static local:Localization.Instance}}"
|
||||
FontSize="14"
|
||||
@@ -1307,7 +1279,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
Foreground="{StaticResource SettingsDescriptionBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="2"
|
||||
<Button Grid.Column="1"
|
||||
x:Name="GithubButton"
|
||||
Classes="optionAction"
|
||||
Content="{Binding [About.GithubButton], Source={x:Static local:Localization.Instance}}"
|
||||
@@ -1317,13 +1289,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
<!--Discord-->
|
||||
<Border Classes="optionsInfoRow">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="18">
|
||||
<Image Grid.Column="0"
|
||||
Source="avares://SharpEmu.GUI/Assets/discord.png"
|
||||
Width="20" Height="20"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,12,0" />
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="18">
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<TextBlock x:Name="DiscordServerLabel"
|
||||
Text="{Binding [About.Discord.Label], Source={x:Static local:Localization.Instance}}"
|
||||
FontSize="14"
|
||||
@@ -1334,7 +1301,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
Foreground="{StaticResource SettingsDescriptionBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="2"
|
||||
<TextBlock Grid.Column="1"
|
||||
x:Name="DiscordComingSoonText"
|
||||
Text="{Binding [About.DiscordComingSoon], Source={x:Static local:Localization.Instance}}"
|
||||
FontSize="12"
|
||||
@@ -1440,10 +1407,19 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<StackPanel>
|
||||
<Border Classes="optionsGroup">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Classes="optionsGroupHeader" Text="{Binding [Options.Env.Group.Debug], Source={x:Static local:Localization.Instance}}" />
|
||||
<local:SettingRow x:Name="EnvRenderDocRow" Classes="optionRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_RENDERDOC"
|
||||
Description="{Binding [Options.Env.RenderDoc.Desc], Source={x:Static local:Localization.Instance}}">
|
||||
<ToggleSwitch x:Name="EnvRenderDocToggle" Classes="optionToggle" />
|
||||
<local:SettingRow x:Name="EnvBthidRow" Classes="optionRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_BTHID_UNAVAILABLE"
|
||||
Description="{Binding [Options.Env.Bthid.Desc], Source={x:Static local:Localization.Instance}}">
|
||||
<ToggleSwitch x:Name="EnvBthidToggle" Classes="optionToggle" />
|
||||
</local:SettingRow>
|
||||
|
||||
<local:SettingRow x:Name="EnvLoopGuardRow" Classes="optionRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_DISABLE_IMPORT_LOOP_GUARD"
|
||||
Description="{Binding [Options.Env.LoopGuard.Desc], Source={x:Static local:Localization.Instance}}">
|
||||
<ToggleSwitch x:Name="EnvLoopGuardToggle" Classes="optionToggle" />
|
||||
</local:SettingRow>
|
||||
|
||||
<local:SettingRow x:Name="EnvWritableApp0Row" Classes="optionRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_WRITABLE_APP0"
|
||||
Description="{Binding [Options.Env.WritableApp0.Desc], Source={x:Static local:Localization.Instance}}">
|
||||
<ToggleSwitch x:Name="EnvWritableApp0Toggle" Classes="optionToggle" />
|
||||
</local:SettingRow>
|
||||
|
||||
<local:SettingRow x:Name="EnvVkValidationRow" Classes="optionRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_VK_VALIDATION"
|
||||
@@ -1471,28 +1447,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<ToggleSwitch x:Name="EnvLogNpToggle" Classes="optionToggle" />
|
||||
</local:SettingRow>
|
||||
|
||||
<Border Classes="optionsGroupDivider" />
|
||||
|
||||
<TextBlock Classes="optionsGroupHeader" Text="{Binding [Options.Env.Group.General], Source={x:Static local:Localization.Instance}}" />
|
||||
<local:SettingRow x:Name="EnvBthidRow" Classes="optionRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_BTHID_UNAVAILABLE"
|
||||
Description="{Binding [Options.Env.Bthid.Desc], Source={x:Static local:Localization.Instance}}">
|
||||
<ToggleSwitch x:Name="EnvBthidToggle" Classes="optionToggle" />
|
||||
</local:SettingRow>
|
||||
|
||||
<local:SettingRow x:Name="EnvLoopGuardRow" Classes="optionRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_DISABLE_IMPORT_LOOP_GUARD"
|
||||
Description="{Binding [Options.Env.LoopGuard.Desc], Source={x:Static local:Localization.Instance}}">
|
||||
<ToggleSwitch x:Name="EnvLoopGuardToggle" Classes="optionToggle" />
|
||||
</local:SettingRow>
|
||||
|
||||
<local:SettingRow x:Name="EnvWritableApp0Row" Classes="optionRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_WRITABLE_APP0"
|
||||
Description="{Binding [Options.Env.WritableApp0.Desc], Source={x:Static local:Localization.Instance}}">
|
||||
<ToggleSwitch x:Name="EnvWritableApp0Toggle" Classes="optionToggle" />
|
||||
</local:SettingRow>
|
||||
|
||||
<local:SettingRow x:Name="EnvGuestImageCpuSyncRow" Classes="optionRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_GUEST_IMAGE_CPU_SYNC"
|
||||
Description="{Binding [Options.Env.GuestImageCpuSync.Desc], Source={x:Static local:Localization.Instance}}">
|
||||
<ToggleSwitch x:Name="EnvGuestImageCpuSyncToggle" Classes="optionToggle" />
|
||||
</local:SettingRow>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
|
||||
@@ -309,35 +309,6 @@ public partial class MainWindow : Window
|
||||
Closing += (_, _) => BeginWindowClosing();
|
||||
Closed += (_, _) => CompleteWindowClosing();
|
||||
|
||||
SdlLauncherGamepad.EnsureStarted();
|
||||
_gamepadTimer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(50),
|
||||
};
|
||||
EnvRenderDocToggle.IsCheckedChanged += (_, _) =>
|
||||
|
||||
SetEnvironmentToggle(
|
||||
"SHARPEMU_RENDERDOC",
|
||||
EnvRenderDocToggle.IsChecked == true);
|
||||
DefaultProfileBox.TextChanged += (_, _) =>
|
||||
_settings.DefaultProfile = GuiSettings.NormalizeDefaultProfile(DefaultProfileBox.Text);
|
||||
LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
|
||||
|
||||
GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel);
|
||||
AddHandler(KeyDownEvent, OnPreviewKeyDown, RoutingStrategies.Tunnel);
|
||||
CtxLaunch.Click += (_, _) => LaunchSelected();
|
||||
CtxOpenFolder.Click += (_, _) => OpenSelectedGameFolder();
|
||||
CtxCopyPath.Click += async (_, _) =>
|
||||
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.Path);
|
||||
CtxCopyTitleId.Click += async (_, _) =>
|
||||
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.TitleId);
|
||||
CtxGameSettings.Click += (_, _) => OpenSelectedGameSettings();
|
||||
CtxRemove.Click += (_, _) => RemoveSelectedFromLibrary();
|
||||
|
||||
Opened += async (_, _) => await OnOpenedAsync();
|
||||
Closing += (_, _) => BeginWindowClosing();
|
||||
Closed += (_, _) => CompleteWindowClosing();
|
||||
|
||||
SdlLauncherGamepad.EnsureStarted();
|
||||
_gamepadTimer = new DispatcherTimer
|
||||
{
|
||||
@@ -1213,8 +1184,6 @@ public partial class MainWindow : Window
|
||||
EnvLogNpToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_NP");
|
||||
EnvGuestImageCpuSyncToggle.IsChecked =
|
||||
_settings.EnvironmentToggles.Contains("SHARPEMU_GUEST_IMAGE_CPU_SYNC");
|
||||
EnvRenderDocToggle.IsChecked =
|
||||
_settings.EnvironmentToggles.Contains("SHARPEMU_RENDERDOC");
|
||||
DefaultProfileBox.Text = _settings.DefaultProfile;
|
||||
WindowModeBox.SelectedIndex = ChoiceIndex(_settings.WindowMode, "Windowed", "Borderless", "Exclusive");
|
||||
LoadHostDisplayOptions();
|
||||
|
||||
@@ -42,27 +42,6 @@ Options page section transitions and content layout
|
||||
<Setter Property="Padding" Value="0" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBlock.optionsGroupHeader">
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Foreground" Value="#8CFFFFFF" />
|
||||
<Setter Property="Margin" Value="2,0,0,10" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.optionsGroupDivider">
|
||||
<Setter Property="Height" Value="1" />
|
||||
<Setter Property="Margin" Value="0,18,0,18" />
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Property="Background">
|
||||
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,0%">
|
||||
<GradientStop Offset="0" Color="#00FFFFFF" />
|
||||
<GradientStop Offset="0.06" Color="#2EFFFFFF" />
|
||||
<GradientStop Offset="0.55" Color="#14FFFFFF" />
|
||||
<GradientStop Offset="1" Color="#00FFFFFF" />
|
||||
</LinearGradientBrush>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.optionsInfoRow">
|
||||
<Setter Property="MinHeight" Value="70" />
|
||||
<Setter Property="Padding" Value="18,12" />
|
||||
|
||||
@@ -95,11 +95,8 @@ public static partial class AgcExports
|
||||
private const uint SpiShaderPgmRsrc1Hs = 0x10A;
|
||||
private const uint SpiShaderPgmLoLs = 0x148;
|
||||
private const uint SpiShaderPgmHiLs = 0x149;
|
||||
// Not 0x8A/0x8B - those are SPI_SHADER_PGM_RSRC1/RSRC2_GS, and reading them
|
||||
// as an address yields a 58-bit value (observed live: 0x30004622C008300).
|
||||
private const uint SpiShaderPgmLoGs = 0x88;
|
||||
private const uint SpiShaderPgmHiGs = 0x89;
|
||||
private const uint SpiShaderPgmRsrc1Gs = 0x8A;
|
||||
private const uint SpiShaderPgmLoGs = 0x8A;
|
||||
private const uint SpiShaderPgmHiGs = 0x8B;
|
||||
private const uint SpiShaderPgmChksumGs = 0x80;
|
||||
private const uint SpiPsInputEna = 0x1B3;
|
||||
private const uint SpiPsInputAddr = 0x1B4;
|
||||
@@ -142,15 +139,9 @@ public static partial class AgcExports
|
||||
private const uint CbColor0Base = 0x318;
|
||||
private const uint CbColorRegisterStride = 15;
|
||||
private const uint CbColor0Info = 0x31C;
|
||||
private const uint CbColor0ClearWord0 = 0x323;
|
||||
private const uint CbColor0ClearWord1 = 0x324;
|
||||
private const uint CbColor0BaseExt = 0x390;
|
||||
private const uint CbColor0Attrib2 = 0x3B0;
|
||||
private const uint CbColor0Attrib3 = 0x3B8;
|
||||
// CB_COLORn_INFO.DCC_ENABLE (gc_10_1_0_sh_mask.h). On GFX10 the legacy
|
||||
// FAST_CLEAR and COMPRESSION bits stay clear because DCC, not CMASK,
|
||||
// carries the compression.
|
||||
private const uint CbColorInfoDccEnableMask = 1u << 28;
|
||||
private const uint CbBlend0Control = 0x1E0;
|
||||
private const uint PaScModeCntl0 = 0x292;
|
||||
// GFX10 DB context registers (register byte address minus 0x28000, / 4).
|
||||
@@ -510,8 +501,7 @@ public static partial class AgcExports
|
||||
float ClearRed = 0f,
|
||||
float ClearGreen = 0f,
|
||||
float ClearBlue = 0f,
|
||||
float ClearAlpha = 1f,
|
||||
bool IsDccFastClear = false);
|
||||
float ClearAlpha = 1f);
|
||||
|
||||
private sealed record TranslatedImageBinding(
|
||||
TextureDescriptor Descriptor,
|
||||
@@ -2184,18 +2174,6 @@ public static partial class AgcExports
|
||||
return (int)ctx[CpuRegister.Rax];
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "r98I08t+LOg",
|
||||
ExportName = "sceAgcDcbDrawIndexIndirectMultiGetSize",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int DcbDrawIndexIndirectMultiGetSize(CpuContext ctx)
|
||||
{
|
||||
// Eight, matching the packet DcbDrawIndexIndirectMulti emits.
|
||||
ctx[CpuRegister.Rax] = 8u * sizeof(uint);
|
||||
return (int)ctx[CpuRegister.Rax];
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "rUuVjyR+Rd4",
|
||||
ExportName = "sceAgcDcbGetLodStatsGetSize",
|
||||
@@ -6448,48 +6426,6 @@ public static partial class AgcExports
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test-only view of a parsed graphics context register. False when the
|
||||
/// register was never written.
|
||||
/// </summary>
|
||||
internal static bool TryGetGraphicsContextRegisterForTests(
|
||||
CpuContext ctx,
|
||||
uint registerOffset,
|
||||
out uint value)
|
||||
{
|
||||
value = 0;
|
||||
if (!_submittedGpuStates.TryGetValue(ctx.Memory, out var gpuState))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (gpuState.Gate)
|
||||
{
|
||||
return gpuState.Graphics.CxRegisters.TryGetValue(registerOffset, out value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SH-register counterpart of <see cref="TryGetGraphicsContextRegisterForTests"/>;
|
||||
/// the shader stage addresses live here.
|
||||
/// </summary>
|
||||
internal static bool TryGetGraphicsShRegisterForTests(
|
||||
CpuContext ctx,
|
||||
uint registerOffset,
|
||||
out uint value)
|
||||
{
|
||||
value = 0;
|
||||
if (!_submittedGpuStates.TryGetValue(ctx.Memory, out var gpuState))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (gpuState.Gate)
|
||||
{
|
||||
return gpuState.Graphics.ShRegisters.TryGetValue(registerOffset, out value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GraphicsDcbSetIndexSize writes VGT_INDEX_TYPE via SET_UCONFIG_REG.
|
||||
/// Mirror that into <see cref="SubmittedDcbState.IndexSize"/>.
|
||||
@@ -6872,29 +6808,6 @@ public static partial class AgcExports
|
||||
$"dst=0x{resolveDestination.Address:X16}");
|
||||
}
|
||||
|
||||
// A DCC fast clear writes metadata only; the colour block discards
|
||||
// the quad's shaded output. Reset the attachment and drop the draw,
|
||||
// which reproduces the observable effect of a clear to zero without
|
||||
// modelling DCC block state.
|
||||
if (translatedDraw.IsDccFastClear)
|
||||
{
|
||||
foreach (var target in translatedDraw.GuestTargets)
|
||||
{
|
||||
if (target.Address != 0)
|
||||
{
|
||||
VulkanVideoPresenter.RequestGuestColorClear(target.Address);
|
||||
}
|
||||
}
|
||||
|
||||
ReturnPooledDrawArrays(
|
||||
translatedDraw,
|
||||
globals: true,
|
||||
vertex: true,
|
||||
index: true);
|
||||
state.TranslatedDraw = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var firstTarget = translatedDraw.RenderTargets.FirstOrDefault();
|
||||
if (firstTarget.Address != 0)
|
||||
{
|
||||
@@ -7531,6 +7444,22 @@ public static partial class AgcExports
|
||||
}
|
||||
}
|
||||
|
||||
state.UcRegisters.TryGetValue(VgtPrimitiveType, out var earlyPrimitiveType);
|
||||
if (IsRectListPrimitive(earlyPrimitiveType) &&
|
||||
(exportEvaluation.VertexInputs is null || exportEvaluation.VertexInputs.Count == 0) &&
|
||||
!VertexProgramExportsParameters(exportState.Program) &&
|
||||
GetInterpolatedAttributeCount(pixelState) != 0)
|
||||
{
|
||||
ReturnPooledEvaluationArrays(exportEvaluation);
|
||||
ReturnPooledEvaluationArrays(pixelEvaluation);
|
||||
error =
|
||||
$"rect-list-no-param-exports ps_inputs={GetInterpolatedAttributeCount(pixelState)}";
|
||||
TraceAgcShader(
|
||||
$"agc.rect_list_skip es=0x{exportShaderAddress:X16} " +
|
||||
$"ps=0x{pixelShaderAddress:X16} {error}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Every bound color target the shader exports to. Deferred renderers
|
||||
// draw a multi-render-target G-buffer (up to eight slots) in one pass.
|
||||
// Fall back to slot 0 if we cannot match any export to a bound target.
|
||||
@@ -7811,12 +7740,6 @@ public static partial class AgcExports
|
||||
pixelUserData[index] = pixelEvaluation.InitialScalarRegisters[index];
|
||||
}
|
||||
|
||||
var renderState = ApplyTransparentPremultipliedFillClear(
|
||||
CreateRenderState(state.CxRegisters, renderTargets, pixelColorExportMasks),
|
||||
textures,
|
||||
vertexInputs,
|
||||
pixelEvaluation.InitialScalarRegisters);
|
||||
|
||||
draw = new TranslatedGuestDraw(
|
||||
exportShaderAddress,
|
||||
pixelShaderAddress,
|
||||
@@ -7834,7 +7757,11 @@ public static partial class AgcExports
|
||||
renderTargets,
|
||||
DecodeDepthTarget(state.CxRegisters),
|
||||
guestTargets,
|
||||
renderState,
|
||||
ApplyTransparentPremultipliedFillClear(
|
||||
CreateRenderState(state.CxRegisters, renderTargets, pixelColorExportMasks),
|
||||
textures,
|
||||
vertexInputs,
|
||||
pixelEvaluation.InitialScalarRegisters),
|
||||
pixelUserData,
|
||||
state.CxRegisters.TryGetValue(CbBlend0Control, out var rawBlend) ? rawBlend : 0,
|
||||
state.CxRegisters.TryGetValue(
|
||||
@@ -7848,15 +7775,7 @@ public static partial class AgcExports
|
||||
fullscreenClearColor.Red,
|
||||
fullscreenClearColor.Green,
|
||||
fullscreenClearColor.Blue,
|
||||
fullscreenClearColor.Alpha,
|
||||
IsDccFastClearDraw(
|
||||
state.CxRegisters,
|
||||
renderTargets,
|
||||
textures,
|
||||
vertexInputs,
|
||||
renderState,
|
||||
primitiveType,
|
||||
vertexCount));
|
||||
fullscreenClearColor.Alpha);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -8133,113 +8052,6 @@ public static partial class AgcExports
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recognises the covering quad a GFX10 driver issues to clear a
|
||||
/// DCC-compressed colour target. There is no clear packet: the driver
|
||||
/// programs CB_COLORn_CLEAR_WORD0/1 and draws a quad that the colour block
|
||||
/// turns into DCC clear codes, discarding whatever the pixel shader
|
||||
/// exported. Executing it as an ordinary draw writes the shaded output
|
||||
/// instead, and because the blend it uses computes
|
||||
/// <c>a <- a_src + a_dst * (1 - a_src)</c> - fixed point 1 - the target's
|
||||
/// alpha then climbs every frame and saturates.
|
||||
///
|
||||
/// Restricted to clear-to-zero. The reset performed for a match clears the
|
||||
/// attachment to zero, so a nonzero CLEAR_WORD would be cleared to the
|
||||
/// wrong colour; those fall through and are drawn. Zero is zero under every
|
||||
/// encoding the register can carry, so the pair needs no format handling.
|
||||
///
|
||||
/// The clip-space test is load-bearing rather than belt-and-braces: fills
|
||||
/// sharing the vertex count, topology and blend outnumber the clears by two
|
||||
/// orders of magnitude and sit at coordinates well outside the frame.
|
||||
/// </summary>
|
||||
private const uint TriangleStripPrimitive = 6;
|
||||
|
||||
// A float32x3 vertex position stream (BUF_DATA_FORMAT_32_32_32 / FLOAT).
|
||||
private const uint PositionDataFormat = 13;
|
||||
private const uint PositionNumberFormat = 7;
|
||||
|
||||
private static bool IsDccFastClearDraw(
|
||||
IReadOnlyDictionary<uint, uint> registers,
|
||||
IReadOnlyList<RenderTargetDescriptor> renderTargets,
|
||||
IReadOnlyList<TranslatedImageBinding> textures,
|
||||
IReadOnlyList<Gen5VertexInputBinding> vertexInputs,
|
||||
GuestRenderState renderState,
|
||||
uint primitiveType,
|
||||
uint vertexCount)
|
||||
{
|
||||
if (textures.Count != 0 ||
|
||||
vertexCount != 4 ||
|
||||
primitiveType != TriangleStripPrimitive ||
|
||||
renderTargets.Count == 0 ||
|
||||
renderState.Blends.Count == 0 ||
|
||||
!renderState.Blends.All(IsTransparentPremultipliedFillBlend))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var slotStride = renderTargets[0].Slot * CbColorRegisterStride;
|
||||
return registers.TryGetValue(CbColor0Info + slotStride, out var info) &&
|
||||
(info & CbColorInfoDccEnableMask) != 0 &&
|
||||
registers.TryGetValue(CbColor0ClearWord0 + slotStride, out var clearWord0) &&
|
||||
registers.TryGetValue(CbColor0ClearWord1 + slotStride, out var clearWord1) &&
|
||||
clearWord0 == 0 &&
|
||||
clearWord1 == 0 &&
|
||||
CoversClipSpace(vertexInputs, vertexCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when the draw's float32x3 position stream spans the full clip
|
||||
/// rectangle, i.e. x and y both reach -1 and +1.
|
||||
/// </summary>
|
||||
private static bool CoversClipSpace(
|
||||
IReadOnlyList<Gen5VertexInputBinding> vertexInputs,
|
||||
uint vertexCount)
|
||||
{
|
||||
const float Tolerance = 0.001f;
|
||||
foreach (var input in vertexInputs)
|
||||
{
|
||||
if (input.DataFormat != PositionDataFormat ||
|
||||
input.NumberFormat != PositionNumberFormat)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var stride = input.Stride == 0 ? 12u : input.Stride;
|
||||
var available = Math.Min(input.DataLength, input.Data.Length);
|
||||
float minX = float.MaxValue, maxX = float.MinValue;
|
||||
float minY = float.MaxValue, maxY = float.MinValue;
|
||||
var seen = 0;
|
||||
for (var vertex = 0u; vertex < vertexCount; vertex++)
|
||||
{
|
||||
var at = (int)(input.OffsetBytes + (vertex * stride));
|
||||
if (at + 12 > available)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var position = input.Data.AsSpan(at);
|
||||
var x = BitConverter.ToSingle(position);
|
||||
var y = BitConverter.ToSingle(position[4..]);
|
||||
if (!float.IsFinite(x) || !float.IsFinite(y))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
minX = Math.Min(minX, x);
|
||||
maxX = Math.Max(maxX, x);
|
||||
minY = Math.Min(minY, y);
|
||||
maxY = Math.Max(maxY, y);
|
||||
seen++;
|
||||
}
|
||||
|
||||
return seen >= 3 &&
|
||||
minX <= -1f + Tolerance && maxX >= 1f - Tolerance &&
|
||||
minY <= -1f + Tolerance && maxY >= 1f - Tolerance;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsTransparentPremultipliedFillBlend(GuestBlendState blend) =>
|
||||
blend is
|
||||
{
|
||||
@@ -8424,6 +8236,20 @@ public static partial class AgcExports
|
||||
? (packedMasks >> (int)(target * 4)) & 0xFu
|
||||
: 0;
|
||||
|
||||
private static bool VertexProgramExportsParameters(Gen5ShaderProgram program)
|
||||
{
|
||||
foreach (var instruction in program.Instructions)
|
||||
{
|
||||
if (instruction.Control is Gen5ExportControl export &&
|
||||
export.Target is >= 32 and < 64)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static uint GetInterpolatedAttributeCount(Gen5ShaderState state)
|
||||
{
|
||||
var maxAttribute = -1;
|
||||
@@ -12686,16 +12512,13 @@ public static partial class AgcExports
|
||||
// GTA V Enhanced HS headers start at RSRC1/RSRC2 (0x10A/0x10B) and
|
||||
// omit PGM_LO/HI from the default table. Still succeed: the code VA
|
||||
// lives at ShaderCodeOffset and later binder paths republish it.
|
||||
// GS front headers can likewise start at RSRC1_GS (0x8A) instead of
|
||||
// PGM_LO_GS (0x88) - same deal, skip the patch here.
|
||||
if ((shaderType == HsFrontShaderType && firstLo is SpiShaderPgmRsrc1Hs or SpiShaderPgmLoHs) ||
|
||||
(shaderType == GsFrontShaderType && firstLo is SpiShaderPgmRsrc1Gs or SpiShaderPgmLoGs))
|
||||
if (shaderType == HsFrontShaderType && firstLo is SpiShaderPgmRsrc1Hs or SpiShaderPgmLoHs)
|
||||
{
|
||||
TraceCreateShader(
|
||||
0,
|
||||
headerAddress,
|
||||
codeAddress,
|
||||
$"skip-pgm-patch type={shaderType} first_lo=0x{firstLo:X8}");
|
||||
$"skip-pgm-patch type={HsFrontShaderType} first_lo=0x{firstLo:X8}");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -12850,6 +12673,9 @@ public static partial class AgcExports
|
||||
private static bool IsEsGeometryShaderType(byte shaderType) =>
|
||||
shaderType is GsShaderType or GsBackShaderType;
|
||||
|
||||
private static bool IsRectListPrimitive(uint primitiveType) =>
|
||||
AgcPrimitiveHelpers.IsRectListPrimitive(primitiveType);
|
||||
|
||||
private static int SetIndirectPatchAddress(CpuContext ctx, string registerSpace)
|
||||
{
|
||||
var commandAddress = ctx[CpuRegister.Rdi];
|
||||
|
||||
@@ -72,7 +72,7 @@ public static class AmprExports
|
||||
private const int MaxCachedHostFiles = 1536;
|
||||
private static readonly object _hostFileCacheGate = new();
|
||||
private static readonly Dictionary<string, LinkedListNode<CachedHostFileEntry>> _hostFileByPath =
|
||||
new(HostFsPath.Comparer);
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly LinkedList<CachedHostFileEntry> _hostFileLru = new();
|
||||
|
||||
[SysAbiExport(
|
||||
|
||||
@@ -111,7 +111,7 @@ internal static class AmprFileRegistry
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (string.Equals(_indexedApp0Root, normalizedRoot, HostFsPath.Comparison))
|
||||
if (string.Equals(_indexedApp0Root, normalizedRoot, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -123,7 +123,7 @@ internal static class AmprFileRegistry
|
||||
if (string.Equals(
|
||||
_indexingApp0Root,
|
||||
normalizedRoot,
|
||||
HostFsPath.Comparison))
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Monitor.Wait(_indexGate);
|
||||
continue;
|
||||
@@ -174,34 +174,20 @@ internal static class AmprFileRegistry
|
||||
}
|
||||
|
||||
var relatives = new List<string>(256 * 1024);
|
||||
try
|
||||
foreach (var hostPath in Directory.EnumerateFiles(
|
||||
normalizedRoot,
|
||||
"*",
|
||||
SearchOption.AllDirectories))
|
||||
{
|
||||
foreach (var hostPath in Directory.EnumerateFiles(
|
||||
normalizedRoot,
|
||||
"*",
|
||||
SearchOption.AllDirectories))
|
||||
var relative = Path.GetRelativePath(normalizedRoot, hostPath)
|
||||
.Replace('\\', '/');
|
||||
if (string.IsNullOrEmpty(relative) ||
|
||||
relative.StartsWith("..", StringComparison.Ordinal))
|
||||
{
|
||||
var relative = Path.GetRelativePath(normalizedRoot, hostPath)
|
||||
.Replace('\\', '/');
|
||||
if (string.IsNullOrEmpty(relative) ||
|
||||
relative.StartsWith("..", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
relatives.Add(relative);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
// The walk is an opportunistic warm-up reached synchronously from
|
||||
// sceAmprCommandBufferConstructor; a dump that moves or a mount
|
||||
// that hiccups must not fault the guest export. The background
|
||||
// preload already swallows this. Leave the root unindexed so a
|
||||
// later call retries.
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] ampr.app0_index_walk_failed root={normalizedRoot}: {exception.Message}");
|
||||
return;
|
||||
|
||||
relatives.Add(relative);
|
||||
}
|
||||
|
||||
// Hash + dictionary fill dominates under Rosetta once the walk is
|
||||
@@ -329,10 +315,7 @@ internal static class AmprFileRegistry
|
||||
"ampr-index");
|
||||
Directory.CreateDirectory(cacheDir);
|
||||
|
||||
// Distinct roots must not share a cache file. Folding case is only
|
||||
// correct where the host filesystem folds it too.
|
||||
var rootKey = OperatingSystem.IsWindows() ? normalizedRoot.ToLowerInvariant() : normalizedRoot;
|
||||
var rootHash = ComputeFileId(rootKey);
|
||||
var rootHash = ComputeFileId(normalizedRoot.ToLowerInvariant());
|
||||
return Path.Combine(cacheDir, $"app0-{rootHash:x8}.v{version}.idx");
|
||||
}
|
||||
|
||||
@@ -377,7 +360,7 @@ internal static class AmprFileRegistry
|
||||
}
|
||||
|
||||
var root = reader.ReadString();
|
||||
if (!string.Equals(root, normalizedRoot, HostFsPath.Comparison))
|
||||
if (!string.Equals(root, normalizedRoot, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -487,7 +470,7 @@ internal static class AmprFileRegistry
|
||||
return;
|
||||
}
|
||||
|
||||
var relatives = new HashSet<string>(HostFsPath.Comparer);
|
||||
var relatives = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var hostPath in _hostPathsById.Values)
|
||||
{
|
||||
var relative = Path.GetRelativePath(normalizedRoot, hostPath)
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Libs;
|
||||
|
||||
/// <summary>
|
||||
/// Key equivalence for caches and comparisons over <em>host</em> filesystem
|
||||
/// paths. Windows resolves names case-insensitively, but Linux hosts are
|
||||
/// case-sensitive and the guest filesystem is too, so a dump can legitimately
|
||||
/// contain "DATA.BIN" alongside "Data.bin". An ignore-case cache aliases those
|
||||
/// distinct files into one entry there, which silently serves the wrong bytes
|
||||
/// or drops one of them entirely.
|
||||
/// </summary>
|
||||
internal static class HostFsPath
|
||||
{
|
||||
public static readonly StringComparer Comparer =
|
||||
OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
|
||||
|
||||
public static readonly StringComparison Comparison =
|
||||
OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
|
||||
}
|
||||
@@ -117,12 +117,17 @@ public static partial class KernelMemoryCompatExports
|
||||
private static readonly Dictionary<string, string> _guestMounts = new(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly HashSet<string> _tracedStatResults = new(StringComparer.Ordinal);
|
||||
// Both caches memoize host filesystem probe outcomes, so their key
|
||||
// equivalence must match the host filesystem's — see HostFsPath. On a
|
||||
// case-sensitive host an ignore-case cache aliases distinct paths: a
|
||||
// cached miss for "/app0/DATA.BIN" keeps answering NOT_FOUND for
|
||||
// "/app0/Data.bin" even though that file exists.
|
||||
private static readonly HashSet<string> _negativeStatCache = new(HostFsPath.Comparer);
|
||||
private static readonly ConcurrentDictionary<string, ulong> _aprFileSizeCache = new(HostFsPath.Comparer);
|
||||
// equivalence must match the host filesystem's: Windows resolves names
|
||||
// case-insensitively, but Linux hosts are case-sensitive, and an
|
||||
// ignore-case cache there aliases distinct paths — a cached miss for
|
||||
// "/app0/DATA.BIN" keeps answering NOT_FOUND for "/app0/Data.bin" even
|
||||
// though that file exists and a fresh probe would find it.
|
||||
private static readonly StringComparer HostFsPathComparer =
|
||||
OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
|
||||
private static readonly StringComparison HostFsPathComparison =
|
||||
OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
|
||||
private static readonly HashSet<string> _negativeStatCache = new(HostFsPathComparer);
|
||||
private static readonly ConcurrentDictionary<string, ulong> _aprFileSizeCache = new(HostFsPathComparer);
|
||||
private static long _nextFileDescriptor = 2;
|
||||
private static string _applicationTitleId = "UNKNOWN";
|
||||
|
||||
@@ -5198,8 +5203,8 @@ public static partial class KernelMemoryCompatExports
|
||||
// host would let a relative path escape into a sibling directory that
|
||||
// differs from the mount root only by case (root ".../Save" vs
|
||||
// sibling ".../save").
|
||||
if (!string.Equals(candidate, matchedHostRoot, HostFsPath.Comparison) &&
|
||||
!candidate.StartsWith(rootWithSeparator, HostFsPath.Comparison))
|
||||
if (!string.Equals(candidate, matchedHostRoot, HostFsPathComparison) &&
|
||||
!candidate.StartsWith(rootWithSeparator, HostFsPathComparison))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -5300,8 +5305,8 @@ public static partial class KernelMemoryCompatExports
|
||||
|
||||
var rootWithSeparator =
|
||||
Path.TrimEndingDirectorySeparator(fullRoot) + Path.DirectorySeparatorChar;
|
||||
if (!string.Equals(candidate, fullRoot, HostFsPath.Comparison) &&
|
||||
!candidate.StartsWith(rootWithSeparator, HostFsPath.Comparison))
|
||||
if (!string.Equals(candidate, fullRoot, HostFsPathComparison) &&
|
||||
!candidate.StartsWith(rootWithSeparator, HostFsPathComparison))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
@@ -5327,7 +5332,7 @@ public static partial class KernelMemoryCompatExports
|
||||
private static bool EscapesMountViaReparsePoint(string mountRoot, string candidate)
|
||||
{
|
||||
var rootTrimmed = Path.TrimEndingDirectorySeparator(mountRoot);
|
||||
if (string.Equals(candidate, rootTrimmed, HostFsPath.Comparison))
|
||||
if (string.Equals(candidate, rootTrimmed, HostFsPathComparison))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -918,8 +918,15 @@ public static class KernelPthreadCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||
}
|
||||
|
||||
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
||||
// Several Gen5 runtimes layer their own owner/count bookkeeping
|
||||
// over a NORMAL kernel mutex. Returning EDEADLK here
|
||||
// leaves that guest bookkeeping out of sync with the HLE owner and
|
||||
// turns the wrapper into a permanent lock/unlock retry loop. Keep
|
||||
// the compatibility recursion used by the original implementation;
|
||||
// ERRORCHECK mutexes still take the strict EDEADLK path below.
|
||||
state.RecursionCount++;
|
||||
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1257,15 +1264,15 @@ public static class KernelPthreadCompatExports
|
||||
return CreateImplicitMutexState(ctx, mutexAddress, MutexTypeAdaptiveNp, out resolvedAddress, out state);
|
||||
}
|
||||
|
||||
if (pointedHandle != 0 && pointedHandle != mutexAddress && _mutexStates.TryGetValue(pointedHandle, out state))
|
||||
{
|
||||
_mutexStates[mutexAddress] = state;
|
||||
resolvedAddress = pointedHandle;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (pointedHandle != 0)
|
||||
{
|
||||
if (_mutexStates.TryGetValue(pointedHandle, out state))
|
||||
{
|
||||
_mutexStates.TryAdd(mutexAddress, state);
|
||||
resolvedAddress = pointedHandle;
|
||||
return true;
|
||||
}
|
||||
|
||||
resolvedAddress = pointedHandle;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.VideoOut;
|
||||
|
||||
public static class FlipProgressTracker
|
||||
{
|
||||
private static long _lastFlipTimestamp;
|
||||
private static long _lastFlipVersion;
|
||||
private static int _hasFlipped;
|
||||
|
||||
public static void RecordFlip(long version)
|
||||
{
|
||||
Volatile.Write(ref _lastFlipVersion, version);
|
||||
Volatile.Write(ref _lastFlipTimestamp, Stopwatch.GetTimestamp());
|
||||
Volatile.Write(ref _hasFlipped, 1);
|
||||
}
|
||||
|
||||
public static bool HasFlipped => Volatile.Read(ref _hasFlipped) != 0;
|
||||
|
||||
public static long LastFlipVersion => Volatile.Read(ref _lastFlipVersion);
|
||||
|
||||
public static double? SecondsSinceLastFlip()
|
||||
{
|
||||
if (Volatile.Read(ref _hasFlipped) == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var elapsedTicks = Stopwatch.GetTimestamp() - Volatile.Read(ref _lastFlipTimestamp);
|
||||
return elapsedTicks / (double)Stopwatch.Frequency;
|
||||
}
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.Libs.VideoOut;
|
||||
|
||||
public static unsafe class RenderDocCapture
|
||||
{
|
||||
private const int ApiVersion1_4_2 = 10402;
|
||||
|
||||
private const int IndexUnloadCrashHandler = 10;
|
||||
private const int IndexSetCaptureFilePathTemplate = 11;
|
||||
private const int IndexGetNumCaptures = 13;
|
||||
private const int IndexGetCapture = 14;
|
||||
private const int IndexStartFrameCapture = 19;
|
||||
private const int IndexIsFrameCapturing = 20;
|
||||
private const int IndexEndFrameCapture = 21;
|
||||
|
||||
private const int StateIdle = 0;
|
||||
private const int StateRequested = 1;
|
||||
private const int StateCapturing = 2;
|
||||
|
||||
private static IntPtr* _api;
|
||||
private static int _state = StateIdle;
|
||||
private static bool _initialized;
|
||||
|
||||
public static bool IsAvailable => _api is not null;
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
|
||||
if (!string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_RENDERDOC"),
|
||||
"1",
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryLoadLibrary(out var module))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] renderdoc: SHARPEMU_RENDERDOC=1 was set but renderdoc.dll could not be loaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!NativeLibrary.TryGetExport(module, "RENDERDOC_GetAPI", out var getApiAddress))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] renderdoc: RENDERDOC_GetAPI is missing; in-app capture disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
void* api = null;
|
||||
var getApi = (delegate* unmanaged[Cdecl]<int, void**, int>)getApiAddress;
|
||||
if (getApi(ApiVersion1_4_2, &api) != 1 || api is null)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] renderdoc: API 1.4.2 unavailable; in-app capture disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
_api = (IntPtr*)api;
|
||||
|
||||
((delegate* unmanaged[Cdecl]<void>)_api[IndexUnloadCrashHandler])();
|
||||
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] renderdoc: in-app capture ready. Press F10 to capture the next presented frame.");
|
||||
}
|
||||
|
||||
public static void SetCaptureDirectory(string titleId)
|
||||
{
|
||||
if (_api is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var safeTitleId = string.IsNullOrWhiteSpace(titleId) ? "UNKNOWN" : titleId.Trim();
|
||||
foreach (var invalid in Path.GetInvalidFileNameChars())
|
||||
{
|
||||
safeTitleId = safeTitleId.Replace(invalid, '_');
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var directory = Path.Combine(
|
||||
AppContext.BaseDirectory,
|
||||
"user",
|
||||
"logs",
|
||||
"capture_logs",
|
||||
safeTitleId);
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
var template = Path.Combine(directory, safeTitleId);
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(template + "\0");
|
||||
fixed (byte* pointer = bytes)
|
||||
{
|
||||
((delegate* unmanaged[Cdecl]<byte*, void>)_api[IndexSetCaptureFilePathTemplate])(
|
||||
pointer);
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] renderdoc: captures will be written under '{directory}'.");
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] renderdoc: could not set the capture directory: {exception.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static void RequestCapture()
|
||||
{
|
||||
if (_api is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Interlocked.CompareExchange(ref _state, StateRequested, StateIdle) == StateIdle)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][INFO] renderdoc: capture requested; the next complete presented frame will be captured.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void OnPresent()
|
||||
{
|
||||
if (_api is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (Volatile.Read(ref _state))
|
||||
{
|
||||
case StateIdle:
|
||||
return;
|
||||
|
||||
case StateRequested:
|
||||
if (IsFrameCapturing())
|
||||
{
|
||||
Volatile.Write(ref _state, StateIdle);
|
||||
return;
|
||||
}
|
||||
|
||||
StartFrameCapture();
|
||||
if (!IsFrameCapturing())
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] renderdoc: StartFrameCapture did not begin a capture.");
|
||||
Volatile.Write(ref _state, StateIdle);
|
||||
return;
|
||||
}
|
||||
|
||||
Volatile.Write(ref _state, StateCapturing);
|
||||
return;
|
||||
|
||||
case StateCapturing:
|
||||
var captured = EndFrameCapture() != 0;
|
||||
Volatile.Write(ref _state, StateIdle);
|
||||
if (captured)
|
||||
{
|
||||
LogNewestCapture();
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][WARN] renderdoc: EndFrameCapture failed.");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void StartFrameCapture() =>
|
||||
((delegate* unmanaged[Cdecl]<IntPtr, IntPtr, void>)_api[IndexStartFrameCapture])(
|
||||
IntPtr.Zero,
|
||||
IntPtr.Zero);
|
||||
|
||||
private static bool IsFrameCapturing() =>
|
||||
((delegate* unmanaged[Cdecl]<uint>)_api[IndexIsFrameCapturing])() != 0;
|
||||
|
||||
private static uint EndFrameCapture() =>
|
||||
((delegate* unmanaged[Cdecl]<IntPtr, IntPtr, uint>)_api[IndexEndFrameCapture])(
|
||||
IntPtr.Zero,
|
||||
IntPtr.Zero);
|
||||
|
||||
private static void LogNewestCapture()
|
||||
{
|
||||
var count = ((delegate* unmanaged[Cdecl]<uint>)_api[IndexGetNumCaptures])();
|
||||
if (count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var getCapture =
|
||||
(delegate* unmanaged[Cdecl]<uint, byte*, uint*, ulong*, uint>)_api[IndexGetCapture];
|
||||
|
||||
uint pathLength = 0;
|
||||
if (getCapture(count - 1, null, &pathLength, null) == 0 || pathLength == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var buffer = new byte[pathLength];
|
||||
fixed (byte* bufferPointer = buffer)
|
||||
{
|
||||
if (getCapture(count - 1, bufferPointer, &pathLength, null) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var path = System.Text.Encoding.UTF8.GetString(buffer).TrimEnd('\0');
|
||||
Console.Error.WriteLine($"[LOADER][INFO] renderdoc: capture written to '{path}'.");
|
||||
}
|
||||
|
||||
private static bool TryLoadLibrary(out IntPtr module)
|
||||
{
|
||||
var configured = Environment.GetEnvironmentVariable("SHARPEMU_RENDERDOC_DLL");
|
||||
if (!string.IsNullOrWhiteSpace(configured) &&
|
||||
NativeLibrary.TryLoad(configured, out module))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var name = OperatingSystem.IsWindows() ? "renderdoc.dll" : "librenderdoc.so";
|
||||
if (NativeLibrary.TryLoad(name, out module))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var candidate in KnownLibraryPaths(name))
|
||||
{
|
||||
if (File.Exists(candidate) && NativeLibrary.TryLoad(candidate, out module))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
module = IntPtr.Zero;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> KnownLibraryPaths(string name)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
yield return "/usr/lib/librenderdoc.so";
|
||||
yield return "/usr/local/lib/librenderdoc.so";
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var variable in (string[])["ProgramFiles", "ProgramW6432", "ProgramFiles(x86)"])
|
||||
{
|
||||
var root = Environment.GetEnvironmentVariable(variable);
|
||||
if (!string.IsNullOrWhiteSpace(root))
|
||||
{
|
||||
yield return Path.Combine(root, "RenderDoc", name);
|
||||
}
|
||||
}
|
||||
|
||||
var localAppData = Environment.GetEnvironmentVariable("LOCALAPPDATA");
|
||||
if (!string.IsNullOrWhiteSpace(localAppData))
|
||||
{
|
||||
yield return Path.Combine(localAppData, "RenderDoc", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -487,10 +487,6 @@ internal sealed unsafe class SdlHostWindow : IDisposable, IHostGamepadOutput
|
||||
{
|
||||
PerfOverlay.Toggle();
|
||||
}
|
||||
else if (keyEvent.key == SDL_Keycode.SDLK_F10)
|
||||
{
|
||||
RenderDocCapture.RequestCapture();
|
||||
}
|
||||
else if (keyEvent.key == SDL_Keycode.SDLK_F11)
|
||||
{
|
||||
ToggleFullscreen();
|
||||
|
||||
@@ -122,8 +122,6 @@ public static class VideoOutExports
|
||||
: titleId.Trim();
|
||||
_applicationWindowTitle = $"{application}{versionSuffix}";
|
||||
}
|
||||
|
||||
RenderDocCapture.SetCaptureDirectory(GetApplicationTitleId());
|
||||
}
|
||||
|
||||
internal static string GetApplicationTitleId()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -712,8 +712,6 @@ public static partial class Gen5SpirvTranslator
|
||||
if (UsesSubgroupOperations())
|
||||
{
|
||||
_module.AddCapability(SpirvCapability.GroupNonUniform);
|
||||
_module.AddCapability(SpirvCapability.GroupNonUniformBallot);
|
||||
|
||||
if (UsesSubgroupShuffle())
|
||||
{
|
||||
_module.AddCapability(SpirvCapability.GroupNonUniformShuffle);
|
||||
@@ -724,6 +722,10 @@ public static partial class Gen5SpirvTranslator
|
||||
_module.AddCapability(SpirvCapability.GroupNonUniformVote);
|
||||
}
|
||||
|
||||
if (UsesSubgroupBroadcast() || UsesWaveControl())
|
||||
{
|
||||
_module.AddCapability(SpirvCapability.GroupNonUniformBallot);
|
||||
}
|
||||
}
|
||||
|
||||
_glsl = _module.ImportExtInst("GLSL.std.450");
|
||||
@@ -1801,16 +1803,13 @@ public static partial class Gen5SpirvTranslator
|
||||
|
||||
if (instruction.Opcode == "SBarrier")
|
||||
{
|
||||
if (_stage == Gen5SpirvStage.Compute)
|
||||
{
|
||||
var workgroup = UInt(2);
|
||||
var semantics = UInt(0x108);
|
||||
_module.AddStatement(
|
||||
SpirvOp.ControlBarrier,
|
||||
workgroup,
|
||||
workgroup,
|
||||
semantics);
|
||||
}
|
||||
var workgroup = UInt(2);
|
||||
var semantics = UInt(0x108);
|
||||
_module.AddStatement(
|
||||
SpirvOp.ControlBarrier,
|
||||
workgroup,
|
||||
workgroup,
|
||||
semantics);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Agc;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
/// <summary>
|
||||
/// Coverage for the graphics context-register path in the PM4 parser. Draw
|
||||
/// translation reads render state out of this dictionary (CB_TARGET_MASK
|
||||
/// decides whether a draw writes alpha, CB_COLOR_CONTROL decides what the draw
|
||||
/// means), so a write that lands under the wrong key, or fails to overwrite an
|
||||
/// earlier one, silently changes what every later draw does. These drive real
|
||||
/// PM4 packets through the public submit export and assert what the parser
|
||||
/// retained.
|
||||
/// </summary>
|
||||
public sealed class AgcContextRegisterTests
|
||||
{
|
||||
private const ulong BaseAddress = 0x2_0000_0000;
|
||||
private const ulong SubmitPacketAddress = BaseAddress + 0x40;
|
||||
private const ulong CommandAddress = BaseAddress + 0x200;
|
||||
private const ulong IndirectTableAddress = BaseAddress + 0x600;
|
||||
|
||||
private const uint ItNop = 0x10;
|
||||
private const uint ItSetContextReg = 0x69;
|
||||
private const uint RCxRegsIndirect = 0x12;
|
||||
private const uint CbTargetMask = 0x8E;
|
||||
private const uint CbColorControl = 0x202;
|
||||
|
||||
// PM4 type-3 header: 0xC0000000 | ((dwords - 2) << 16) | (opcode << 8), with the
|
||||
// NOP sub-register in bits 2..7 — the parser reads it as (header >> 2) & 0x3F.
|
||||
private static uint Pm4Header(uint dwords, uint opcode, uint register = 0) =>
|
||||
0xC000_0000u | ((dwords - 2) << 16) | (opcode << 8) | ((register & 0x3Fu) << 2);
|
||||
|
||||
[Fact]
|
||||
public void SetContextRegRetainsTargetMask()
|
||||
{
|
||||
var ctx = CreateContext(out var memory);
|
||||
WriteDwords(
|
||||
memory,
|
||||
CommandAddress,
|
||||
Pm4Header(3, ItSetContextReg),
|
||||
CbTargetMask,
|
||||
0x0000_0007u);
|
||||
Submit(ctx, memory, dwordCount: 3);
|
||||
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsContextRegisterForTests(ctx, CbTargetMask, out var value));
|
||||
Assert.Equal(0x0000_0007u, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The indirect form carries (offset, value) pairs out of guest memory
|
||||
/// rather than inline dwords, so an offset-encoding mismatch here would
|
||||
/// store the register under a key no reader looks at.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IndirectRegisterWriteRetainsTargetMask()
|
||||
{
|
||||
var ctx = CreateContext(out var memory);
|
||||
WriteIndirectRegisterCommand(memory, (CbTargetMask, 0xFFFF_FFFFu));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsContextRegisterForTests(ctx, CbTargetMask, out var value));
|
||||
Assert.Equal(0xFFFF_FFFFu, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Context registers persist across submissions on hardware until something
|
||||
/// clears them, so a mask written in one submission has to still be there
|
||||
/// for a draw in the next.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TargetMaskSurvivesASecondSubmission()
|
||||
{
|
||||
var ctx = CreateContext(out var memory);
|
||||
WriteIndirectRegisterCommand(memory, (CbTargetMask, 0x8888_8888u));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
// A second, unrelated submission: a bare NOP that touches no registers.
|
||||
WriteDwords(memory, CommandAddress, Pm4Header(2, ItNop), 0);
|
||||
Submit(ctx, memory, dwordCount: 2);
|
||||
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsContextRegisterForTests(ctx, CbTargetMask, out var value));
|
||||
Assert.Equal(0x8888_8888u, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Both encodings must land on the same key, or a title that sets the
|
||||
/// register one way and a reader that expects the other silently disagree.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DirectAndIndirectWritesShareOneKey()
|
||||
{
|
||||
var ctx = CreateContext(out var memory);
|
||||
WriteDwords(
|
||||
memory,
|
||||
CommandAddress,
|
||||
Pm4Header(3, ItSetContextReg),
|
||||
CbTargetMask,
|
||||
0x0000_0007u);
|
||||
Submit(ctx, memory, dwordCount: 3);
|
||||
|
||||
WriteIndirectRegisterCommand(memory, (CbTargetMask, 0x0000_000Fu));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsContextRegisterForTests(ctx, CbTargetMask, out var value));
|
||||
Assert.Equal(0x0000_000Fu, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CB_COLOR_CONTROL (0x202) MODE bits [6:4] give Normal=1,
|
||||
/// EliminateFastClear=2, Resolve=3, FmaskDecompress=5, DccDecompress=6. The
|
||||
/// value has to survive the parser intact, ROP3 bits and all, because the
|
||||
/// mode decides whether a draw shades or resolves.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(0x0000_0010u, 1u)] // Normal
|
||||
[InlineData(0x0000_0020u, 2u)] // EliminateFastClear
|
||||
[InlineData(0x00CC_0060u, 6u)] // DccDecompress, with ROP3=0xCC alongside
|
||||
public void ColorControlRetainsMode(uint written, uint expectedMode)
|
||||
{
|
||||
var ctx = CreateContext(out var memory);
|
||||
WriteIndirectRegisterCommand(memory, (CbColorControl, written));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsContextRegisterForTests(ctx, CbColorControl, out var value));
|
||||
Assert.Equal(written, value);
|
||||
Assert.Equal(expectedMode, (value >> 4) & 0x7u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A later write must win. If the parser kept the first value, a draw that
|
||||
/// sets EliminateFastClear after an earlier Normal would still read Normal
|
||||
/// and the clear would be silently dropped.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ColorControlLaterWriteOverwritesEarlier()
|
||||
{
|
||||
var ctx = CreateContext(out var memory);
|
||||
WriteIndirectRegisterCommand(memory, (CbColorControl, 0x00CC_0010u));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
WriteIndirectRegisterCommand(memory, (CbColorControl, 0x00CC_0020u));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsContextRegisterForTests(ctx, CbColorControl, out var value));
|
||||
Assert.Equal(0x00CC_0020u, value);
|
||||
Assert.Equal(2u, (value >> 4) & 0x7u);
|
||||
}
|
||||
|
||||
private static void WriteIndirectRegisterCommand(
|
||||
FakeCpuMemory memory,
|
||||
params (uint Offset, uint Value)[] registers)
|
||||
{
|
||||
WriteDwords(
|
||||
memory,
|
||||
CommandAddress,
|
||||
Pm4Header(4, ItNop, RCxRegsIndirect),
|
||||
(uint)registers.Length,
|
||||
(uint)(IndirectTableAddress & 0xFFFF_FFFFu),
|
||||
(uint)(IndirectTableAddress >> 32));
|
||||
|
||||
for (var index = 0; index < registers.Length; index++)
|
||||
{
|
||||
var entry = IndirectTableAddress + ((ulong)index * 8);
|
||||
WriteUInt32(memory, entry, registers[index].Offset);
|
||||
WriteUInt32(memory, entry + 4, registers[index].Value);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Submit(CpuContext ctx, FakeCpuMemory memory, uint dwordCount)
|
||||
{
|
||||
WriteUInt64(memory, SubmitPacketAddress, CommandAddress);
|
||||
WriteUInt32(memory, SubmitPacketAddress + 8, dwordCount);
|
||||
ctx[CpuRegister.Rdi] = SubmitPacketAddress;
|
||||
AgcExports.DriverSubmitDcb(ctx);
|
||||
}
|
||||
|
||||
private static CpuContext CreateContext(out FakeCpuMemory memory)
|
||||
{
|
||||
memory = new FakeCpuMemory(BaseAddress, 0x1000);
|
||||
return new CpuContext(memory, Generation.Gen5);
|
||||
}
|
||||
|
||||
private static void WriteDwords(FakeCpuMemory memory, ulong address, params uint[] values)
|
||||
{
|
||||
for (var index = 0; index < values.Length; index++)
|
||||
{
|
||||
WriteUInt32(memory, address + ((ulong)index * sizeof(uint)), values[index]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
Assert.True(memory.TryWrite(address, buffer));
|
||||
}
|
||||
|
||||
private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||
Assert.True(memory.TryWrite(address, buffer));
|
||||
}
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Agc;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
/// <summary>
|
||||
/// Coverage for the SH-register path in the PM4 parser. A draw resolves its
|
||||
/// vertex stage from SPI_SHADER_PGM_LO_ES/HI_ES and its pixel stage from
|
||||
/// SPI_SHADER_PGM_LO_PS/HI_PS, both out of this dictionary, so a key that is
|
||||
/// dropped or written under a different encoding pairs a current pixel shader
|
||||
/// with a stale vertex shader — a failure that produces plausible-looking
|
||||
/// garbage rather than an error. These drive real PM4 packets through the
|
||||
/// public submit export and assert what the parser retained.
|
||||
/// </summary>
|
||||
public sealed class AgcShaderStageRegisterTests
|
||||
{
|
||||
private const ulong BaseAddress = 0x2_0000_0000;
|
||||
private const ulong SubmitPacketAddress = BaseAddress + 0x40;
|
||||
private const ulong CommandAddress = BaseAddress + 0x200;
|
||||
private const ulong IndirectTableAddress = BaseAddress + 0x600;
|
||||
|
||||
private const uint ItNop = 0x10;
|
||||
private const uint ItSetShReg = 0x76;
|
||||
private const uint RShRegsIndirect = 0x11;
|
||||
|
||||
// SH register offsets. ES is the vertex stage on GFX10 — the standalone
|
||||
// PGM_LO/HI_GS pair is dead post-GCN and the merged ES/GS stage is addressed
|
||||
// through ES.
|
||||
private const uint SpiShaderPgmLoPs = 0x8;
|
||||
private const uint SpiShaderPgmLoEs = 0xC8;
|
||||
private const uint SpiShaderPgmHiEs = 0xC9;
|
||||
|
||||
private static uint Pm4Header(uint dwords, uint opcode, uint register = 0) =>
|
||||
0xC000_0000u | ((dwords - 2) << 16) | (opcode << 8) | ((register & 0x3Fu) << 2);
|
||||
|
||||
/// <summary>
|
||||
/// The baseline: a direct SET_SH_REG write of the vertex stage address has
|
||||
/// to be readable afterwards. If this fails, nothing downstream can pair
|
||||
/// shaders correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SetShRegRetainsExportShaderAddress()
|
||||
{
|
||||
var ctx = CreateContext(out var memory);
|
||||
WriteDwords(
|
||||
memory,
|
||||
CommandAddress,
|
||||
Pm4Header(3, ItSetShReg),
|
||||
SpiShaderPgmLoEs,
|
||||
0x0044_8582u);
|
||||
Submit(ctx, memory, dwordCount: 3);
|
||||
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsShRegisterForTests(ctx, SpiShaderPgmLoEs, out var value));
|
||||
Assert.Equal(0x0044_8582u, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The indirect encoding must land on the same keys as the direct one. A
|
||||
/// mismatch would store the stage address where the draw never reads it,
|
||||
/// leaving the draw to see whatever a previous submission left behind.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IndirectShRegisterWriteRetainsExportShaderAddress()
|
||||
{
|
||||
var ctx = CreateContext(out var memory);
|
||||
WriteIndirectShRegisterCommand(memory, (SpiShaderPgmLoEs, 0x0044_8DD1u));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsShRegisterForTests(ctx, SpiShaderPgmLoEs, out var value));
|
||||
Assert.Equal(0x0044_8DD1u, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Both stages written in one submission must both read back as written. If
|
||||
/// the vertex stage kept an older value while the pixel stage updated, every
|
||||
/// draw after it would be mis-paired.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BothStagesUpdateTogetherWithinOneSubmission()
|
||||
{
|
||||
var ctx = CreateContext(out var memory);
|
||||
WriteIndirectShRegisterCommand(
|
||||
memory,
|
||||
(SpiShaderPgmLoEs, 0x0080_2933u),
|
||||
(SpiShaderPgmLoPs, 0x0044_858Au));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
WriteIndirectShRegisterCommand(
|
||||
memory,
|
||||
(SpiShaderPgmLoEs, 0x0044_8581u),
|
||||
(SpiShaderPgmLoPs, 0x0044_8719u));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsShRegisterForTests(ctx, SpiShaderPgmLoEs, out var es));
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsShRegisterForTests(ctx, SpiShaderPgmLoPs, out var ps));
|
||||
Assert.Equal(0x0044_8581u, es);
|
||||
Assert.Equal(0x0044_8719u, ps);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updating only the pixel stage must leave the vertex stage at its previous
|
||||
/// value rather than dropping the key, or the draw falls back to whatever
|
||||
/// default the resolver finds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void PixelStageUpdateLeavesExportStageIntact()
|
||||
{
|
||||
var ctx = CreateContext(out var memory);
|
||||
WriteIndirectShRegisterCommand(memory, (SpiShaderPgmLoEs, 0x0044_8582u));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
WriteIndirectShRegisterCommand(memory, (SpiShaderPgmLoPs, 0x0044_858Au));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsShRegisterForTests(ctx, SpiShaderPgmLoEs, out var es));
|
||||
Assert.Equal(0x0044_8582u, es);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stage addresses are 64-bit: LO carries bits 39:8 and HI the top bits, and
|
||||
/// the draw combines them. A HI retained from an earlier shader while LO
|
||||
/// updates resolves to a splice of two different programs.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void HighAndLowHalvesUpdateTogether()
|
||||
{
|
||||
var ctx = CreateContext(out var memory);
|
||||
WriteIndirectShRegisterCommand(
|
||||
memory,
|
||||
(SpiShaderPgmLoEs, 0x0080_2933u),
|
||||
(SpiShaderPgmHiEs, 0x0000_0008u));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
WriteIndirectShRegisterCommand(
|
||||
memory,
|
||||
(SpiShaderPgmLoEs, 0x0044_8582u),
|
||||
(SpiShaderPgmHiEs, 0x0000_0004u));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsShRegisterForTests(ctx, SpiShaderPgmLoEs, out var lo));
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsShRegisterForTests(ctx, SpiShaderPgmHiEs, out var hi));
|
||||
Assert.Equal(0x0044_8582u, lo);
|
||||
Assert.Equal(0x0000_0004u, hi);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SH registers persist across submissions on hardware. A stage address set
|
||||
/// in one submission must still be there for a draw in the next.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ExportShaderAddressSurvivesASecondSubmission()
|
||||
{
|
||||
var ctx = CreateContext(out var memory);
|
||||
WriteIndirectShRegisterCommand(memory, (SpiShaderPgmLoEs, 0x0044_8583u));
|
||||
Submit(ctx, memory, dwordCount: 4);
|
||||
|
||||
WriteDwords(memory, CommandAddress, Pm4Header(2, ItNop), 0);
|
||||
Submit(ctx, memory, dwordCount: 2);
|
||||
|
||||
Assert.True(
|
||||
AgcExports.TryGetGraphicsShRegisterForTests(ctx, SpiShaderPgmLoEs, out var value));
|
||||
Assert.Equal(0x0044_8583u, value);
|
||||
}
|
||||
|
||||
private static void WriteIndirectShRegisterCommand(
|
||||
FakeCpuMemory memory,
|
||||
params (uint Offset, uint Value)[] registers)
|
||||
{
|
||||
WriteDwords(
|
||||
memory,
|
||||
CommandAddress,
|
||||
Pm4Header(4, ItNop, RShRegsIndirect),
|
||||
(uint)registers.Length,
|
||||
(uint)(IndirectTableAddress & 0xFFFF_FFFFu),
|
||||
(uint)(IndirectTableAddress >> 32));
|
||||
|
||||
for (var index = 0; index < registers.Length; index++)
|
||||
{
|
||||
var entry = IndirectTableAddress + ((ulong)index * 8);
|
||||
WriteUInt32(memory, entry, registers[index].Offset);
|
||||
WriteUInt32(memory, entry + 4, registers[index].Value);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Submit(CpuContext ctx, FakeCpuMemory memory, uint dwordCount)
|
||||
{
|
||||
WriteUInt64(memory, SubmitPacketAddress, CommandAddress);
|
||||
WriteUInt32(memory, SubmitPacketAddress + 8, dwordCount);
|
||||
ctx[CpuRegister.Rdi] = SubmitPacketAddress;
|
||||
AgcExports.DriverSubmitDcb(ctx);
|
||||
}
|
||||
|
||||
private static CpuContext CreateContext(out FakeCpuMemory memory)
|
||||
{
|
||||
memory = new FakeCpuMemory(BaseAddress, 0x1000);
|
||||
return new CpuContext(memory, Generation.Gen5);
|
||||
}
|
||||
|
||||
private static void WriteDwords(FakeCpuMemory memory, ulong address, params uint[] values)
|
||||
{
|
||||
for (var index = 0; index < values.Length; index++)
|
||||
{
|
||||
WriteUInt32(memory, address + ((ulong)index * sizeof(uint)), values[index]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
Assert.True(memory.TryWrite(address, buffer));
|
||||
}
|
||||
|
||||
private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||
Assert.True(memory.TryWrite(address, buffer));
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,6 @@ using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Ampr;
|
||||
|
||||
// AmprFileRegistry is process-global static state, so the classes that index
|
||||
// or clear it must not run concurrently with each other.
|
||||
[Collection("AmprFileRegistry")]
|
||||
public class AmprFileRegistryTests
|
||||
{
|
||||
[Fact]
|
||||
@@ -65,79 +62,6 @@ public class AmprFileRegistryTests
|
||||
Assert.Equal(host, d);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void App0_index_cache_keeps_files_that_differ_only_by_case()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), "sharpemu-ampr-case-" + Guid.NewGuid().ToString("N"));
|
||||
var cacheDir = Path.Combine(root, "..", "sharpemu-ampr-cache-" + Guid.NewGuid().ToString("N"));
|
||||
var upper = Path.Combine(root, "data", "ASSET.bin");
|
||||
var lower = Path.Combine(root, "data", "asset.bin");
|
||||
var previousCacheDir = Environment.GetEnvironmentVariable("SHARPEMU_AMPR_INDEX_CACHE");
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(Path.Combine(root, "sce_sys"));
|
||||
Directory.CreateDirectory(Path.Combine(root, "data"));
|
||||
File.WriteAllText(Path.Combine(root, "sce_sys", "param.json"), "{}");
|
||||
File.WriteAllBytes(upper, [1, 2, 3]);
|
||||
if (File.Exists(lower))
|
||||
{
|
||||
// Case-insensitive host: the two names are one file, so there is
|
||||
// nothing for an ignore-case index to lose.
|
||||
return;
|
||||
}
|
||||
|
||||
File.WriteAllBytes(lower, [4, 5, 6]);
|
||||
Environment.SetEnvironmentVariable("SHARPEMU_AMPR_INDEX_CACHE", cacheDir);
|
||||
|
||||
var normalizedRoot = Path.GetFullPath(root);
|
||||
var expectedUpper = Path.Combine(normalizedRoot, "data", "ASSET.bin");
|
||||
var expectedLower = Path.Combine(normalizedRoot, "data", "asset.bin");
|
||||
|
||||
// Fresh tree walk, which also writes the on-disk index cache.
|
||||
AmprFileRegistry.ClearForTests();
|
||||
AmprFileRegistry.EnsureApp0Indexed(root);
|
||||
AssertResolves(expectedUpper, expectedLower);
|
||||
|
||||
// Second boot: served from the cache the walk just wrote.
|
||||
AmprFileRegistry.ClearForTests();
|
||||
AmprFileRegistry.EnsureApp0Indexed(root);
|
||||
AssertResolves(expectedUpper, expectedLower);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SHARPEMU_AMPR_INDEX_CACHE", previousCacheDir);
|
||||
AmprFileRegistry.ClearForTests();
|
||||
TryDeleteDirectory(cacheDir);
|
||||
TryDeleteDirectory(root);
|
||||
}
|
||||
|
||||
static void AssertResolves(string expectedUpper, string expectedLower)
|
||||
{
|
||||
Assert.True(
|
||||
AmprFileRegistry.TryGetHostPath(
|
||||
AmprFileRegistry.ComputeFileId("$/data/ASSET.bin"), out var actualUpper),
|
||||
"data/ASSET.bin is missing from the app0 index.");
|
||||
Assert.True(
|
||||
AmprFileRegistry.TryGetHostPath(
|
||||
AmprFileRegistry.ComputeFileId("$/data/asset.bin"), out var actualLower),
|
||||
"data/asset.bin is missing from the app0 index.");
|
||||
Assert.Equal(expectedUpper, actualUpper);
|
||||
Assert.Equal(expectedLower, actualLower);
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryDeleteDirectory(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(path, recursive: true);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Temp cleanup is best-effort.
|
||||
}
|
||||
}
|
||||
|
||||
private static uint FnvUtf8(string text)
|
||||
{
|
||||
const uint offset = 2166136261;
|
||||
|
||||
@@ -8,7 +8,6 @@ using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Ampr;
|
||||
|
||||
[Collection("AmprFileRegistry")]
|
||||
public sealed class AmprWriteAddressTests
|
||||
{
|
||||
[Fact]
|
||||
|
||||
@@ -9,7 +9,6 @@ using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Ampr;
|
||||
|
||||
[Collection("AmprFileRegistry")]
|
||||
public sealed class AprStreamingContractTests
|
||||
{
|
||||
[Fact]
|
||||
|
||||
@@ -104,45 +104,6 @@ public sealed class GuestMemoryAllocatorTests
|
||||
Assert.Equal(0UL, (ulong)memory.GetPointer(address));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjacentFixedGuestPageMappingsShareAHostGranule()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var memory = new PhysicalVirtualMemory(new GranularityAwareHostMemory());
|
||||
const ulong baseAddress = 0x0000008001600000;
|
||||
|
||||
Assert.Equal(baseAddress, memory.AllocateAt(baseAddress, 0x4000, executable: false, allowAlternative: false));
|
||||
Assert.Equal(
|
||||
baseAddress + 0x4000,
|
||||
memory.AllocateAt(baseAddress + 0x4000, 0x4000, executable: false, allowAlternative: false));
|
||||
Assert.Equal(
|
||||
baseAddress + 0x8000,
|
||||
memory.AllocateAt(baseAddress + 0x8000, 0x8000, executable: false, allowAlternative: false));
|
||||
|
||||
Assert.True(memory.IsAccessible(baseAddress, 0x10000));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryBackFixedRangeSharesAHostGranuleAcrossCallsOnWindows()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var memory = new PhysicalVirtualMemory(new GranularityAwareHostMemory());
|
||||
const ulong baseAddress = 0x0000008001600000;
|
||||
|
||||
Assert.True(memory.TryBackFixedRange(baseAddress, 0x4000, executable: false));
|
||||
Assert.True(memory.TryBackFixedRange(baseAddress + 0x4000, 0x4000, executable: false));
|
||||
|
||||
Assert.True(memory.IsAccessible(baseAddress, 0x8000));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AlignedAllocationDoesNotRetainOverallocatedMappingsOutsideMacOS()
|
||||
{
|
||||
@@ -172,11 +133,6 @@ public sealed class GuestMemoryAllocatorTests
|
||||
[Fact]
|
||||
public void TryBackFixedRangeRollsBackEarlierGapsWhenLaterGapCannotBeBacked()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Layout: committed | free | committed | free
|
||||
// First free gap allocates successfully, second fails.
|
||||
// The first allocation must be freed — nothing should leak.
|
||||
@@ -198,11 +154,6 @@ public sealed class GuestMemoryAllocatorTests
|
||||
[Fact]
|
||||
public void TryBackFixedRangeFillsOnlyTheFreePagesOfAPartiallyOccupiedRange()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const ulong rangeBase = 0x0000_0020_2F00_0000;
|
||||
const ulong rangeSize = 0x40_0000;
|
||||
const ulong occupiedSize = 0x4_0000;
|
||||
@@ -568,154 +519,6 @@ public sealed class GuestMemoryAllocatorTests
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class GranularityAwareHostMemory : IHostMemory
|
||||
{
|
||||
private const ulong Granularity = 0x10000;
|
||||
private const ulong Page = 0x1000;
|
||||
|
||||
private readonly SortedDictionary<ulong, (ulong Size, SortedSet<ulong> CommittedPages)> _allocations = new();
|
||||
|
||||
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection)
|
||||
{
|
||||
var reservedBase = Reserve(desiredAddress, size, protection);
|
||||
if (reservedBase != 0)
|
||||
{
|
||||
var start = desiredAddress == 0 ? reservedBase : AlignDown(desiredAddress, Page);
|
||||
Commit(start, size, protection);
|
||||
}
|
||||
|
||||
return reservedBase;
|
||||
}
|
||||
|
||||
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection)
|
||||
{
|
||||
if (desiredAddress == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var allocationBase = AlignDown(desiredAddress, Granularity);
|
||||
var end = AlignUp(desiredAddress + size, Page);
|
||||
foreach (var (existingBase, existing) in _allocations)
|
||||
{
|
||||
if (allocationBase < existingBase + existing.Size && existingBase < end)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
_allocations[allocationBase] = (end - allocationBase, new SortedSet<ulong>());
|
||||
return allocationBase;
|
||||
}
|
||||
|
||||
public bool Commit(ulong address, ulong size, HostPageProtection protection)
|
||||
{
|
||||
var start = AlignDown(address, Page);
|
||||
var end = AlignUp(address + size, Page);
|
||||
if (!TryFindAllocation(start, out var allocationBase, out var allocation) ||
|
||||
end > allocationBase + allocation.Size)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var page = start; page < end; page += Page)
|
||||
{
|
||||
allocation.CommittedPages.Add(page);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Free(ulong address) => _allocations.Remove(address);
|
||||
|
||||
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
|
||||
{
|
||||
rawOldProtection = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection)
|
||||
{
|
||||
rawOldProtection = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Query(ulong address, out HostRegionInfo info)
|
||||
{
|
||||
var page = AlignDown(address, Page);
|
||||
if (TryFindAllocation(page, out var allocationBase, out var allocation))
|
||||
{
|
||||
var committed = allocation.CommittedPages.Contains(page);
|
||||
var runEnd = page + Page;
|
||||
while (runEnd < allocationBase + allocation.Size &&
|
||||
allocation.CommittedPages.Contains(runEnd) == committed)
|
||||
{
|
||||
runEnd += Page;
|
||||
}
|
||||
|
||||
info = new HostRegionInfo(
|
||||
page,
|
||||
allocationBase,
|
||||
runEnd - page,
|
||||
committed ? HostRegionState.Committed : HostRegionState.Reserved,
|
||||
0,
|
||||
committed ? HostPageProtection.ReadWrite : HostPageProtection.NoAccess,
|
||||
0,
|
||||
0);
|
||||
return true;
|
||||
}
|
||||
|
||||
var freeEnd = ulong.MaxValue;
|
||||
foreach (var existingBase in _allocations.Keys)
|
||||
{
|
||||
if (existingBase > page)
|
||||
{
|
||||
freeEnd = existingBase;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
info = new HostRegionInfo(
|
||||
page,
|
||||
0,
|
||||
freeEnd - page,
|
||||
HostRegionState.Free,
|
||||
0,
|
||||
HostPageProtection.NoAccess,
|
||||
0,
|
||||
0);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void FlushInstructionCache(ulong address, ulong size)
|
||||
{
|
||||
}
|
||||
|
||||
private bool TryFindAllocation(
|
||||
ulong address,
|
||||
out ulong allocationBase,
|
||||
out (ulong Size, SortedSet<ulong> CommittedPages) allocation)
|
||||
{
|
||||
foreach (var (existingBase, existing) in _allocations)
|
||||
{
|
||||
if (address >= existingBase && address < existingBase + existing.Size)
|
||||
{
|
||||
allocationBase = existingBase;
|
||||
allocation = existing;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
allocationBase = 0;
|
||||
allocation = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ulong AlignDown(ulong value, ulong alignment) => value & ~(alignment - 1);
|
||||
|
||||
private static ulong AlignUp(ulong value, ulong alignment) => (value + alignment - 1) & ~(alignment - 1);
|
||||
}
|
||||
|
||||
private sealed class LazyHostMemory(ulong address) : IHostMemory, IDisposable
|
||||
{
|
||||
public bool CommitSucceeds { get; set; } = true;
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using Silk.NET.Vulkan;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.VideoOut;
|
||||
|
||||
public sealed class VulkanFormatConversionTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(Format.R8G8B8A8Unorm, Format.A2R10G10B10UnormPack32, true)]
|
||||
[InlineData(Format.R8G8B8A8Unorm, Format.A2B10G10R10UnormPack32, true)]
|
||||
[InlineData(Format.A2R10G10B10UnormPack32, Format.R8G8B8A8Unorm, true)]
|
||||
[InlineData(Format.A2B10G10R10UnormPack32, Format.R8G8B8A8Unorm, true)]
|
||||
public void RequiresRealFormatConversion_FlagsTheBitIncompatiblePair(
|
||||
Format from,
|
||||
Format to,
|
||||
bool expected)
|
||||
{
|
||||
Assert.Equal(expected, VulkanVideoPresenter.RequiresRealFormatConversion(from, to));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Format.R8G8B8A8Unorm, Format.B8G8R8A8Unorm)]
|
||||
[InlineData(Format.R8G8B8A8Unorm, Format.R8G8B8A8Srgb)]
|
||||
[InlineData(Format.R8G8B8A8Unorm, Format.R8G8B8A8Unorm)]
|
||||
[InlineData(Format.A2R10G10B10UnormPack32, Format.A2B10G10R10UnormPack32)]
|
||||
[InlineData(Format.R16G16B16A16Sfloat, Format.R32G32Sfloat)]
|
||||
public void RequiresRealFormatConversion_LeavesEveryOtherPairAlone(Format from, Format to)
|
||||
{
|
||||
Assert.False(VulkanVideoPresenter.RequiresRealFormatConversion(from, to));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BitCastOfOpaqueBlackRgba8AsA2r10g10b10_ProducesTheObservedRed()
|
||||
{
|
||||
const uint opaqueBlackRgba8 = 0xFF000000u; // bytes 00 00 00 FF, little-endian
|
||||
|
||||
var alpha2Bit = (opaqueBlackRgba8 >> 30) & 0x3u;
|
||||
var red10Bit = (opaqueBlackRgba8 >> 20) & 0x3FFu;
|
||||
var green10Bit = (opaqueBlackRgba8 >> 10) & 0x3FFu;
|
||||
var blue10Bit = opaqueBlackRgba8 & 0x3FFu;
|
||||
|
||||
Assert.Equal(3u, alpha2Bit);
|
||||
Assert.Equal(1008u, red10Bit);
|
||||
Assert.Equal(0u, green10Bit);
|
||||
Assert.Equal(0u, blue10Bit);
|
||||
|
||||
var redAsFloat = red10Bit / 1023.0;
|
||||
Assert.True(
|
||||
Math.Abs(redAsFloat - 0.9853372434443793) < 0.0001,
|
||||
$"expected ~0.9853 (matches the red observed live), got {redAsFloat}");
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,7 @@ public sealed class VulkanGuestImageAliasTests
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Format.R8Srgb, Format.R8Unorm)]
|
||||
[InlineData(Format.BC3SrgbBlock, Format.BC3UnormBlock)]
|
||||
public void CounterpartsOutsideTheViewClassTableAreNotAliased(
|
||||
Format existing,
|
||||
@@ -67,15 +68,6 @@ public sealed class VulkanGuestImageAliasTests
|
||||
VulkanVideoPresenter.IsAliasableGuestImageFormat(existing, requested));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void R8SrgbAndR8UnormShareOneCompatibilityClass()
|
||||
{
|
||||
Assert.True(
|
||||
VulkanVideoPresenter.IsCompatibleGuestImageViewFormat(
|
||||
Format.R8Srgb,
|
||||
Format.R8Unorm));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AliasedPairStaysWithinOneCompatibilityClass()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user