mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-22 19:06:15 +08:00
Fix virtual memory allocation and access (#193)
* Fix virtual memory allocation and access * Update test dependency lock file
This commit is contained in:
@@ -50,4 +50,9 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemo
|
|||||||
address = 0;
|
address = 0;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TryFreeGuestMemory(ulong address)
|
||||||
|
{
|
||||||
|
return _inner is IGuestMemoryAllocator allocator && allocator.TryFreeGuestMemory(address);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
private readonly IHostMemory _hostMemory;
|
private readonly IHostMemory _hostMemory;
|
||||||
private ulong _guestAllocationArenaBase;
|
private ulong _guestAllocationArenaBase;
|
||||||
private ulong _guestAllocationOffset;
|
private readonly SortedDictionary<ulong, ulong> _guestAllocationFreeRanges = new();
|
||||||
|
private readonly Dictionary<ulong, (ulong Offset, ulong Size)> _guestAllocations = new();
|
||||||
private static readonly ulong LazyReservePrimeBytes = ResolveLazyReservePrimeBytes();
|
private static readonly ulong LazyReservePrimeBytes = ResolveLazyReservePrimeBytes();
|
||||||
|
|
||||||
public PhysicalVirtualMemory(IHostMemory? hostMemory = null)
|
public PhysicalVirtualMemory(IHostMemory? hostMemory = null)
|
||||||
@@ -301,7 +302,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
GuestAllocationArenaSize,
|
GuestAllocationArenaSize,
|
||||||
executable: false,
|
executable: false,
|
||||||
allowAlternative: true);
|
allowAlternative: true);
|
||||||
_guestAllocationOffset = GuestAllocationArenaStartOffset;
|
_guestAllocationFreeRanges.Add(
|
||||||
|
GuestAllocationArenaStartOffset,
|
||||||
|
GuestAllocationArenaSize - GuestAllocationArenaStartOffset);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
@@ -309,14 +312,89 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var alignedOffset = AlignUp(_guestAllocationOffset, alignment);
|
ulong rangeOffset = 0;
|
||||||
if (alignedOffset > GuestAllocationArenaSize || size > GuestAllocationArenaSize - alignedOffset)
|
ulong rangeSize = 0;
|
||||||
|
ulong alignedOffset = 0;
|
||||||
|
var found = false;
|
||||||
|
foreach (var range in _guestAllocationFreeRanges)
|
||||||
|
{
|
||||||
|
alignedOffset = AlignUp(range.Key, alignment);
|
||||||
|
if (alignedOffset >= range.Key &&
|
||||||
|
alignedOffset - range.Key <= range.Value &&
|
||||||
|
size <= range.Value - (alignedOffset - range.Key))
|
||||||
|
{
|
||||||
|
rangeOffset = range.Key;
|
||||||
|
rangeSize = range.Value;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_guestAllocationFreeRanges.Remove(rangeOffset);
|
||||||
|
if (alignedOffset > rangeOffset)
|
||||||
|
{
|
||||||
|
_guestAllocationFreeRanges.Add(rangeOffset, alignedOffset - rangeOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
var allocationEnd = alignedOffset + size;
|
||||||
|
var rangeEnd = rangeOffset + rangeSize;
|
||||||
|
if (allocationEnd < rangeEnd)
|
||||||
|
{
|
||||||
|
_guestAllocationFreeRanges.Add(allocationEnd, rangeEnd - allocationEnd);
|
||||||
|
}
|
||||||
|
|
||||||
address = _guestAllocationArenaBase + alignedOffset;
|
address = _guestAllocationArenaBase + alignedOffset;
|
||||||
_guestAllocationOffset = alignedOffset + size;
|
_guestAllocations.Add(address, (alignedOffset, size));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryFreeGuestMemory(ulong address)
|
||||||
|
{
|
||||||
|
lock (_guestAllocationGate)
|
||||||
|
{
|
||||||
|
if (!_guestAllocations.Remove(address, out var allocation))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var freeOffset = allocation.Offset;
|
||||||
|
var freeSize = allocation.Size;
|
||||||
|
ulong? previousOffset = null;
|
||||||
|
ulong? nextOffset = null;
|
||||||
|
|
||||||
|
foreach (var range in _guestAllocationFreeRanges)
|
||||||
|
{
|
||||||
|
if (range.Key < freeOffset)
|
||||||
|
{
|
||||||
|
previousOffset = range.Key;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextOffset = range.Key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousOffset is { } previous &&
|
||||||
|
previous + _guestAllocationFreeRanges[previous] == freeOffset)
|
||||||
|
{
|
||||||
|
freeOffset = previous;
|
||||||
|
freeSize += _guestAllocationFreeRanges[previous];
|
||||||
|
_guestAllocationFreeRanges.Remove(previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextOffset is { } next && freeOffset + freeSize == next)
|
||||||
|
{
|
||||||
|
freeSize += _guestAllocationFreeRanges[next];
|
||||||
|
_guestAllocationFreeRanges.Remove(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
_guestAllocationFreeRanges.Add(freeOffset, freeSize);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -380,7 +458,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
|
|
||||||
_guestAllocationArenaBase = 0;
|
_guestAllocationArenaBase = 0;
|
||||||
_guestAllocationOffset = 0;
|
_guestAllocationFreeRanges.Clear();
|
||||||
|
_guestAllocations.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,12 +518,33 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
private void ApplySegmentProtection(ulong mapStart, ulong mapEnd, ProgramHeaderFlags flags)
|
private void ApplySegmentProtection(ulong mapStart, ulong mapEnd, ProgramHeaderFlags flags)
|
||||||
{
|
{
|
||||||
|
var runStart = mapStart;
|
||||||
|
var runFlags = ProgramHeaderFlags.None;
|
||||||
|
var hasRun = false;
|
||||||
|
|
||||||
for (var pageAddress = mapStart; pageAddress < mapEnd; pageAddress += PageSize)
|
for (var pageAddress = mapStart; pageAddress < mapEnd; pageAddress += PageSize)
|
||||||
{
|
{
|
||||||
_pageProtections.TryGetValue(pageAddress, out var existingFlags);
|
_pageProtections.TryGetValue(pageAddress, out var existingFlags);
|
||||||
var mergedFlags = existingFlags | flags;
|
var mergedFlags = existingFlags | flags;
|
||||||
_pageProtections[pageAddress] = mergedFlags;
|
_pageProtections[pageAddress] = mergedFlags;
|
||||||
SetProtection(pageAddress, PageSize, mergedFlags);
|
|
||||||
|
if (!hasRun)
|
||||||
|
{
|
||||||
|
runStart = pageAddress;
|
||||||
|
runFlags = mergedFlags;
|
||||||
|
hasRun = true;
|
||||||
|
}
|
||||||
|
else if (mergedFlags != runFlags)
|
||||||
|
{
|
||||||
|
SetProtection(runStart, pageAddress - runStart, runFlags);
|
||||||
|
runStart = pageAddress;
|
||||||
|
runFlags = mergedFlags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasRun)
|
||||||
|
{
|
||||||
|
SetProtection(runStart, mapEnd - runStart, runFlags);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -789,9 +889,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
_gate.EnterReadLock();
|
_gate.EnterReadLock();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return FindRegion(virtualAddress, 1) is not null
|
var region = FindRegion(virtualAddress, 1);
|
||||||
? (void*)virtualAddress
|
if (region is null ||
|
||||||
: null;
|
(region.IsReservedOnly && !EnsureRangeCommitted(virtualAddress, 1, region)))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (void*)virtualAddress;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -41,15 +41,14 @@ public sealed class VirtualMemory : IVirtualMemory
|
|||||||
|
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
foreach (var existing in _regions)
|
var insertionIndex = FindInsertionIndex(virtualAddress);
|
||||||
|
if ((insertionIndex > 0 && virtualAddress < _regions[insertionIndex - 1].EndAddress) ||
|
||||||
|
(insertionIndex < _regions.Count && endAddress > _regions[insertionIndex].Region.VirtualAddress))
|
||||||
{
|
{
|
||||||
if (virtualAddress < existing.EndAddress && endAddress > existing.Region.VirtualAddress)
|
throw new InvalidOperationException("Attempted to map an overlapping virtual memory region.");
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Attempted to map an overlapping virtual memory region.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_regions.Add(new MappedRegion(
|
_regions.Insert(insertionIndex, new MappedRegion(
|
||||||
new VirtualMemoryRegion(virtualAddress, memorySize, fileOffset, (ulong)fileData.Length, protection),
|
new VirtualMemoryRegion(virtualAddress, memorySize, fileOffset, (ulong)fileData.Length, protection),
|
||||||
endAddress,
|
endAddress,
|
||||||
backingMemory));
|
backingMemory));
|
||||||
@@ -74,12 +73,12 @@ public sealed class VirtualMemory : IVirtualMemory
|
|||||||
{
|
{
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
if (!TryResolveRegion(virtualAddress, destination.Length, out var region, out var offset))
|
if (!TryValidateRange(virtualAddress, destination.Length, ProgramHeaderFlags.Read, out var regionIndex))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
region.BackingMemory.AsSpan(offset, destination.Length).CopyTo(destination);
|
CopyFromRegions(virtualAddress, destination, regionIndex);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,39 +87,127 @@ public sealed class VirtualMemory : IVirtualMemory
|
|||||||
{
|
{
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
if (!TryResolveRegion(virtualAddress, source.Length, out var region, out var offset))
|
if (!TryValidateRange(virtualAddress, source.Length, ProgramHeaderFlags.Write, out var regionIndex))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
source.CopyTo(region.BackingMemory.AsSpan(offset, source.Length));
|
CopyToRegions(virtualAddress, source, regionIndex);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryResolveRegion(ulong virtualAddress, int length, out MappedRegion region, out int offset)
|
private bool TryValidateRange(
|
||||||
|
ulong virtualAddress,
|
||||||
|
int length,
|
||||||
|
ProgramHeaderFlags requiredProtection,
|
||||||
|
out int regionIndex)
|
||||||
{
|
{
|
||||||
foreach (var candidate in _regions)
|
regionIndex = FindContainingRegionIndex(virtualAddress);
|
||||||
|
if (regionIndex < 0)
|
||||||
{
|
{
|
||||||
if (virtualAddress < candidate.Region.VirtualAddress || virtualAddress >= candidate.EndAddress)
|
return false;
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var candidateOffset = checked((int)(virtualAddress - candidate.Region.VirtualAddress));
|
|
||||||
if (candidateOffset + length > candidate.BackingMemory.Length)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
region = candidate;
|
|
||||||
offset = candidateOffset;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
region = default;
|
var currentAddress = virtualAddress;
|
||||||
offset = 0;
|
var remaining = length;
|
||||||
return false;
|
var currentIndex = regionIndex;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (currentIndex >= _regions.Count)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var region = _regions[currentIndex];
|
||||||
|
if (currentAddress < region.Region.VirtualAddress ||
|
||||||
|
currentAddress >= region.EndAddress ||
|
||||||
|
(region.Region.Protection & requiredProtection) == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remaining == 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var available = region.EndAddress - currentAddress;
|
||||||
|
var chunkLength = (int)Math.Min((ulong)remaining, available);
|
||||||
|
remaining -= chunkLength;
|
||||||
|
if (remaining == 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentAddress += (ulong)chunkLength;
|
||||||
|
currentIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int FindContainingRegionIndex(ulong virtualAddress)
|
||||||
|
{
|
||||||
|
var insertionIndex = FindInsertionIndex(virtualAddress);
|
||||||
|
if (insertionIndex < _regions.Count &&
|
||||||
|
_regions[insertionIndex].Region.VirtualAddress == virtualAddress)
|
||||||
|
{
|
||||||
|
return insertionIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidateIndex = insertionIndex - 1;
|
||||||
|
return candidateIndex >= 0 && virtualAddress < _regions[candidateIndex].EndAddress
|
||||||
|
? candidateIndex
|
||||||
|
: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CopyFromRegions(ulong virtualAddress, Span<byte> destination, int regionIndex)
|
||||||
|
{
|
||||||
|
var copied = 0;
|
||||||
|
var currentAddress = virtualAddress;
|
||||||
|
while (copied < destination.Length)
|
||||||
|
{
|
||||||
|
var region = _regions[regionIndex++];
|
||||||
|
var regionOffset = checked((int)(currentAddress - region.Region.VirtualAddress));
|
||||||
|
var chunkLength = Math.Min(destination.Length - copied, region.BackingMemory.Length - regionOffset);
|
||||||
|
region.BackingMemory.AsSpan(regionOffset, chunkLength).CopyTo(destination[copied..]);
|
||||||
|
copied += chunkLength;
|
||||||
|
currentAddress += (ulong)chunkLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CopyToRegions(ulong virtualAddress, ReadOnlySpan<byte> source, int regionIndex)
|
||||||
|
{
|
||||||
|
var copied = 0;
|
||||||
|
var currentAddress = virtualAddress;
|
||||||
|
while (copied < source.Length)
|
||||||
|
{
|
||||||
|
var region = _regions[regionIndex++];
|
||||||
|
var regionOffset = checked((int)(currentAddress - region.Region.VirtualAddress));
|
||||||
|
var chunkLength = Math.Min(source.Length - copied, region.BackingMemory.Length - regionOffset);
|
||||||
|
source.Slice(copied, chunkLength).CopyTo(region.BackingMemory.AsSpan(regionOffset, chunkLength));
|
||||||
|
copied += chunkLength;
|
||||||
|
currentAddress += (ulong)chunkLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int FindInsertionIndex(ulong virtualAddress)
|
||||||
|
{
|
||||||
|
var lower = 0;
|
||||||
|
var upper = _regions.Count;
|
||||||
|
while (lower < upper)
|
||||||
|
{
|
||||||
|
var middle = lower + ((upper - lower) / 2);
|
||||||
|
if (_regions[middle].Region.VirtualAddress < virtualAddress)
|
||||||
|
{
|
||||||
|
lower = middle + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
upper = middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lower;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly record struct MappedRegion(VirtualMemoryRegion Region, ulong EndAddress, byte[] BackingMemory);
|
private readonly record struct MappedRegion(VirtualMemoryRegion Region, ulong EndAddress, byte[] BackingMemory);
|
||||||
|
|||||||
@@ -6,4 +6,6 @@ namespace SharpEmu.HLE;
|
|||||||
public interface IGuestMemoryAllocator
|
public interface IGuestMemoryAllocator
|
||||||
{
|
{
|
||||||
bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address);
|
bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address);
|
||||||
|
|
||||||
|
bool TryFreeGuestMemory(ulong address);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -629,6 +629,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
if (!InitializeMutexObject(ctx, handle, state))
|
if (!InitializeMutexObject(ctx, handle, state))
|
||||||
{
|
{
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
state.Semaphore.Dispose();
|
state.Semaphore.Dispose();
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -641,6 +642,7 @@ public static class KernelPthreadCompatExports
|
|||||||
_mutexStates.TryRemove(mutexAddress, out _);
|
_mutexStates.TryRemove(mutexAddress, out _);
|
||||||
_mutexStates.TryRemove(handle, out _);
|
_mutexStates.TryRemove(handle, out _);
|
||||||
|
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
state.Semaphore.Dispose();
|
state.Semaphore.Dispose();
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -668,6 +670,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, mutexAddress, 0);
|
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, mutexAddress, 0);
|
||||||
|
TryFreeOpaqueObject(ctx, resolvedAddress);
|
||||||
state.Semaphore.Dispose();
|
state.Semaphore.Dispose();
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -895,6 +898,7 @@ public static class KernelPthreadCompatExports
|
|||||||
var initialState = new PthreadMutexAttrState(MutexTypeErrorCheck, 0);
|
var initialState = new PthreadMutexAttrState(MutexTypeErrorCheck, 0);
|
||||||
if (!WriteMutexAttrObject(ctx, handle, initialState))
|
if (!WriteMutexAttrObject(ctx, handle, initialState))
|
||||||
{
|
{
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -912,6 +916,7 @@ public static class KernelPthreadCompatExports
|
|||||||
_mutexAttrStates.Remove(handle);
|
_mutexAttrStates.Remove(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -935,6 +940,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TryFreeOpaqueObject(ctx, resolvedAddress);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1195,6 +1201,7 @@ public static class KernelPthreadCompatExports
|
|||||||
{
|
{
|
||||||
if (_condStates.TryGetValue(condAddress, out var raced))
|
if (_condStates.TryGetValue(condAddress, out var raced))
|
||||||
{
|
{
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
resolvedAddress = condAddress;
|
resolvedAddress = condAddress;
|
||||||
state = raced;
|
state = raced;
|
||||||
return true;
|
return true;
|
||||||
@@ -1212,6 +1219,7 @@ public static class KernelPthreadCompatExports
|
|||||||
_condStates.Remove(handle);
|
_condStates.Remove(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1231,7 +1239,22 @@ public static class KernelPthreadCompatExports
|
|||||||
|
|
||||||
Span<byte> initialData = stackalloc byte[size];
|
Span<byte> initialData = stackalloc byte[size];
|
||||||
initialData.Clear();
|
initialData.Clear();
|
||||||
return ctx.Memory.TryWrite(address, initialData);
|
if (ctx.Memory.TryWrite(address, initialData))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
allocator.TryFreeGuestMemory(address);
|
||||||
|
address = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryFreeOpaqueObject(CpuContext ctx, ulong address)
|
||||||
|
{
|
||||||
|
if (ctx.Memory is IGuestMemoryAllocator allocator)
|
||||||
|
{
|
||||||
|
allocator.TryFreeGuestMemory(address);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool InitializeMutexObject(CpuContext ctx, ulong address, PthreadMutexState state) =>
|
private static bool InitializeMutexObject(CpuContext ctx, ulong address, PthreadMutexState state) =>
|
||||||
@@ -1269,6 +1292,7 @@ public static class KernelPthreadCompatExports
|
|||||||
_condStates.Remove(handle);
|
_condStates.Remove(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1293,6 +1317,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, 0);
|
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, 0);
|
||||||
|
TryFreeOpaqueObject(ctx, resolvedAddress);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1775,6 +1800,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
if (!InitializeMutexObject(ctx, handle, createdState))
|
if (!InitializeMutexObject(ctx, handle, createdState))
|
||||||
{
|
{
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
resolvedAddress = 0;
|
resolvedAddress = 0;
|
||||||
state = null;
|
state = null;
|
||||||
return false;
|
return false;
|
||||||
@@ -1784,12 +1810,14 @@ public static class KernelPthreadCompatExports
|
|||||||
{
|
{
|
||||||
if (_mutexStates.TryGetValue(mutexAddress, out state))
|
if (_mutexStates.TryGetValue(mutexAddress, out state))
|
||||||
{
|
{
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
resolvedAddress = mutexAddress;
|
resolvedAddress = mutexAddress;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_mutexStates.TryGetValue(handle, out state))
|
if (_mutexStates.TryGetValue(handle, out state))
|
||||||
{
|
{
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
resolvedAddress = handle;
|
resolvedAddress = handle;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1803,6 +1831,7 @@ public static class KernelPthreadCompatExports
|
|||||||
_mutexStates.TryRemove(mutexAddress, out _);
|
_mutexStates.TryRemove(mutexAddress, out _);
|
||||||
_mutexStates.TryRemove(handle, out _);
|
_mutexStates.TryRemove(handle, out _);
|
||||||
|
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
resolvedAddress = 0;
|
resolvedAddress = 0;
|
||||||
state = null;
|
state = null;
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.Core.Memory;
|
||||||
|
using SharpEmu.Core.Loader;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests.Memory;
|
||||||
|
|
||||||
|
public sealed class GuestMemoryAllocatorTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void FreedRangesAreReusedAndCoalesced()
|
||||||
|
{
|
||||||
|
using var memory = new PhysicalVirtualMemory(new FakeHostMemory());
|
||||||
|
const ulong usableArenaSize = 0x0100_0000 - 0x1000;
|
||||||
|
|
||||||
|
Assert.True(memory.TryAllocateGuestMemory(0x4000, 0x1000, out var first));
|
||||||
|
Assert.True(memory.TryAllocateGuestMemory(0x8000, 0x1000, out var second));
|
||||||
|
Assert.True(memory.TryAllocateGuestMemory(usableArenaSize - 0xC000, 0x1000, out var third));
|
||||||
|
Assert.False(memory.TryAllocateGuestMemory(1, 1, out _));
|
||||||
|
|
||||||
|
Assert.True(memory.TryFreeGuestMemory(second));
|
||||||
|
Assert.True(memory.TryAllocateGuestMemory(0x8000, 0x1000, out var reused));
|
||||||
|
Assert.Equal(second, reused);
|
||||||
|
|
||||||
|
Assert.True(memory.TryFreeGuestMemory(first));
|
||||||
|
Assert.True(memory.TryFreeGuestMemory(reused));
|
||||||
|
Assert.True(memory.TryFreeGuestMemory(third));
|
||||||
|
Assert.False(memory.TryFreeGuestMemory(third));
|
||||||
|
|
||||||
|
Assert.True(memory.TryAllocateGuestMemory(usableArenaSize, 0x1000, out var coalesced));
|
||||||
|
Assert.Equal(first, coalesced);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SegmentProtectionIsAppliedInContiguousRuns()
|
||||||
|
{
|
||||||
|
const ulong pageSize = 0x1000;
|
||||||
|
using var host = new RecordingHostMemory(3 * pageSize);
|
||||||
|
using var memory = new PhysicalVirtualMemory(host);
|
||||||
|
|
||||||
|
memory.Map(host.Address, 3 * pageSize, 0, ReadOnlySpan<byte>.Empty, ProgramHeaderFlags.Read);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
[
|
||||||
|
(host.Address, 3 * pageSize, HostPageProtection.ReadWrite),
|
||||||
|
(host.Address, 3 * pageSize, HostPageProtection.ReadOnly),
|
||||||
|
],
|
||||||
|
host.ProtectionCalls);
|
||||||
|
|
||||||
|
host.ProtectionCalls.Clear();
|
||||||
|
memory.Map(host.Address + pageSize, pageSize, 0, ReadOnlySpan<byte>.Empty, ProgramHeaderFlags.Write);
|
||||||
|
host.ProtectionCalls.Clear();
|
||||||
|
|
||||||
|
memory.Map(host.Address, 3 * pageSize, 0, ReadOnlySpan<byte>.Empty, ProgramHeaderFlags.Execute);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
[
|
||||||
|
(host.Address, 3 * pageSize, HostPageProtection.ReadWriteExecute),
|
||||||
|
(host.Address, pageSize, HostPageProtection.ReadExecute),
|
||||||
|
(host.Address + pageSize, pageSize, HostPageProtection.ReadWriteExecute),
|
||||||
|
(host.Address + (2 * pageSize), pageSize, HostPageProtection.ReadExecute),
|
||||||
|
],
|
||||||
|
host.ProtectionCalls);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public unsafe void GetPointerCommitsLazyPageBeforeReturningIt()
|
||||||
|
{
|
||||||
|
const ulong address = 0x00005000_0000_0000;
|
||||||
|
const ulong pageSize = 0x1000;
|
||||||
|
using var host = new LazyHostMemory(address);
|
||||||
|
using var memory = new PhysicalVirtualMemory(host);
|
||||||
|
memory.AllocateAt(address, (4UL << 30) + pageSize, executable: false, allowAlternative: false);
|
||||||
|
host.CommitCalls.Clear();
|
||||||
|
|
||||||
|
var pointer = memory.GetPointer(address + 0x123);
|
||||||
|
|
||||||
|
Assert.Equal(address + 0x123, (ulong)pointer);
|
||||||
|
Assert.Equal([(address, pageSize, HostPageProtection.ReadWrite)], host.CommitCalls);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public unsafe void GetPointerReturnsNullWhenLazyCommitFails()
|
||||||
|
{
|
||||||
|
const ulong address = 0x00005000_0000_0000;
|
||||||
|
using var host = new LazyHostMemory(address);
|
||||||
|
using var memory = new PhysicalVirtualMemory(host);
|
||||||
|
memory.AllocateAt(address, (4UL << 30) + 0x1000, executable: false, allowAlternative: false);
|
||||||
|
host.CommitCalls.Clear();
|
||||||
|
host.CommitSucceeds = false;
|
||||||
|
|
||||||
|
Assert.Equal(0UL, (ulong)memory.GetPointer(address));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakeHostMemory : IHostMemory
|
||||||
|
{
|
||||||
|
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
||||||
|
desiredAddress != 0 ? desiredAddress : 0x00007000_0000_0000;
|
||||||
|
|
||||||
|
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
||||||
|
Allocate(desiredAddress, size, protection);
|
||||||
|
|
||||||
|
public bool Commit(ulong address, ulong size, HostPageProtection protection) => true;
|
||||||
|
|
||||||
|
public bool Free(ulong address) => true;
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
info = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void FlushInstructionCache(ulong address, ulong size)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RecordingHostMemory : IHostMemory, IDisposable
|
||||||
|
{
|
||||||
|
private readonly nint _allocation;
|
||||||
|
private bool _freed;
|
||||||
|
|
||||||
|
public RecordingHostMemory(ulong size)
|
||||||
|
{
|
||||||
|
_allocation = System.Runtime.InteropServices.Marshal.AllocHGlobal(checked((nint)(size + 0xFFF)));
|
||||||
|
Address = (unchecked((ulong)_allocation) + 0xFFF) & ~0xFFFUL;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ulong Address { get; }
|
||||||
|
|
||||||
|
public List<(ulong Address, ulong Size, HostPageProtection Protection)> ProtectionCalls { get; } = [];
|
||||||
|
|
||||||
|
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
||||||
|
desiredAddress == Address ? Address : 0;
|
||||||
|
|
||||||
|
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) => 0;
|
||||||
|
|
||||||
|
public bool Commit(ulong address, ulong size, HostPageProtection protection) => true;
|
||||||
|
|
||||||
|
public bool Free(ulong address)
|
||||||
|
{
|
||||||
|
if (address != Address || _freed)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Runtime.InteropServices.Marshal.FreeHGlobal(_allocation);
|
||||||
|
_freed = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
|
||||||
|
{
|
||||||
|
ProtectionCalls.Add((address, size, protection));
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
info = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void FlushInstructionCache(ulong address, ulong size)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (!_freed)
|
||||||
|
{
|
||||||
|
System.Runtime.InteropServices.Marshal.FreeHGlobal(_allocation);
|
||||||
|
_freed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class LazyHostMemory(ulong address) : IHostMemory, IDisposable
|
||||||
|
{
|
||||||
|
public bool CommitSucceeds { get; set; } = true;
|
||||||
|
|
||||||
|
public List<(ulong Address, ulong Size, HostPageProtection Protection)> CommitCalls { get; } = [];
|
||||||
|
|
||||||
|
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) => 0;
|
||||||
|
|
||||||
|
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
||||||
|
desiredAddress == address ? address : 0;
|
||||||
|
|
||||||
|
public bool Commit(ulong commitAddress, ulong size, HostPageProtection protection)
|
||||||
|
{
|
||||||
|
CommitCalls.Add((commitAddress, size, protection));
|
||||||
|
return CommitSucceeds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Free(ulong freeAddress) => freeAddress == address;
|
||||||
|
|
||||||
|
public bool Protect(ulong protectAddress, ulong size, HostPageProtection protection, out uint rawOldProtection)
|
||||||
|
{
|
||||||
|
rawOldProtection = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ProtectRaw(ulong protectAddress, ulong size, uint rawProtection, out uint rawOldProtection)
|
||||||
|
{
|
||||||
|
rawOldProtection = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Query(ulong queryAddress, out HostRegionInfo info)
|
||||||
|
{
|
||||||
|
var pageAddress = queryAddress & ~0xFFFUL;
|
||||||
|
info = new HostRegionInfo(
|
||||||
|
pageAddress,
|
||||||
|
address,
|
||||||
|
0x1000,
|
||||||
|
HostRegionState.Reserved,
|
||||||
|
0,
|
||||||
|
HostPageProtection.NoAccess,
|
||||||
|
0,
|
||||||
|
0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void FlushInstructionCache(ulong flushAddress, ulong size)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.Core.Loader;
|
||||||
|
using SharpEmu.Core.Memory;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests.Memory;
|
||||||
|
|
||||||
|
public sealed class VirtualMemoryTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void OutOfOrderMappingsRemainSortedAndResolveAtBoundaries()
|
||||||
|
{
|
||||||
|
var memory = new VirtualMemory();
|
||||||
|
memory.Map(0x3000, 0x100, 0, [3], ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
|
||||||
|
memory.Map(0x1000, 0x100, 0, [1], ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
|
||||||
|
memory.Map(0x2000, 0x100, 0, [2], ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
|
||||||
|
|
||||||
|
Assert.Equal([0x1000UL, 0x2000UL, 0x3000UL], memory.SnapshotRegions().Select(region => region.VirtualAddress));
|
||||||
|
|
||||||
|
Span<byte> value = stackalloc byte[1];
|
||||||
|
Assert.True(memory.TryRead(0x1000, value));
|
||||||
|
Assert.Equal(1, value[0]);
|
||||||
|
Assert.True(memory.TryRead(0x2000, value));
|
||||||
|
Assert.Equal(2, value[0]);
|
||||||
|
Assert.True(memory.TryRead(0x3000, value));
|
||||||
|
Assert.Equal(3, value[0]);
|
||||||
|
Assert.False(memory.TryRead(0x1100, value));
|
||||||
|
Assert.False(memory.TryRead(0x0FFF, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MappingRejectsOverlapWithEitherNeighbor()
|
||||||
|
{
|
||||||
|
var memory = new VirtualMemory();
|
||||||
|
memory.Map(0x2000, 0x100, 0, [], ProgramHeaderFlags.Read);
|
||||||
|
memory.Map(0x4000, 0x100, 0, [], ProgramHeaderFlags.Read);
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
memory.Map(0x1FFF, 2, 0, [], ProgramHeaderFlags.Read));
|
||||||
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
memory.Map(0x3FFF, 2, 0, [], ProgramHeaderFlags.Read));
|
||||||
|
|
||||||
|
memory.Map(0x2100, 0x1F00, 0, [], ProgramHeaderFlags.Read);
|
||||||
|
Assert.Equal(3, memory.SnapshotRegions().Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReadAndWriteSpanAdjacentRegions()
|
||||||
|
{
|
||||||
|
var memory = new VirtualMemory();
|
||||||
|
const ProgramHeaderFlags protection = ProgramHeaderFlags.Read | ProgramHeaderFlags.Write;
|
||||||
|
memory.Map(0x1000, 4, 0, [1, 2, 3, 4], protection);
|
||||||
|
memory.Map(0x1004, 4, 0, [5, 6, 7, 8], protection);
|
||||||
|
|
||||||
|
Span<byte> read = stackalloc byte[4];
|
||||||
|
Assert.True(memory.TryRead(0x1002, read));
|
||||||
|
Assert.Equal([3, 4, 5, 6], read.ToArray());
|
||||||
|
|
||||||
|
Assert.True(memory.TryWrite(0x1002, [9, 10, 11, 12]));
|
||||||
|
Assert.True(memory.TryRead(0x1000, read));
|
||||||
|
Assert.Equal([1, 2, 9, 10], read.ToArray());
|
||||||
|
Assert.True(memory.TryRead(0x1004, read));
|
||||||
|
Assert.Equal([11, 12, 7, 8], read.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AccessRequiresPermissionAcrossEntireRange()
|
||||||
|
{
|
||||||
|
var memory = new VirtualMemory();
|
||||||
|
memory.Map(0x1000, 4, 0, [1, 2, 3, 4], ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
|
||||||
|
memory.Map(0x1004, 4, 0, [5, 6, 7, 8], ProgramHeaderFlags.Read);
|
||||||
|
memory.Map(0x2000, 4, 0, [1, 2, 3, 4], ProgramHeaderFlags.Execute);
|
||||||
|
memory.Map(0x3000, 4, 0, [], ProgramHeaderFlags.Write);
|
||||||
|
|
||||||
|
Assert.False(memory.TryWrite(0x1002, [9, 9, 9, 9]));
|
||||||
|
Span<byte> unchanged = stackalloc byte[4];
|
||||||
|
Assert.True(memory.TryRead(0x1000, unchanged));
|
||||||
|
Assert.Equal([1, 2, 3, 4], unchanged.ToArray());
|
||||||
|
|
||||||
|
Assert.False(memory.TryRead(0x2000, unchanged));
|
||||||
|
Assert.False(memory.TryWrite(0x2000, [9]));
|
||||||
|
Assert.False(memory.TryRead(0x3000, unchanged));
|
||||||
|
Assert.True(memory.TryWrite(0x3000, [9]));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,9 +7,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\SharpEmu.Core\SharpEmu.Core.csproj" />
|
||||||
<ProjectReference Include="..\..\src\SharpEmu.Libs\SharpEmu.Libs.csproj" />
|
<ProjectReference Include="..\..\src\SharpEmu.Libs\SharpEmu.Libs.csproj" />
|
||||||
<ProjectReference Include="..\..\src\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
<ProjectReference Include="..\..\src\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -149,6 +149,15 @@
|
|||||||
"xunit.extensibility.core": "[2.9.3]"
|
"xunit.extensibility.core": "[2.9.3]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"sharpemu.core": {
|
||||||
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"Iced": "[1.21.0, )",
|
||||||
|
"SharpEmu.HLE": "[1.0.0, )",
|
||||||
|
"SharpEmu.Libs": "[1.0.0, )",
|
||||||
|
"SharpEmu.Logging": "[1.0.0, )"
|
||||||
|
}
|
||||||
|
},
|
||||||
"sharpemu.hle": {
|
"sharpemu.hle": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -168,6 +177,12 @@
|
|||||||
"sharpemu.logging": {
|
"sharpemu.logging": {
|
||||||
"type": "Project"
|
"type": "Project"
|
||||||
},
|
},
|
||||||
|
"Iced": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[1.21.0, )",
|
||||||
|
"resolved": "1.21.0",
|
||||||
|
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
|
||||||
|
},
|
||||||
"Silk.NET.Vulkan": {
|
"Silk.NET.Vulkan": {
|
||||||
"type": "CentralTransitive",
|
"type": "CentralTransitive",
|
||||||
"requested": "[2.23.0, )",
|
"requested": "[2.23.0, )",
|
||||||
|
|||||||
Reference in New Issue
Block a user