mirror of
https://github.com/par274/sharpemu.git
synced 2026-08-03 08:29:53 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3d9439952 | |||
| 8df4039ca4 | |||
| f36ce4084a | |||
| 4b5ea6a793 | |||
| cf3bd0b4f2 | |||
| 5ee7cd1dfa | |||
| ea9be7484f | |||
| a8fa9c96dc | |||
| c387b969e1 |
@@ -26,6 +26,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
private long _mappingGeneration;
|
private long _mappingGeneration;
|
||||||
private const ulong PageSize = 0x1000;
|
private const ulong PageSize = 0x1000;
|
||||||
|
private const ulong HostAllocationGranularity = 0x10000;
|
||||||
private const ulong GuestAllocationArenaAddress = 0x00006000_0000_0000;
|
private const ulong GuestAllocationArenaAddress = 0x00006000_0000_0000;
|
||||||
private const ulong GuestAllocationArenaSize = 0x0100_0000;
|
private const ulong GuestAllocationArenaSize = 0x0100_0000;
|
||||||
private const ulong GuestAllocationArenaStartOffset = PageSize;
|
private const ulong GuestAllocationArenaStartOffset = PageSize;
|
||||||
@@ -117,6 +118,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
private const uint PAGE_READONLY = 0x02;
|
private const uint PAGE_READONLY = 0x02;
|
||||||
|
|
||||||
private readonly IHostMemory _hostMemory;
|
private readonly IHostMemory _hostMemory;
|
||||||
|
|
||||||
|
private readonly object _fixedAllocationGate = new();
|
||||||
|
private readonly HashSet<ulong> _fixedGranuleReservationBases = new();
|
||||||
private ulong _guestAllocationArenaBase;
|
private ulong _guestAllocationArenaBase;
|
||||||
private readonly SortedDictionary<ulong, ulong> _guestAllocationFreeRanges = new();
|
private readonly SortedDictionary<ulong, ulong> _guestAllocationFreeRanges = new();
|
||||||
private readonly Dictionary<ulong, (ulong Offset, ulong Size)> _guestAllocations = new();
|
private readonly Dictionary<ulong, (ulong Offset, ulong Size)> _guestAllocations = new();
|
||||||
@@ -247,7 +251,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
// reserve-only + lazy commit only when a huge non-exec commit fails —
|
// reserve-only + lazy commit only when a huge non-exec commit fails —
|
||||||
// that is the Poppy / large-reservation path #608 was aiming for.
|
// that is the Poppy / large-reservation path #608 was aiming for.
|
||||||
var reservedOnly = false;
|
var reservedOnly = false;
|
||||||
var result = _hostMemory.Allocate(desiredAddress, alignedSize, hostProtection);
|
var result = TryAllocateFixedThroughGranules(desiredAddress, alignedSize, hostProtection, traceReject: false);
|
||||||
|
if (result == 0)
|
||||||
|
{
|
||||||
|
result = _hostMemory.Allocate(desiredAddress, alignedSize, hostProtection);
|
||||||
|
}
|
||||||
|
|
||||||
if (result == 0 && allowLazyReserve)
|
if (result == 0 && allowLazyReserve)
|
||||||
{
|
{
|
||||||
result = _hostMemory.Reserve(desiredAddress, alignedSize, HostPageProtection.ReadWrite);
|
result = _hostMemory.Reserve(desiredAddress, alignedSize, HostPageProtection.ReadWrite);
|
||||||
@@ -329,7 +338,16 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
// Prefer a full commit. Only fall back to reserve-only when a large
|
// Prefer a full commit. Only fall back to reserve-only when a large
|
||||||
// non-executable commit cannot be satisfied (see TryAllocateAtExact).
|
// non-executable commit cannot be satisfied (see TryAllocateAtExact).
|
||||||
ulong result = _hostMemory.Allocate(desiredAddress, alignedSize, hostProtection);
|
ulong result = 0;
|
||||||
|
if (desiredAddress != 0)
|
||||||
|
{
|
||||||
|
result = TryAllocateFixedThroughGranules(desiredAddress, alignedSize, hostProtection, traceReject: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == 0)
|
||||||
|
{
|
||||||
|
result = _hostMemory.Allocate(desiredAddress, alignedSize, hostProtection);
|
||||||
|
}
|
||||||
|
|
||||||
if (result == 0)
|
if (result == 0)
|
||||||
{
|
{
|
||||||
@@ -436,6 +454,183 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return $"fail:{primeBytes:X}";
|
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)
|
public bool TryBackFixedRange(ulong address, ulong size, bool executable)
|
||||||
{
|
{
|
||||||
if (size == 0)
|
if (size == 0)
|
||||||
@@ -463,7 +658,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
// MemoryRegions are inserted only once every gap in the range has been
|
// 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
|
// 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.
|
// and no region is inserted, so the address space is left untouched.
|
||||||
var stagedAllocations = new List<(ulong Address, ulong Size)>();
|
var stagedAllocations = new List<(ulong Address, ulong Size, bool GranuleTracked)>();
|
||||||
|
|
||||||
var cursor = start;
|
var cursor = start;
|
||||||
while (cursor < end)
|
while (cursor < end)
|
||||||
@@ -482,7 +677,21 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
goto Rollback;
|
goto Rollback;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.State == HostRegionState.Free)
|
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)
|
||||||
{
|
{
|
||||||
var runSize = runEnd - cursor;
|
var runSize = runEnd - cursor;
|
||||||
var allocated = _hostMemory.Allocate(cursor, runSize, hostProtection);
|
var allocated = _hostMemory.Allocate(cursor, runSize, hostProtection);
|
||||||
@@ -496,10 +705,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
goto Rollback;
|
goto Rollback;
|
||||||
}
|
}
|
||||||
|
|
||||||
stagedAllocations.Add((cursor, runSize));
|
stagedAllocations.Add((cursor, runSize, false));
|
||||||
TraceVmem($"Backed fixed range gap: 0x{cursor:X16} - 0x{runEnd:X16} ({runSize} bytes)");
|
TraceVmem($"Backed fixed range gap: 0x{cursor:X16} - 0x{runEnd:X16} ({runSize} bytes)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
cursor = runEnd;
|
cursor = runEnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,7 +723,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
_gate.EnterWriteLock();
|
_gate.EnterWriteLock();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
foreach (var (gapAddress, gapSize) in stagedAllocations)
|
foreach (var (gapAddress, gapSize, _) in stagedAllocations)
|
||||||
{
|
{
|
||||||
InsertRegionSorted(new MemoryRegion
|
InsertRegionSorted(new MemoryRegion
|
||||||
{
|
{
|
||||||
@@ -533,10 +743,13 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
Rollback:
|
Rollback:
|
||||||
foreach (var (gapAddress, _) in stagedAllocations)
|
foreach (var (gapAddress, _, granuleTracked) in stagedAllocations)
|
||||||
|
{
|
||||||
|
if (!granuleTracked)
|
||||||
{
|
{
|
||||||
_hostMemory.Free(gapAddress);
|
_hostMemory.Free(gapAddress);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -790,14 +1003,30 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
lock (_guestAllocationGate)
|
lock (_guestAllocationGate)
|
||||||
|
{
|
||||||
|
lock (_fixedAllocationGate)
|
||||||
{
|
{
|
||||||
_gate.EnterWriteLock();
|
_gate.EnterWriteLock();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var freedBases = new HashSet<ulong>();
|
||||||
foreach (var region in _regions)
|
foreach (var region in _regions)
|
||||||
|
{
|
||||||
|
if (freedBases.Add(region.VirtualAddress))
|
||||||
{
|
{
|
||||||
_hostMemory.Free(region.VirtualAddress);
|
_hostMemory.Free(region.VirtualAddress);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var reservationBase in _fixedGranuleReservationBases)
|
||||||
|
{
|
||||||
|
if (freedBases.Add(reservationBase))
|
||||||
|
{
|
||||||
|
_hostMemory.Free(reservationBase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_fixedGranuleReservationBases.Clear();
|
||||||
_regions.Clear();
|
_regions.Clear();
|
||||||
_pageProtections.Clear();
|
_pageProtections.Clear();
|
||||||
lock (_allocationSearchHintGate)
|
lock (_allocationSearchHintGate)
|
||||||
@@ -810,6 +1039,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
{
|
{
|
||||||
_gate.ExitWriteLock();
|
_gate.ExitWriteLock();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_guestAllocationArenaBase = 0;
|
_guestAllocationArenaBase = 0;
|
||||||
_guestAllocationFreeRanges.Clear();
|
_guestAllocationFreeRanges.Clear();
|
||||||
@@ -1402,6 +1632,42 @@ 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);
|
_regions.Insert(low, region);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1220,8 +1220,13 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<StackPanel Spacing="8">
|
<StackPanel Spacing="8">
|
||||||
<!--Latest commit info-->
|
<!--Latest commit info-->
|
||||||
<Border Classes="optionsInfoRow">
|
<Border Classes="optionsInfoRow">
|
||||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="18">
|
<Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="18">
|
||||||
<StackPanel VerticalAlignment="Center">
|
<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">
|
||||||
<TextBlock x:Name="LatestCommitLabel"
|
<TextBlock x:Name="LatestCommitLabel"
|
||||||
Text="{Binding [About.Github.LatestCommitLabel], Source={x:Static local:Localization.Instance}}"
|
Text="{Binding [About.Github.LatestCommitLabel], Source={x:Static local:Localization.Instance}}"
|
||||||
FontSize="14"
|
FontSize="14"
|
||||||
@@ -1232,7 +1237,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
Foreground="{StaticResource SettingsDescriptionBrush}"
|
Foreground="{StaticResource SettingsDescriptionBrush}"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Button Grid.Column="1"
|
<Button Grid.Column="2"
|
||||||
x:Name="LatestCommitHashText"
|
x:Name="LatestCommitHashText"
|
||||||
Classes="optionAction"
|
Classes="optionAction"
|
||||||
Content="Loading…"
|
Content="Loading…"
|
||||||
@@ -1245,8 +1250,13 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
|
|
||||||
<!--Update-->
|
<!--Update-->
|
||||||
<Border Classes="optionsInfoRow">
|
<Border Classes="optionsInfoRow">
|
||||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="18">
|
<Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="18">
|
||||||
<StackPanel VerticalAlignment="Center">
|
<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">
|
||||||
<TextBlock x:Name="UpdateLabel"
|
<TextBlock x:Name="UpdateLabel"
|
||||||
Text="{Binding [Updater.Label], Source={x:Static local:Localization.Instance}}"
|
Text="{Binding [Updater.Label], Source={x:Static local:Localization.Instance}}"
|
||||||
FontSize="14"
|
FontSize="14"
|
||||||
@@ -1257,7 +1267,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
Foreground="{StaticResource SettingsDescriptionBrush}"
|
Foreground="{StaticResource SettingsDescriptionBrush}"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Button Grid.Column="1"
|
<Button Grid.Column="2"
|
||||||
x:Name="UpdateButton"
|
x:Name="UpdateButton"
|
||||||
Classes="optionAction"
|
Classes="optionAction"
|
||||||
Content="Check for updates"
|
Content="Check for updates"
|
||||||
@@ -1267,8 +1277,13 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
|
|
||||||
<!--Github-->
|
<!--Github-->
|
||||||
<Border Classes="optionsInfoRow">
|
<Border Classes="optionsInfoRow">
|
||||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="18">
|
<Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="18">
|
||||||
<StackPanel VerticalAlignment="Center">
|
<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">
|
||||||
<TextBlock x:Name="GithubLabel"
|
<TextBlock x:Name="GithubLabel"
|
||||||
Text="{Binding [About.Github.Label], Source={x:Static local:Localization.Instance}}"
|
Text="{Binding [About.Github.Label], Source={x:Static local:Localization.Instance}}"
|
||||||
FontSize="14"
|
FontSize="14"
|
||||||
@@ -1279,7 +1294,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
Foreground="{StaticResource SettingsDescriptionBrush}"
|
Foreground="{StaticResource SettingsDescriptionBrush}"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Button Grid.Column="1"
|
<Button Grid.Column="2"
|
||||||
x:Name="GithubButton"
|
x:Name="GithubButton"
|
||||||
Classes="optionAction"
|
Classes="optionAction"
|
||||||
Content="{Binding [About.GithubButton], Source={x:Static local:Localization.Instance}}"
|
Content="{Binding [About.GithubButton], Source={x:Static local:Localization.Instance}}"
|
||||||
@@ -1289,8 +1304,13 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
|
|
||||||
<!--Discord-->
|
<!--Discord-->
|
||||||
<Border Classes="optionsInfoRow">
|
<Border Classes="optionsInfoRow">
|
||||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="18">
|
<Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="18">
|
||||||
<StackPanel VerticalAlignment="Center">
|
<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">
|
||||||
<TextBlock x:Name="DiscordServerLabel"
|
<TextBlock x:Name="DiscordServerLabel"
|
||||||
Text="{Binding [About.Discord.Label], Source={x:Static local:Localization.Instance}}"
|
Text="{Binding [About.Discord.Label], Source={x:Static local:Localization.Instance}}"
|
||||||
FontSize="14"
|
FontSize="14"
|
||||||
@@ -1301,7 +1321,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
Foreground="{StaticResource SettingsDescriptionBrush}"
|
Foreground="{StaticResource SettingsDescriptionBrush}"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<TextBlock Grid.Column="1"
|
<TextBlock Grid.Column="2"
|
||||||
x:Name="DiscordComingSoonText"
|
x:Name="DiscordComingSoonText"
|
||||||
Text="{Binding [About.DiscordComingSoon], Source={x:Static local:Localization.Instance}}"
|
Text="{Binding [About.DiscordComingSoon], Source={x:Static local:Localization.Instance}}"
|
||||||
FontSize="12"
|
FontSize="12"
|
||||||
|
|||||||
@@ -95,8 +95,11 @@ public static partial class AgcExports
|
|||||||
private const uint SpiShaderPgmRsrc1Hs = 0x10A;
|
private const uint SpiShaderPgmRsrc1Hs = 0x10A;
|
||||||
private const uint SpiShaderPgmLoLs = 0x148;
|
private const uint SpiShaderPgmLoLs = 0x148;
|
||||||
private const uint SpiShaderPgmHiLs = 0x149;
|
private const uint SpiShaderPgmHiLs = 0x149;
|
||||||
private const uint SpiShaderPgmLoGs = 0x8A;
|
// Not 0x8A/0x8B - those are SPI_SHADER_PGM_RSRC1/RSRC2_GS, and reading them
|
||||||
private const uint SpiShaderPgmHiGs = 0x8B;
|
// 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 SpiShaderPgmChksumGs = 0x80;
|
private const uint SpiShaderPgmChksumGs = 0x80;
|
||||||
private const uint SpiPsInputEna = 0x1B3;
|
private const uint SpiPsInputEna = 0x1B3;
|
||||||
private const uint SpiPsInputAddr = 0x1B4;
|
private const uint SpiPsInputAddr = 0x1B4;
|
||||||
@@ -139,9 +142,15 @@ public static partial class AgcExports
|
|||||||
private const uint CbColor0Base = 0x318;
|
private const uint CbColor0Base = 0x318;
|
||||||
private const uint CbColorRegisterStride = 15;
|
private const uint CbColorRegisterStride = 15;
|
||||||
private const uint CbColor0Info = 0x31C;
|
private const uint CbColor0Info = 0x31C;
|
||||||
|
private const uint CbColor0ClearWord0 = 0x323;
|
||||||
|
private const uint CbColor0ClearWord1 = 0x324;
|
||||||
private const uint CbColor0BaseExt = 0x390;
|
private const uint CbColor0BaseExt = 0x390;
|
||||||
private const uint CbColor0Attrib2 = 0x3B0;
|
private const uint CbColor0Attrib2 = 0x3B0;
|
||||||
private const uint CbColor0Attrib3 = 0x3B8;
|
private const uint CbColor0Attrib3 = 0x3B8;
|
||||||
|
// CB_COLORn_INFO.DCC_ENABLE (gc_10_1_0_sh_mask.h). On GFX10 the legacy
|
||||||
|
// 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 CbBlend0Control = 0x1E0;
|
||||||
private const uint PaScModeCntl0 = 0x292;
|
private const uint PaScModeCntl0 = 0x292;
|
||||||
// GFX10 DB context registers (register byte address minus 0x28000, / 4).
|
// GFX10 DB context registers (register byte address minus 0x28000, / 4).
|
||||||
@@ -501,7 +510,8 @@ public static partial class AgcExports
|
|||||||
float ClearRed = 0f,
|
float ClearRed = 0f,
|
||||||
float ClearGreen = 0f,
|
float ClearGreen = 0f,
|
||||||
float ClearBlue = 0f,
|
float ClearBlue = 0f,
|
||||||
float ClearAlpha = 1f);
|
float ClearAlpha = 1f,
|
||||||
|
bool IsDccFastClear = false);
|
||||||
|
|
||||||
private sealed record TranslatedImageBinding(
|
private sealed record TranslatedImageBinding(
|
||||||
TextureDescriptor Descriptor,
|
TextureDescriptor Descriptor,
|
||||||
@@ -2174,6 +2184,18 @@ public static partial class AgcExports
|
|||||||
return (int)ctx[CpuRegister.Rax];
|
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(
|
[SysAbiExport(
|
||||||
Nid = "rUuVjyR+Rd4",
|
Nid = "rUuVjyR+Rd4",
|
||||||
ExportName = "sceAgcDcbGetLodStatsGetSize",
|
ExportName = "sceAgcDcbGetLodStatsGetSize",
|
||||||
@@ -6426,6 +6448,48 @@ 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>
|
/// <summary>
|
||||||
/// GraphicsDcbSetIndexSize writes VGT_INDEX_TYPE via SET_UCONFIG_REG.
|
/// GraphicsDcbSetIndexSize writes VGT_INDEX_TYPE via SET_UCONFIG_REG.
|
||||||
/// Mirror that into <see cref="SubmittedDcbState.IndexSize"/>.
|
/// Mirror that into <see cref="SubmittedDcbState.IndexSize"/>.
|
||||||
@@ -6808,6 +6872,29 @@ public static partial class AgcExports
|
|||||||
$"dst=0x{resolveDestination.Address:X16}");
|
$"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();
|
var firstTarget = translatedDraw.RenderTargets.FirstOrDefault();
|
||||||
if (firstTarget.Address != 0)
|
if (firstTarget.Address != 0)
|
||||||
{
|
{
|
||||||
@@ -7444,22 +7531,6 @@ 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
|
// Every bound color target the shader exports to. Deferred renderers
|
||||||
// draw a multi-render-target G-buffer (up to eight slots) in one pass.
|
// 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.
|
// Fall back to slot 0 if we cannot match any export to a bound target.
|
||||||
@@ -7740,6 +7811,12 @@ public static partial class AgcExports
|
|||||||
pixelUserData[index] = pixelEvaluation.InitialScalarRegisters[index];
|
pixelUserData[index] = pixelEvaluation.InitialScalarRegisters[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var renderState = ApplyTransparentPremultipliedFillClear(
|
||||||
|
CreateRenderState(state.CxRegisters, renderTargets, pixelColorExportMasks),
|
||||||
|
textures,
|
||||||
|
vertexInputs,
|
||||||
|
pixelEvaluation.InitialScalarRegisters);
|
||||||
|
|
||||||
draw = new TranslatedGuestDraw(
|
draw = new TranslatedGuestDraw(
|
||||||
exportShaderAddress,
|
exportShaderAddress,
|
||||||
pixelShaderAddress,
|
pixelShaderAddress,
|
||||||
@@ -7757,11 +7834,7 @@ public static partial class AgcExports
|
|||||||
renderTargets,
|
renderTargets,
|
||||||
DecodeDepthTarget(state.CxRegisters),
|
DecodeDepthTarget(state.CxRegisters),
|
||||||
guestTargets,
|
guestTargets,
|
||||||
ApplyTransparentPremultipliedFillClear(
|
renderState,
|
||||||
CreateRenderState(state.CxRegisters, renderTargets, pixelColorExportMasks),
|
|
||||||
textures,
|
|
||||||
vertexInputs,
|
|
||||||
pixelEvaluation.InitialScalarRegisters),
|
|
||||||
pixelUserData,
|
pixelUserData,
|
||||||
state.CxRegisters.TryGetValue(CbBlend0Control, out var rawBlend) ? rawBlend : 0,
|
state.CxRegisters.TryGetValue(CbBlend0Control, out var rawBlend) ? rawBlend : 0,
|
||||||
state.CxRegisters.TryGetValue(
|
state.CxRegisters.TryGetValue(
|
||||||
@@ -7775,7 +7848,15 @@ public static partial class AgcExports
|
|||||||
fullscreenClearColor.Red,
|
fullscreenClearColor.Red,
|
||||||
fullscreenClearColor.Green,
|
fullscreenClearColor.Green,
|
||||||
fullscreenClearColor.Blue,
|
fullscreenClearColor.Blue,
|
||||||
fullscreenClearColor.Alpha);
|
fullscreenClearColor.Alpha,
|
||||||
|
IsDccFastClearDraw(
|
||||||
|
state.CxRegisters,
|
||||||
|
renderTargets,
|
||||||
|
textures,
|
||||||
|
vertexInputs,
|
||||||
|
renderState,
|
||||||
|
primitiveType,
|
||||||
|
vertexCount));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8052,6 +8133,113 @@ 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) =>
|
private static bool IsTransparentPremultipliedFillBlend(GuestBlendState blend) =>
|
||||||
blend is
|
blend is
|
||||||
{
|
{
|
||||||
@@ -8236,20 +8424,6 @@ public static partial class AgcExports
|
|||||||
? (packedMasks >> (int)(target * 4)) & 0xFu
|
? (packedMasks >> (int)(target * 4)) & 0xFu
|
||||||
: 0;
|
: 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)
|
private static uint GetInterpolatedAttributeCount(Gen5ShaderState state)
|
||||||
{
|
{
|
||||||
var maxAttribute = -1;
|
var maxAttribute = -1;
|
||||||
@@ -12512,13 +12686,16 @@ public static partial class AgcExports
|
|||||||
// GTA V Enhanced HS headers start at RSRC1/RSRC2 (0x10A/0x10B) and
|
// 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
|
// omit PGM_LO/HI from the default table. Still succeed: the code VA
|
||||||
// lives at ShaderCodeOffset and later binder paths republish it.
|
// lives at ShaderCodeOffset and later binder paths republish it.
|
||||||
if (shaderType == HsFrontShaderType && firstLo is SpiShaderPgmRsrc1Hs or SpiShaderPgmLoHs)
|
// 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))
|
||||||
{
|
{
|
||||||
TraceCreateShader(
|
TraceCreateShader(
|
||||||
0,
|
0,
|
||||||
headerAddress,
|
headerAddress,
|
||||||
codeAddress,
|
codeAddress,
|
||||||
$"skip-pgm-patch type={HsFrontShaderType} first_lo=0x{firstLo:X8}");
|
$"skip-pgm-patch type={shaderType} first_lo=0x{firstLo:X8}");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12673,9 +12850,6 @@ public static partial class AgcExports
|
|||||||
private static bool IsEsGeometryShaderType(byte shaderType) =>
|
private static bool IsEsGeometryShaderType(byte shaderType) =>
|
||||||
shaderType is GsShaderType or GsBackShaderType;
|
shaderType is GsShaderType or GsBackShaderType;
|
||||||
|
|
||||||
private static bool IsRectListPrimitive(uint primitiveType) =>
|
|
||||||
AgcPrimitiveHelpers.IsRectListPrimitive(primitiveType);
|
|
||||||
|
|
||||||
private static int SetIndirectPatchAddress(CpuContext ctx, string registerSpace)
|
private static int SetIndirectPatchAddress(CpuContext ctx, string registerSpace)
|
||||||
{
|
{
|
||||||
var commandAddress = ctx[CpuRegister.Rdi];
|
var commandAddress = ctx[CpuRegister.Rdi];
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ public static class AmprExports
|
|||||||
private const int MaxCachedHostFiles = 1536;
|
private const int MaxCachedHostFiles = 1536;
|
||||||
private static readonly object _hostFileCacheGate = new();
|
private static readonly object _hostFileCacheGate = new();
|
||||||
private static readonly Dictionary<string, LinkedListNode<CachedHostFileEntry>> _hostFileByPath =
|
private static readonly Dictionary<string, LinkedListNode<CachedHostFileEntry>> _hostFileByPath =
|
||||||
new(StringComparer.OrdinalIgnoreCase);
|
new(HostFsPath.Comparer);
|
||||||
private static readonly LinkedList<CachedHostFileEntry> _hostFileLru = new();
|
private static readonly LinkedList<CachedHostFileEntry> _hostFileLru = new();
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ internal static class AmprFileRegistry
|
|||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
if (string.Equals(_indexedApp0Root, normalizedRoot, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(_indexedApp0Root, normalizedRoot, HostFsPath.Comparison))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -123,7 +123,7 @@ internal static class AmprFileRegistry
|
|||||||
if (string.Equals(
|
if (string.Equals(
|
||||||
_indexingApp0Root,
|
_indexingApp0Root,
|
||||||
normalizedRoot,
|
normalizedRoot,
|
||||||
StringComparison.OrdinalIgnoreCase))
|
HostFsPath.Comparison))
|
||||||
{
|
{
|
||||||
Monitor.Wait(_indexGate);
|
Monitor.Wait(_indexGate);
|
||||||
continue;
|
continue;
|
||||||
@@ -174,6 +174,8 @@ internal static class AmprFileRegistry
|
|||||||
}
|
}
|
||||||
|
|
||||||
var relatives = new List<string>(256 * 1024);
|
var relatives = new List<string>(256 * 1024);
|
||||||
|
try
|
||||||
|
{
|
||||||
foreach (var hostPath in Directory.EnumerateFiles(
|
foreach (var hostPath in Directory.EnumerateFiles(
|
||||||
normalizedRoot,
|
normalizedRoot,
|
||||||
"*",
|
"*",
|
||||||
@@ -189,6 +191,18 @@ internal static class AmprFileRegistry
|
|||||||
|
|
||||||
relatives.Add(relative);
|
relatives.Add(relative);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// Hash + dictionary fill dominates under Rosetta once the walk is
|
// Hash + dictionary fill dominates under Rosetta once the walk is
|
||||||
// done; parallelize across cores without re-walking the tree.
|
// done; parallelize across cores without re-walking the tree.
|
||||||
@@ -315,7 +329,10 @@ internal static class AmprFileRegistry
|
|||||||
"ampr-index");
|
"ampr-index");
|
||||||
Directory.CreateDirectory(cacheDir);
|
Directory.CreateDirectory(cacheDir);
|
||||||
|
|
||||||
var rootHash = ComputeFileId(normalizedRoot.ToLowerInvariant());
|
// 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);
|
||||||
return Path.Combine(cacheDir, $"app0-{rootHash:x8}.v{version}.idx");
|
return Path.Combine(cacheDir, $"app0-{rootHash:x8}.v{version}.idx");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,7 +377,7 @@ internal static class AmprFileRegistry
|
|||||||
}
|
}
|
||||||
|
|
||||||
var root = reader.ReadString();
|
var root = reader.ReadString();
|
||||||
if (!string.Equals(root, normalizedRoot, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(root, normalizedRoot, HostFsPath.Comparison))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -470,7 +487,7 @@ internal static class AmprFileRegistry
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var relatives = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
var relatives = new HashSet<string>(HostFsPath.Comparer);
|
||||||
foreach (var hostPath in _hostPathsById.Values)
|
foreach (var hostPath in _hostPathsById.Values)
|
||||||
{
|
{
|
||||||
var relative = Path.GetRelativePath(normalizedRoot, hostPath)
|
var relative = Path.GetRelativePath(normalizedRoot, hostPath)
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// 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,17 +117,12 @@ public static partial class KernelMemoryCompatExports
|
|||||||
private static readonly Dictionary<string, string> _guestMounts = new(StringComparer.OrdinalIgnoreCase);
|
private static readonly Dictionary<string, string> _guestMounts = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private static readonly HashSet<string> _tracedStatResults = new(StringComparer.Ordinal);
|
private static readonly HashSet<string> _tracedStatResults = new(StringComparer.Ordinal);
|
||||||
// Both caches memoize host filesystem probe outcomes, so their key
|
// Both caches memoize host filesystem probe outcomes, so their key
|
||||||
// equivalence must match the host filesystem's: Windows resolves names
|
// equivalence must match the host filesystem's — see HostFsPath. On a
|
||||||
// case-insensitively, but Linux hosts are case-sensitive, and an
|
// case-sensitive host an ignore-case cache aliases distinct paths: a
|
||||||
// ignore-case cache there aliases distinct paths — a cached miss for
|
// cached miss for "/app0/DATA.BIN" keeps answering NOT_FOUND for
|
||||||
// "/app0/DATA.BIN" keeps answering NOT_FOUND for "/app0/Data.bin" even
|
// "/app0/Data.bin" even though that file exists.
|
||||||
// though that file exists and a fresh probe would find it.
|
private static readonly HashSet<string> _negativeStatCache = new(HostFsPath.Comparer);
|
||||||
private static readonly StringComparer HostFsPathComparer =
|
private static readonly ConcurrentDictionary<string, ulong> _aprFileSizeCache = new(HostFsPath.Comparer);
|
||||||
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 long _nextFileDescriptor = 2;
|
||||||
private static string _applicationTitleId = "UNKNOWN";
|
private static string _applicationTitleId = "UNKNOWN";
|
||||||
|
|
||||||
@@ -5203,8 +5198,8 @@ public static partial class KernelMemoryCompatExports
|
|||||||
// host would let a relative path escape into a sibling directory that
|
// host would let a relative path escape into a sibling directory that
|
||||||
// differs from the mount root only by case (root ".../Save" vs
|
// differs from the mount root only by case (root ".../Save" vs
|
||||||
// sibling ".../save").
|
// sibling ".../save").
|
||||||
if (!string.Equals(candidate, matchedHostRoot, HostFsPathComparison) &&
|
if (!string.Equals(candidate, matchedHostRoot, HostFsPath.Comparison) &&
|
||||||
!candidate.StartsWith(rootWithSeparator, HostFsPathComparison))
|
!candidate.StartsWith(rootWithSeparator, HostFsPath.Comparison))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -5305,8 +5300,8 @@ public static partial class KernelMemoryCompatExports
|
|||||||
|
|
||||||
var rootWithSeparator =
|
var rootWithSeparator =
|
||||||
Path.TrimEndingDirectorySeparator(fullRoot) + Path.DirectorySeparatorChar;
|
Path.TrimEndingDirectorySeparator(fullRoot) + Path.DirectorySeparatorChar;
|
||||||
if (!string.Equals(candidate, fullRoot, HostFsPathComparison) &&
|
if (!string.Equals(candidate, fullRoot, HostFsPath.Comparison) &&
|
||||||
!candidate.StartsWith(rootWithSeparator, HostFsPathComparison))
|
!candidate.StartsWith(rootWithSeparator, HostFsPath.Comparison))
|
||||||
{
|
{
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
@@ -5332,7 +5327,7 @@ public static partial class KernelMemoryCompatExports
|
|||||||
private static bool EscapesMountViaReparsePoint(string mountRoot, string candidate)
|
private static bool EscapesMountViaReparsePoint(string mountRoot, string candidate)
|
||||||
{
|
{
|
||||||
var rootTrimmed = Path.TrimEndingDirectorySeparator(mountRoot);
|
var rootTrimmed = Path.TrimEndingDirectorySeparator(mountRoot);
|
||||||
if (string.Equals(candidate, rootTrimmed, HostFsPathComparison))
|
if (string.Equals(candidate, rootTrimmed, HostFsPath.Comparison))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -918,15 +918,8 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Several Gen5 runtimes layer their own owner/count bookkeeping
|
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK);
|
||||||
// over a NORMAL kernel mutex. Returning EDEADLK here
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
||||||
// 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
|
else
|
||||||
{
|
{
|
||||||
@@ -1264,15 +1257,15 @@ public static class KernelPthreadCompatExports
|
|||||||
return CreateImplicitMutexState(ctx, mutexAddress, MutexTypeAdaptiveNp, out resolvedAddress, out state);
|
return CreateImplicitMutexState(ctx, mutexAddress, MutexTypeAdaptiveNp, out resolvedAddress, out state);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pointedHandle != 0)
|
if (pointedHandle != 0 && pointedHandle != mutexAddress && _mutexStates.TryGetValue(pointedHandle, out state))
|
||||||
{
|
{
|
||||||
if (_mutexStates.TryGetValue(pointedHandle, out state))
|
_mutexStates[mutexAddress] = state;
|
||||||
{
|
|
||||||
_mutexStates.TryAdd(mutexAddress, state);
|
|
||||||
resolvedAddress = pointedHandle;
|
resolvedAddress = pointedHandle;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pointedHandle != 0)
|
||||||
|
{
|
||||||
resolvedAddress = pointedHandle;
|
resolvedAddress = pointedHandle;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -712,6 +712,8 @@ public static partial class Gen5SpirvTranslator
|
|||||||
if (UsesSubgroupOperations())
|
if (UsesSubgroupOperations())
|
||||||
{
|
{
|
||||||
_module.AddCapability(SpirvCapability.GroupNonUniform);
|
_module.AddCapability(SpirvCapability.GroupNonUniform);
|
||||||
|
_module.AddCapability(SpirvCapability.GroupNonUniformBallot);
|
||||||
|
|
||||||
if (UsesSubgroupShuffle())
|
if (UsesSubgroupShuffle())
|
||||||
{
|
{
|
||||||
_module.AddCapability(SpirvCapability.GroupNonUniformShuffle);
|
_module.AddCapability(SpirvCapability.GroupNonUniformShuffle);
|
||||||
@@ -722,10 +724,6 @@ public static partial class Gen5SpirvTranslator
|
|||||||
_module.AddCapability(SpirvCapability.GroupNonUniformVote);
|
_module.AddCapability(SpirvCapability.GroupNonUniformVote);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (UsesSubgroupBroadcast() || UsesWaveControl())
|
|
||||||
{
|
|
||||||
_module.AddCapability(SpirvCapability.GroupNonUniformBallot);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_glsl = _module.ImportExtInst("GLSL.std.450");
|
_glsl = _module.ImportExtInst("GLSL.std.450");
|
||||||
@@ -1802,6 +1800,8 @@ public static partial class Gen5SpirvTranslator
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (instruction.Opcode == "SBarrier")
|
if (instruction.Opcode == "SBarrier")
|
||||||
|
{
|
||||||
|
if (_stage == Gen5SpirvStage.Compute)
|
||||||
{
|
{
|
||||||
var workgroup = UInt(2);
|
var workgroup = UInt(2);
|
||||||
var semantics = UInt(0x108);
|
var semantics = UInt(0x108);
|
||||||
@@ -1810,6 +1810,7 @@ public static partial class Gen5SpirvTranslator
|
|||||||
workgroup,
|
workgroup,
|
||||||
workgroup,
|
workgroup,
|
||||||
semantics);
|
semantics);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
// 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
// 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,6 +6,9 @@ using Xunit;
|
|||||||
|
|
||||||
namespace SharpEmu.Libs.Tests.Ampr;
|
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
|
public class AmprFileRegistryTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -62,6 +65,79 @@ public class AmprFileRegistryTests
|
|||||||
Assert.Equal(host, d);
|
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)
|
private static uint FnvUtf8(string text)
|
||||||
{
|
{
|
||||||
const uint offset = 2166136261;
|
const uint offset = 2166136261;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using Xunit;
|
|||||||
|
|
||||||
namespace SharpEmu.Libs.Tests.Ampr;
|
namespace SharpEmu.Libs.Tests.Ampr;
|
||||||
|
|
||||||
|
[Collection("AmprFileRegistry")]
|
||||||
public sealed class AmprWriteAddressTests
|
public sealed class AmprWriteAddressTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using Xunit;
|
|||||||
|
|
||||||
namespace SharpEmu.Libs.Tests.Ampr;
|
namespace SharpEmu.Libs.Tests.Ampr;
|
||||||
|
|
||||||
|
[Collection("AmprFileRegistry")]
|
||||||
public sealed class AprStreamingContractTests
|
public sealed class AprStreamingContractTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -104,6 +104,45 @@ public sealed class GuestMemoryAllocatorTests
|
|||||||
Assert.Equal(0UL, (ulong)memory.GetPointer(address));
|
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]
|
[Fact]
|
||||||
public void AlignedAllocationDoesNotRetainOverallocatedMappingsOutsideMacOS()
|
public void AlignedAllocationDoesNotRetainOverallocatedMappingsOutsideMacOS()
|
||||||
{
|
{
|
||||||
@@ -133,6 +172,11 @@ public sealed class GuestMemoryAllocatorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void TryBackFixedRangeRollsBackEarlierGapsWhenLaterGapCannotBeBacked()
|
public void TryBackFixedRangeRollsBackEarlierGapsWhenLaterGapCannotBeBacked()
|
||||||
{
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Layout: committed | free | committed | free
|
// Layout: committed | free | committed | free
|
||||||
// First free gap allocates successfully, second fails.
|
// First free gap allocates successfully, second fails.
|
||||||
// The first allocation must be freed — nothing should leak.
|
// The first allocation must be freed — nothing should leak.
|
||||||
@@ -154,6 +198,11 @@ public sealed class GuestMemoryAllocatorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void TryBackFixedRangeFillsOnlyTheFreePagesOfAPartiallyOccupiedRange()
|
public void TryBackFixedRangeFillsOnlyTheFreePagesOfAPartiallyOccupiedRange()
|
||||||
{
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const ulong rangeBase = 0x0000_0020_2F00_0000;
|
const ulong rangeBase = 0x0000_0020_2F00_0000;
|
||||||
const ulong rangeSize = 0x40_0000;
|
const ulong rangeSize = 0x40_0000;
|
||||||
const ulong occupiedSize = 0x4_0000;
|
const ulong occupiedSize = 0x4_0000;
|
||||||
@@ -519,6 +568,154 @@ 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
|
private sealed class LazyHostMemory(ulong address) : IHostMemory, IDisposable
|
||||||
{
|
{
|
||||||
public bool CommitSucceeds { get; set; } = true;
|
public bool CommitSucceeds { get; set; } = true;
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// 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,7 +53,6 @@ public sealed class VulkanGuestImageAliasTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(Format.R8Srgb, Format.R8Unorm)]
|
|
||||||
[InlineData(Format.BC3SrgbBlock, Format.BC3UnormBlock)]
|
[InlineData(Format.BC3SrgbBlock, Format.BC3UnormBlock)]
|
||||||
public void CounterpartsOutsideTheViewClassTableAreNotAliased(
|
public void CounterpartsOutsideTheViewClassTableAreNotAliased(
|
||||||
Format existing,
|
Format existing,
|
||||||
@@ -68,6 +67,15 @@ public sealed class VulkanGuestImageAliasTests
|
|||||||
VulkanVideoPresenter.IsAliasableGuestImageFormat(existing, requested));
|
VulkanVideoPresenter.IsAliasableGuestImageFormat(existing, requested));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void R8SrgbAndR8UnormShareOneCompatibilityClass()
|
||||||
|
{
|
||||||
|
Assert.True(
|
||||||
|
VulkanVideoPresenter.IsCompatibleGuestImageViewFormat(
|
||||||
|
Format.R8Srgb,
|
||||||
|
Format.R8Unorm));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AliasedPairStaysWithinOneCompatibilityClass()
|
public void AliasedPairStaysWithinOneCompatibilityClass()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user