[Gpu] Reuse image lookups that found the same backing
FindImage remembers, in a 256-slot table, each request that resolved to an image with the same backing, keyed by the request bytes and a generation that every index registration or removal advances. A repeated request touches the image and returns the remembered result without walking the page owner index. SHARPEMU_IMAGE_LOOKUP_MEMO=0 turns it off; [PERF][IMAGE_LOOKUP] reports hits and misses.
This commit is contained in:
parent
f125d17510
commit
2a42567bcf
|
|
@ -193,12 +193,14 @@ public sealed partial class GuestImageCache
|
|||
internal void AddPageOwner(ulong address, ResourceSlotIdentifier imageIdentifier)
|
||||
{
|
||||
using var held = _lock.Hold();
|
||||
InvalidateLookups();
|
||||
_pageOwners.GetOrCreate(address >> ImagePageOwnerTable.PageBits).Add(imageIdentifier);
|
||||
}
|
||||
|
||||
internal bool RemovePageOwner(ulong address, ResourceSlotIdentifier imageIdentifier)
|
||||
{
|
||||
using var held = _lock.Hold();
|
||||
InvalidateLookups();
|
||||
return _pageOwners.Find(address >> ImagePageOwnerTable.PageBits)?.Remove(imageIdentifier) == true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.Libs.Gpu.Buffers;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.Images;
|
||||
|
||||
public sealed partial class GuestImageCache
|
||||
{
|
||||
private const int LookupSlots = 256;
|
||||
|
||||
private static readonly bool LookupMemoEnabled = Environment.GetEnvironmentVariable("SHARPEMU_IMAGE_LOOKUP_MEMO") != "0";
|
||||
|
||||
private struct LookupMemo
|
||||
{
|
||||
public ImageRequest Request;
|
||||
public ImageViewDescription View;
|
||||
public ResourceSlotIdentifier Result;
|
||||
public ulong Generation;
|
||||
public bool ExactFormat;
|
||||
}
|
||||
|
||||
private LookupMemo[]? _lookups;
|
||||
private ulong _lookupGeneration = 1;
|
||||
private long _lookupHits;
|
||||
private long _lookupMisses;
|
||||
private static long _totalLookupHits;
|
||||
private static long _totalLookupMisses;
|
||||
|
||||
public (long Hits, long Misses) LookupMemoCounters => (_lookupHits, _lookupMisses);
|
||||
|
||||
public static string TakeLookupReport() => FormattableString.Invariant(
|
||||
$"[PERF][IMAGE_LOOKUP] memo_hits={Interlocked.Exchange(ref _totalLookupHits, 0)} memo_misses={Interlocked.Exchange(ref _totalLookupMisses, 0)}");
|
||||
|
||||
private void InvalidateLookups() => _lookupGeneration++;
|
||||
|
||||
private static int LookupSlot(in ImageRequest request, bool exactFormat)
|
||||
{
|
||||
var hash = HashCode.Combine(request.Description.Data.Address, request.Description.Data.Size, request.Description.PixelFormat,
|
||||
request.Description.Extent.Width, request.View.Format, request.View.BaseLevel, request.View.BaseLayer,
|
||||
HashCode.Combine(request.Role, exactFormat, request.View.Type, request.Description.Resources));
|
||||
return hash & (LookupSlots - 1);
|
||||
}
|
||||
|
||||
private static bool SameRequest(in ImageRequest left, in ImageRequest right) =>
|
||||
MemoryMarshal.AsBytes(new ReadOnlySpan<ImageRequest>(in left)).SequenceEqual(MemoryMarshal.AsBytes(new ReadOnlySpan<ImageRequest>(in right)));
|
||||
|
||||
private bool TryReuseLookup(ref ImageRequest request, bool exactFormat, out ResourceSlotIdentifier result)
|
||||
{
|
||||
result = ResourceSlotIdentifier.Invalid;
|
||||
if (!LookupMemoEnabled || _lookups is not { } lookups || request.Role == ImageRole.DisplaySurface)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ref var memo = ref lookups[LookupSlot(request, exactFormat)];
|
||||
if (memo.Generation != _lookupGeneration || memo.ExactFormat != exactFormat || !SameRequest(memo.Request, request) ||
|
||||
_slots.TryGet(memo.Result) is not { Registered: true } image)
|
||||
{
|
||||
_lookupMisses++;
|
||||
Interlocked.Increment(ref _totalLookupMisses);
|
||||
return false;
|
||||
}
|
||||
|
||||
_lookupHits++;
|
||||
Interlocked.Increment(ref _totalLookupHits);
|
||||
request.View = memo.View;
|
||||
image.LastAccessTick = _scheduler.CurrentTick;
|
||||
TouchImage(image);
|
||||
result = memo.Result;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void RememberLookup(in ImageRequest original, bool exactFormat, in ImageViewDescription view, ResourceSlotIdentifier result)
|
||||
{
|
||||
if (!LookupMemoEnabled || original.Role == ImageRole.DisplaySurface)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var lookups = _lookups ??= new LookupMemo[LookupSlots];
|
||||
ref var memo = ref lookups[LookupSlot(original, exactFormat)];
|
||||
memo.Request = original;
|
||||
memo.View = view;
|
||||
memo.Result = result;
|
||||
memo.ExactFormat = exactFormat;
|
||||
memo.Generation = _lookupGeneration;
|
||||
}
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ public sealed partial class GuestImageCache
|
|||
}
|
||||
|
||||
image.Registered = true;
|
||||
InvalidateLookups();
|
||||
image.RecencyEntryIndex = _recencyQueue.Insert(imageIdentifier, _collectionTick);
|
||||
_totalUsedMemory += image.AccountedSize;
|
||||
}
|
||||
|
|
@ -67,6 +68,7 @@ public sealed partial class GuestImageCache
|
|||
|
||||
_totalUsedMemory -= accounted;
|
||||
image.Registered = false;
|
||||
InvalidateLookups();
|
||||
}
|
||||
|
||||
// Removes the image and its stencil associations; the slot is freed after the current tick.
|
||||
|
|
|
|||
|
|
@ -124,6 +124,13 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
|
|||
}
|
||||
|
||||
using var held = _lock.Hold();
|
||||
if (TryReuseLookup(ref request, exactFormat, out var reused))
|
||||
{
|
||||
return reused;
|
||||
}
|
||||
|
||||
var original = request;
|
||||
var generation = _lookupGeneration;
|
||||
var result = ResourceSlotIdentifier.Invalid;
|
||||
var candidates = FindImagesInRange(request.Description.Data.Address, request.Description.Data.Size, pageOverlap: false);
|
||||
foreach (var imageIdentifier in candidates)
|
||||
|
|
@ -134,6 +141,7 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
|
|||
}
|
||||
}
|
||||
|
||||
var sameBacking = result.IsValid;
|
||||
var viewMip = -1;
|
||||
var viewLayer = -1;
|
||||
if (!result.IsValid)
|
||||
|
|
@ -220,6 +228,11 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
|
|||
|
||||
image.LastAccessTick = _scheduler.CurrentTick;
|
||||
TouchImage(image);
|
||||
if (sameBacking && generation == _lookupGeneration)
|
||||
{
|
||||
RememberLookup(original, exactFormat, request.View, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -469,6 +469,7 @@ internal static class RenderPhaseProfile
|
|||
BufferUploadProfile.Report();
|
||||
Console.Error.WriteLine(SharpEmu.ShaderCompiler.Resources.ResourceMaterializationCache.TakeReport());
|
||||
Console.Error.WriteLine(SharpEmu.Libs.Gpu.Buffers.GuestBufferCache.TakeAsyncReadbackReport());
|
||||
Console.Error.WriteLine(SharpEmu.Libs.Gpu.Images.GuestImageCache.TakeLookupReport());
|
||||
SharpEmu.ShaderCompiler.Resources.ResourceMaterializationProfile.WriteReport();
|
||||
SharpEmu.Libs.Diagnostics.AgcRegisterPacketProfile.WriteReport();
|
||||
SharpEmu.HLE.GpuMemory.GpuMemoryAccessProfile.WriteReport();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Gpu.Buffers;
|
||||
using SharpEmu.Libs.Gpu.Images;
|
||||
using SharpEmu.Libs.Tests.Gpu.Buffers;
|
||||
using Silk.NET.Vulkan;
|
||||
using Xunit;
|
||||
using static SharpEmu.Libs.Tests.Gpu.Images.ImageCacheTestSupport;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Gpu.Images;
|
||||
|
||||
public sealed partial class GuestImageCacheTests
|
||||
{
|
||||
[Fact]
|
||||
public void FindImage_ReusesASameBackingLookupUntilTheIndexChanges()
|
||||
{
|
||||
if (!GatePrerequisites.Ready(_vulkan)) return;
|
||||
using var harness = new CacheHarness(_vulkan);
|
||||
var address = harness.MapBacked(0x20000, ReadWrite);
|
||||
harness.Write(address, Bytes(0x44332211u));
|
||||
|
||||
var first = Sampled(address);
|
||||
var firstId = harness.Find(ref first);
|
||||
var created = Sampled(address);
|
||||
Assert.Equal(firstId, harness.Find(ref created));
|
||||
var baseline = harness.Images.LookupMemoCounters;
|
||||
|
||||
var repeated = Sampled(address);
|
||||
Assert.Equal(firstId, harness.Find(ref repeated));
|
||||
Assert.Equal(baseline.Hits + 1, harness.Images.LookupMemoCounters.Hits);
|
||||
Assert.Equal(first.View, repeated.View);
|
||||
|
||||
var stale = new ResourceSlotIdentifier(firstId.Index, firstId.Generation + 1);
|
||||
harness.Images.AddPageOwner(address, stale);
|
||||
Assert.True(harness.Images.RemovePageOwner(address, stale));
|
||||
var afterChange = Sampled(address);
|
||||
Assert.Equal(firstId, harness.Find(ref afterChange));
|
||||
Assert.Equal(baseline.Hits + 1, harness.Images.LookupMemoCounters.Hits);
|
||||
|
||||
var reusedAgain = Sampled(address);
|
||||
Assert.Equal(firstId, harness.Find(ref reusedAgain));
|
||||
Assert.Equal(baseline.Hits + 2, harness.Images.LookupMemoCounters.Hits);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindImage_ADifferentViewIsNotReused()
|
||||
{
|
||||
if (!GatePrerequisites.Ready(_vulkan)) return;
|
||||
using var harness = new CacheHarness(_vulkan);
|
||||
var address = harness.MapBacked(0x20000, ReadWrite);
|
||||
harness.Write(address, Bytes(0x44332211u));
|
||||
|
||||
var first = Sampled(address);
|
||||
var firstId = harness.Find(ref first);
|
||||
var again = Sampled(address);
|
||||
harness.Find(ref again);
|
||||
var baseline = harness.Images.LookupMemoCounters;
|
||||
|
||||
var swizzled = Sampled(address);
|
||||
swizzled.View = swizzled.View with { Mapping = new ComponentMapping(ComponentSwizzle.B, ComponentSwizzle.G, ComponentSwizzle.R, ComponentSwizzle.A) };
|
||||
Assert.Equal(firstId, harness.Find(ref swizzled));
|
||||
Assert.Equal(ComponentSwizzle.B, swizzled.View.Mapping.R);
|
||||
Assert.Equal(baseline.Hits, harness.Images.LookupMemoCounters.Hits);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue