[GPU/HLE] Cut per-draw and per-call managed allocations on hot paths (#1054)
* [GPU/HLE] Cut per-draw and per-call allocations on hot paths * [HLE] Keep nested tracker uploads from sharing rollback scratch
This commit is contained in:
parent
98f223ce69
commit
8acc081205
|
|
@ -242,50 +242,108 @@ public sealed class GuestPageTracker
|
|||
}
|
||||
}
|
||||
|
||||
// Region locks stay held across uploadFunc only for a written range, which turns GPU-dirty.
|
||||
// Receives the CPU-dirty runs an upload copies, then performs the upload while the regions are held.
|
||||
public interface IUploadRangeSink
|
||||
{
|
||||
void Range(ulong address, ulong size);
|
||||
|
||||
void Upload();
|
||||
}
|
||||
|
||||
private struct DelegateUploadSink(Action<ulong, ulong> rangeFunc, Action uploadFunc) : IUploadRangeSink
|
||||
{
|
||||
public readonly void Range(ulong address, ulong size) => rangeFunc(address, size);
|
||||
|
||||
public readonly void Upload() => uploadFunc();
|
||||
}
|
||||
|
||||
// Forwards a region's upload runs to the sink and remembers the runs it cleared, for rollback.
|
||||
private struct RegionUploadVisitor<TSink>(TSink sink, List<(TrackedRegion Region, ulong Address, ulong Size)> cleared)
|
||||
: TrackedRegion.ICpuUploadVisitor
|
||||
where TSink : struct, IUploadRangeSink
|
||||
{
|
||||
public TSink Sink = sink;
|
||||
|
||||
public readonly void Cleared(TrackedRegion region, ulong address, ulong size) => cleared.Add((region, address, size));
|
||||
|
||||
public void Upload(ulong address, ulong size) => Sink.Range(address, size);
|
||||
}
|
||||
|
||||
// Uploads run on every bound buffer of every draw, so the bookkeeping lists are reused per thread.
|
||||
[ThreadStatic]
|
||||
private static List<TrackedRegion>? _scratchHeld;
|
||||
|
||||
[ThreadStatic]
|
||||
private static List<(TrackedRegion Region, ulong Address, ulong Size)>? _scratchCleared;
|
||||
|
||||
public void ForEachUploadRange(ulong vaddr, ulong size, bool isWritten, Action<ulong, ulong> rangeFunc, Action uploadFunc,
|
||||
bool preserveCpuWriteHotPages = true)
|
||||
{
|
||||
var sink = new DelegateUploadSink(rangeFunc, uploadFunc);
|
||||
ForEachUploadRange(vaddr, size, isWritten, ref sink, preserveCpuWriteHotPages);
|
||||
}
|
||||
|
||||
// Region locks stay held across the upload only for a written range, which turns GPU-dirty.
|
||||
public void ForEachUploadRange<TSink>(ulong vaddr, ulong size, bool isWritten, ref TSink sink, bool preserveCpuWriteHotPages = true)
|
||||
where TSink : struct, IUploadRangeSink
|
||||
{
|
||||
RejectUploadCallbackReentry();
|
||||
VisitRegions(vaddr, size, create: true, static (_, _, _) => false);
|
||||
var previousOwner = _uploadOwner;
|
||||
_uploadOwner = this;
|
||||
var held = new List<TrackedRegion>();
|
||||
var cleared = new List<(TrackedRegion Region, ulong Address, ulong Size)>();
|
||||
// Another tracker may upload from this callback. It needs its own rollback
|
||||
// state; the reentry guard only rejects reentry into this tracker.
|
||||
var held = previousOwner is null
|
||||
? _scratchHeld ??= new List<TrackedRegion>()
|
||||
: new List<TrackedRegion>();
|
||||
var cleared = previousOwner is null
|
||||
? _scratchCleared ??= new List<(TrackedRegion Region, ulong Address, ulong Size)>()
|
||||
: new List<(TrackedRegion Region, ulong Address, ulong Size)>();
|
||||
held.Clear();
|
||||
cleared.Clear();
|
||||
try
|
||||
{
|
||||
using (GpuMemoryAccessProfile.Measure(GpuMemoryAccessProfile.Operation.UploadTracking, size))
|
||||
{
|
||||
VisitRegions(vaddr, size, create: false, (region, offset, bytes) =>
|
||||
var visitor = new RegionUploadVisitor<TSink>(sink, cleared);
|
||||
var preserveHotPages = !isWritten && preserveCpuWriteHotPages;
|
||||
for (var (index, offset, remaining) = (vaddr / BlockBytes, vaddr % BlockBytes, size); remaining != 0; index++, offset = 0)
|
||||
{
|
||||
var bytes = Math.Min(BlockBytes - offset, remaining);
|
||||
remaining -= bytes;
|
||||
if (Volatile.Read(ref _regions[index]) is not { } region)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
region.Lock.Enter();
|
||||
held.Add(region);
|
||||
var address = region.BaseAddress + offset;
|
||||
region.ForEachCpuUploadRange(
|
||||
preserveHotPages: !isWritten && preserveCpuWriteHotPages,
|
||||
address,
|
||||
bytes,
|
||||
(runAddress, runSize) => cleared.Add((region, runAddress, runSize)),
|
||||
rangeFunc);
|
||||
region.ForEachCpuUploadRange(preserveHotPages, region.BaseAddress + offset, bytes, ref visitor);
|
||||
if (!isWritten)
|
||||
{
|
||||
region.Lock.Exit();
|
||||
held.Remove(region);
|
||||
held.RemoveAt(held.Count - 1);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
sink = visitor.Sink;
|
||||
}
|
||||
uploadFunc();
|
||||
sink.Upload();
|
||||
if (isWritten)
|
||||
{
|
||||
VisitRegions(vaddr, size, create: false, (region, offset, bytes) =>
|
||||
for (var (index, offset, remaining) = (vaddr / BlockBytes, vaddr % BlockBytes, size); remaining != 0; index++, offset = 0)
|
||||
{
|
||||
var bytes = Math.Min(BlockBytes - offset, remaining);
|
||||
remaining -= bytes;
|
||||
if (Volatile.Read(ref _regions[index]) is not { } region)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
region.ChangeState(WriteOrigin.Gpu, enable: true, region.BaseAddress + offset, bytes);
|
||||
region.Lock.Exit();
|
||||
held.Remove(region);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
|
|
@ -333,6 +391,8 @@ public sealed class GuestPageTracker
|
|||
region.Lock.Exit();
|
||||
}
|
||||
|
||||
held.Clear();
|
||||
cleared.Clear();
|
||||
_uploadOwner = previousOwner;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,20 +144,24 @@ public sealed class TrackedRegion
|
|||
}
|
||||
}
|
||||
|
||||
// Receives the runs one upload clears and the runs it copies; a struct visitor keeps the walk allocation-free.
|
||||
public interface ICpuUploadVisitor
|
||||
{
|
||||
void Cleared(TrackedRegion region, ulong address, ulong size);
|
||||
|
||||
void Upload(ulong address, ulong size);
|
||||
}
|
||||
|
||||
// Hot read-only pages stay writable and dirty, so each obtain observes current CPU bytes.
|
||||
public void ForEachCpuUploadRange(
|
||||
bool preserveHotPages,
|
||||
ulong address,
|
||||
ulong size,
|
||||
Action<ulong, ulong> visitCleared,
|
||||
Action<ulong, ulong> visitUpload)
|
||||
public void ForEachCpuUploadRange<TVisitor>(bool preserveHotPages, ulong address, ulong size, ref TVisitor visitor)
|
||||
where TVisitor : struct, ICpuUploadVisitor
|
||||
{
|
||||
var (start, end) = GetPageRange(address, size);
|
||||
var upload = new PageMask(_cpuDirty, start, end);
|
||||
var cleared = preserveHotPages ? upload & ~_hotCpuWrites : upload;
|
||||
foreach (var (runStart, runEnd) in cleared)
|
||||
{
|
||||
visitCleared(BaseAddress + (ulong)runStart * PageBytes, (ulong)(runEnd - runStart) * PageBytes);
|
||||
visitor.Cleared(this, BaseAddress + (ulong)runStart * PageBytes, (ulong)(runEnd - runStart) * PageBytes);
|
||||
_cpuDirty.UnsetRange(runStart, runEnd);
|
||||
_recentCpuUploads.SetRange(runStart, runEnd);
|
||||
}
|
||||
|
|
@ -165,7 +169,7 @@ public sealed class TrackedRegion
|
|||
UpdateCpuProtection(track: true);
|
||||
foreach (var (runStart, runEnd) in upload)
|
||||
{
|
||||
visitUpload(BaseAddress + (ulong)runStart * PageBytes, (ulong)(runEnd - runStart) * PageBytes);
|
||||
visitor.Upload(BaseAddress + (ulong)runStart * PageBytes, (ulong)(runEnd - runStart) * PageBytes);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -455,11 +455,15 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
// The fill pattern staged per thread; DCC fills arrive with every compressed target clear.
|
||||
[ThreadStatic]
|
||||
private static uint[]? _fillPattern;
|
||||
|
||||
private bool TryWriteFillToBacking(ulong guestAddress, ulong size, uint value)
|
||||
{
|
||||
var values = new uint[(int)Math.Min(size / sizeof(uint), 4096)];
|
||||
Array.Fill(values, value);
|
||||
var bytes = MemoryMarshal.AsBytes<uint>(values);
|
||||
var values = (_fillPattern ??= new uint[4096]).AsSpan(0, (int)Math.Min(size / sizeof(uint), 4096));
|
||||
values.Fill(value);
|
||||
var bytes = MemoryMarshal.AsBytes(values);
|
||||
for (ulong offset = 0; offset < size;)
|
||||
{
|
||||
var chunk = (int)Math.Min(size - offset, (ulong)bytes.Length);
|
||||
|
|
@ -2043,20 +2047,11 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
profileScope.SwitchPhase(isWritten
|
||||
? RenderPhaseProfile.Phase.BufferDirtySyncWritten
|
||||
: isTexelBuffer ? RenderPhaseProfile.Phase.BufferDirtySyncTexel : RenderPhaseProfile.Phase.BufferDirtySyncUpload);
|
||||
var copies = new List<BufferCopy>();
|
||||
var totalSize = 0UL;
|
||||
GpuBuffer? source = null;
|
||||
_tracker.ForEachUploadRange(
|
||||
guestAddress,
|
||||
size,
|
||||
isWritten,
|
||||
(address, bytes) =>
|
||||
{
|
||||
copies.Add(new BufferCopy(totalSize, buffer.Offset(address), bytes));
|
||||
totalSize += bytes;
|
||||
},
|
||||
() => source = _uploader.PrepareSource(buffer.CpuAddress, CollectionsMarshal.AsSpan(copies), totalSize, guestAddress, size),
|
||||
preserveCpuWriteHotPages);
|
||||
var copies = _syncCopies ??= new List<BufferCopy>();
|
||||
copies.Clear();
|
||||
var sink = new UploadSink(_uploader, buffer, copies, guestAddress, size);
|
||||
_tracker.ForEachUploadRange(guestAddress, size, isWritten, ref sink, preserveCpuWriteHotPages);
|
||||
var source = sink.Source;
|
||||
if (source != null)
|
||||
{
|
||||
buffer.NoteGpuWrite();
|
||||
|
|
@ -2098,7 +2093,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
ulong hotBytes = 0;
|
||||
foreach (var copy in copies)
|
||||
hotBytes += _tracker.CountCpuWriteHotBytes(buffer.CpuAddress + copy.DstOffset, copy.Size);
|
||||
BufferUploadProfile.Record(guestAddress, size, copies.Count, totalSize, hotBytes, elapsedTicks);
|
||||
BufferUploadProfile.Record(guestAddress, size, copies.Count, sink.TotalSize, hotBytes, elapsedTicks);
|
||||
}
|
||||
|
||||
if (isTexelBuffer)
|
||||
|
|
@ -2115,6 +2110,28 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
|
|||
return false;
|
||||
}
|
||||
|
||||
// The copies of one synchronization; the tracker forbids nesting, so one list per thread is reused.
|
||||
[ThreadStatic]
|
||||
private static List<BufferCopy>? _syncCopies;
|
||||
|
||||
// Collects a buffer's CPU-dirty runs as copies, then stages them while the tracker holds the regions.
|
||||
private struct UploadSink(GuestBufferUploader uploader, GpuBuffer buffer, List<BufferCopy> copies, ulong guestAddress, ulong size)
|
||||
: GuestPageTracker.IUploadRangeSink
|
||||
{
|
||||
public ulong TotalSize { get; private set; }
|
||||
|
||||
public GpuBuffer? Source { get; private set; }
|
||||
|
||||
public void Range(ulong address, ulong bytes)
|
||||
{
|
||||
copies.Add(new BufferCopy(TotalSize, buffer.Offset(address), bytes));
|
||||
TotalSize += bytes;
|
||||
}
|
||||
|
||||
public void Upload() =>
|
||||
Source = uploader.PrepareSource(buffer.CpuAddress, CollectionsMarshal.AsSpan(copies), TotalSize, guestAddress, size);
|
||||
}
|
||||
|
||||
private GpuBuffer? FindOwner(ulong guestAddress, ulong size)
|
||||
{
|
||||
return _registry.TryGetRegisteredBuffer(_registry.FindContainingBuffer(guestAddress, size));
|
||||
|
|
|
|||
|
|
@ -40,9 +40,12 @@ public sealed unsafe partial class CachedImage
|
|||
};
|
||||
|
||||
// Barriers for the requested state; a repeated write always gets a barrier.
|
||||
private static readonly List<ImageMemoryBarrier2> NoBarriers = new(0);
|
||||
|
||||
public (List<ImageMemoryBarrier2> Barriers, PipelineStageFlags SourceStages) GetBarriers(ImageLayout layout, AccessFlags access, PipelineStageFlags stage, SubresourceRange? range)
|
||||
{
|
||||
var barriers = new List<ImageMemoryBarrier2>();
|
||||
// Most requests need no barrier; the list exists only once one does. Callers never add to it.
|
||||
List<ImageMemoryBarrier2>? barriers = null;
|
||||
PipelineStageFlags sourceStages = 0;
|
||||
if ((access & (WriteAccess | AccessFlags.ColorAttachmentWriteBit | AccessFlags.DepthStencilAttachmentWriteBit)) != 0)
|
||||
{
|
||||
|
|
@ -95,7 +98,7 @@ public sealed unsafe partial class CachedImage
|
|||
}
|
||||
else if (state.Layout != layout || state.Access != access || repeatedWrite)
|
||||
{
|
||||
barriers.Add(MakeBarrier(state, layout, access, level, 1, layer, 1));
|
||||
(barriers ??= new List<ImageMemoryBarrier2>(1)).Add(MakeBarrier(state, layout, access, level, 1, layer, 1));
|
||||
sourceStages |= state.Stage;
|
||||
states[index] = new ImageAccessState(stage, access, layout);
|
||||
}
|
||||
|
|
@ -113,21 +116,21 @@ public sealed unsafe partial class CachedImage
|
|||
var repeatedWrite = (state.Access & WriteAccess) != 0;
|
||||
if (state.Layout == layout && state.Access == access && !repeatedWrite)
|
||||
{
|
||||
return (barriers, sourceStages);
|
||||
return (barriers ?? NoBarriers, sourceStages);
|
||||
}
|
||||
|
||||
if (IsReadAfterRead(state, layout, access))
|
||||
{
|
||||
Backing.State = new ImageAccessState(state.Stage | stage, state.Access | access, layout);
|
||||
return (barriers, sourceStages);
|
||||
return (barriers ?? NoBarriers, sourceStages);
|
||||
}
|
||||
|
||||
barriers.Add(MakeBarrier(state, layout, access, 0, Vk.RemainingMipLevels, 0, Vk.RemainingArrayLayers));
|
||||
(barriers ??= new List<ImageMemoryBarrier2>(1)).Add(MakeBarrier(state, layout, access, 0, Vk.RemainingMipLevels, 0, Vk.RemainingArrayLayers));
|
||||
sourceStages |= state.Stage;
|
||||
}
|
||||
|
||||
Backing.State = new ImageAccessState(stage, access, layout);
|
||||
return (barriers, sourceStages);
|
||||
return (barriers ?? NoBarriers, sourceStages);
|
||||
}
|
||||
|
||||
// The layout every subresource in the range is in, or null when they differ.
|
||||
|
|
@ -217,7 +220,7 @@ public sealed unsafe partial class CachedImage
|
|||
|
||||
private void RecordBarriers(CommandBuffer command, PipelineStageFlags sourceStages, PipelineStageFlags destinationStages, BufferMemoryBarrier2* bufferBarrier, List<ImageMemoryBarrier2> imageBarriers)
|
||||
{
|
||||
var images = imageBarriers.ToArray();
|
||||
var images = System.Runtime.InteropServices.CollectionsMarshal.AsSpan(imageBarriers);
|
||||
fixed (ImageMemoryBarrier2* imagePointer = images)
|
||||
{
|
||||
VulkanSynchronization.PipelineBarrier(_device.Vk,
|
||||
|
|
|
|||
|
|
@ -2358,41 +2358,53 @@ public static partial class KernelMemoryCompatExports
|
|||
positionBefore = -1;
|
||||
}
|
||||
|
||||
var buffer = GC.AllocateUninitializedArray<byte>(requested);
|
||||
var read = stream.Read(buffer, 0, requested);
|
||||
if (read > 0 && useBinkCompletionShim)
|
||||
{
|
||||
// The patched NumFrames field is what tells the guest "this
|
||||
// movie is fully consumed" - hold that specific read until the
|
||||
// host has actually finished showing it, so guest-side game
|
||||
// logic can't race ahead of what's still on screen.
|
||||
if (completionShim.Patch(positionBefore, buffer.AsSpan(0, read)))
|
||||
{
|
||||
HostMovieBridge.WaitForHostPlaybackToFinish(stream.Name);
|
||||
}
|
||||
}
|
||||
if (read > 0 && !ctx.Memory.TryWrite(bufferAddress, buffer.AsSpan(0, read)))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
long positionAfter;
|
||||
// Guest reads are frequent and mostly small: a pooled buffer spares each one an allocation.
|
||||
var pooled = requested <= MaxPooledReadBytes;
|
||||
var buffer = pooled ? ArrayPool<byte>.Shared.Rent(requested) : GC.AllocateUninitializedArray<byte>(requested);
|
||||
try
|
||||
{
|
||||
positionAfter = stream.Position;
|
||||
var read = stream.Read(buffer, 0, requested);
|
||||
if (read > 0 && useBinkCompletionShim)
|
||||
{
|
||||
// The patched NumFrames field is what tells the guest "this
|
||||
// movie is fully consumed" - hold that specific read until the
|
||||
// host has actually finished showing it, so guest-side game
|
||||
// logic can't race ahead of what's still on screen.
|
||||
if (completionShim.Patch(positionBefore, buffer.AsSpan(0, read)))
|
||||
{
|
||||
HostMovieBridge.WaitForHostPlaybackToFinish(stream.Name);
|
||||
}
|
||||
}
|
||||
if (read > 0 && !ctx.Memory.TryWrite(bufferAddress, buffer.AsSpan(0, read)))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
long positionAfter;
|
||||
try
|
||||
{
|
||||
positionAfter = stream.Position;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
positionAfter = -1;
|
||||
}
|
||||
|
||||
LogIoTrace(
|
||||
"read",
|
||||
stream.Name,
|
||||
$"fd={fd} req={requested} read={read} pos={positionBefore}->{positionAfter} preview='{PreviewIoBytes(buffer, read, 64)}' hex={PreviewIoHex(buffer, read, 32)} guest_tail={PreviewGuestHex(ctx, bufferAddress + (ulong)Math.Max(read, 0), 32)}");
|
||||
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)read);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
catch (IOException)
|
||||
finally
|
||||
{
|
||||
positionAfter = -1;
|
||||
if (pooled)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
LogIoTrace(
|
||||
"read",
|
||||
stream.Name,
|
||||
$"fd={fd} req={requested} read={read} pos={positionBefore}->{positionAfter} preview='{PreviewIoBytes(buffer, read, 64)}' hex={PreviewIoHex(buffer, read, 32)} guest_tail={PreviewGuestHex(ctx, bufferAddress + (ulong)Math.Max(read, 0), 32)}");
|
||||
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)read);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
|
@ -7206,9 +7218,41 @@ public static partial class KernelMemoryCompatExports
|
|||
}
|
||||
|
||||
// Read once: file-heavy titles call LogIoTrace for every resolved path.
|
||||
private const int MaxPooledReadBytes = 1 << 20;
|
||||
|
||||
private static readonly bool _logIo =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_IO"), "1", StringComparison.Ordinal);
|
||||
|
||||
// Builds an I/O trace detail only while SHARPEMU_LOG_IO is on: its holes (byte previews of
|
||||
// every read) are otherwise never evaluated.
|
||||
[System.Runtime.CompilerServices.InterpolatedStringHandler]
|
||||
private ref struct IoTraceDetail
|
||||
{
|
||||
private System.Runtime.CompilerServices.DefaultInterpolatedStringHandler _inner;
|
||||
|
||||
public IoTraceDetail(int literalLength, int formattedCount, out bool enabled)
|
||||
{
|
||||
enabled = _logIo;
|
||||
_inner = enabled ? new System.Runtime.CompilerServices.DefaultInterpolatedStringHandler(literalLength, formattedCount) : default;
|
||||
}
|
||||
|
||||
public void AppendLiteral(string value) => _inner.AppendLiteral(value);
|
||||
|
||||
public void AppendFormatted<T>(T value) => _inner.AppendFormatted(value);
|
||||
|
||||
public void AppendFormatted<T>(T value, string? format) => _inner.AppendFormatted(value, format);
|
||||
|
||||
public string ToStringAndClear() => _inner.ToStringAndClear();
|
||||
}
|
||||
|
||||
private static void LogIoTrace(string operation, string path, IoTraceDetail detail)
|
||||
{
|
||||
if (_logIo)
|
||||
{
|
||||
LogIoTrace(operation, path, detail.ToStringAndClear());
|
||||
}
|
||||
}
|
||||
|
||||
private static void LogIoTrace(string operation, string path, string detail)
|
||||
{
|
||||
if (!_logIo)
|
||||
|
|
|
|||
|
|
@ -1054,12 +1054,7 @@ public static class KernelPthreadCompatExports
|
|||
}
|
||||
|
||||
if (canCooperativelyBlock && waiter is not null &&
|
||||
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"pthread_mutex_lock",
|
||||
waiter.WakeKey,
|
||||
() => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter),
|
||||
() => TryGrantBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter)))
|
||||
RequestBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter))
|
||||
{
|
||||
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
|
|
@ -1070,6 +1065,21 @@ public static class KernelPthreadCompatExports
|
|||
return hostResult;
|
||||
}
|
||||
|
||||
// The continuations capture the lock's arguments in a closure; keeping them out of
|
||||
// PthreadMutexLockCore spares every uncontended lock that allocation.
|
||||
private static bool RequestBlockedMutexLock(
|
||||
CpuContext ctx,
|
||||
ulong mutexAddress,
|
||||
ulong resolvedAddress,
|
||||
PthreadMutexState state,
|
||||
PthreadMutexWaiter waiter) =>
|
||||
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"pthread_mutex_lock",
|
||||
waiter.WakeKey,
|
||||
() => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter),
|
||||
() => TryGrantBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter));
|
||||
|
||||
private static int PthreadMutexUnlockCore(CpuContext ctx, ulong mutexAddress, bool requireOwner)
|
||||
{
|
||||
if (mutexAddress == 0)
|
||||
|
|
|
|||
|
|
@ -62,14 +62,7 @@ internal static class ScalarGraphDiskCache
|
|||
Directory.CreateDirectory(directory);
|
||||
// Keep a bounded cache across compiler versions too. No live
|
||||
// analysis objects or guest memory are retained by the cache.
|
||||
var entries = new DirectoryInfo(directory).GetFiles("*.graph").OrderBy(f => f.LastWriteTimeUtc).ToArray();
|
||||
var total = entries.Sum(f => f.Length) + stream.Length + 32;
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (total <= MaxCacheBytes) break;
|
||||
total -= entry.Length;
|
||||
entry.Delete();
|
||||
}
|
||||
EvictOldest(directory, stream.Length + 32);
|
||||
var temporary = path + "." + Guid.NewGuid().ToString("N") + ".tmp";
|
||||
try
|
||||
{
|
||||
|
|
@ -79,6 +72,8 @@ internal static class ScalarGraphDiskCache
|
|||
file.Write(bytes);
|
||||
}
|
||||
File.Move(temporary, path, true);
|
||||
_entries!.Enqueue((path, stream.Length + 32));
|
||||
_totalBytes += stream.Length + 32;
|
||||
}
|
||||
finally { File.Delete(temporary); }
|
||||
}
|
||||
|
|
@ -89,6 +84,30 @@ internal static class ScalarGraphDiskCache
|
|||
return graph;
|
||||
}
|
||||
|
||||
// The entries of the cache directory, oldest first, and their total size. The directory is
|
||||
// listed once per process: a new build misses on every shader, and listing it per miss cost
|
||||
// more than the analysis it saved. Caller holds WriteLock.
|
||||
private static string? _entriesDirectory;
|
||||
private static Queue<(string Path, long Bytes)>? _entries;
|
||||
private static long _totalBytes;
|
||||
|
||||
private static void EvictOldest(string directory, long incoming)
|
||||
{
|
||||
if (_entries is null || _entriesDirectory != directory)
|
||||
{
|
||||
var files = new DirectoryInfo(directory).GetFiles("*.graph").OrderBy(f => f.LastWriteTimeUtc).ToArray();
|
||||
_entries = new Queue<(string Path, long Bytes)>(files.Select(f => (f.FullName, f.Length)));
|
||||
_totalBytes = files.Sum(f => f.Length);
|
||||
_entriesDirectory = directory;
|
||||
}
|
||||
|
||||
while (_totalBytes + incoming > MaxCacheBytes && _entries.TryDequeue(out var entry))
|
||||
{
|
||||
_totalBytes -= entry.Bytes;
|
||||
File.Delete(entry.Path);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string Key(Gen5ShaderProgram program, uint userDataBase, uint userDataCount,
|
||||
IReadOnlySet<uint>? fixedFunctionVertexLoads, uint waveSize)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -739,6 +739,31 @@ public sealed class GuestPageTrackerTests : IDisposable
|
|||
Release(address, Page);
|
||||
}
|
||||
|
||||
[NativePageProtectionFact]
|
||||
public void NestedUploadOnAnotherTrackerKeepsTheOuterRollbackState()
|
||||
{
|
||||
using var other = new GuestPageTrackerTests();
|
||||
var address = Allocate(1);
|
||||
var otherAddress = other.Allocate(1);
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
_tracker.ForEachUploadRange(address, Page, false, NoRange, () =>
|
||||
{
|
||||
other._tracker.ForEachUploadRange(otherAddress, Page, false, NoRange, NoUpload);
|
||||
throw new InvalidOperationException("outer upload failed");
|
||||
}));
|
||||
|
||||
Assert.True(_tracker.HasCpuDirtyPages(address, Page));
|
||||
Assert.True(IsWritable(address));
|
||||
Assert.False(other._tracker.HasCpuDirtyPages(otherAddress, Page));
|
||||
var uploaded = 0;
|
||||
_tracker.ForEachUploadRange(address, Page, false, (_, _) => uploaded++, NoUpload);
|
||||
Assert.Equal(1, uploaded);
|
||||
_tracker.UntrackMemory(address, Page);
|
||||
Release(address, Page);
|
||||
other._tracker.UntrackMemory(otherAddress, Page);
|
||||
other.Release(otherAddress, Page);
|
||||
}
|
||||
|
||||
[NativePageProtectionTheory]
|
||||
[InlineData(false, true)]
|
||||
[InlineData(true, true)]
|
||||
|
|
|
|||
Loading…
Reference in New Issue