[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:
ParantezTech 2026-10-02 18:50:27 +03:00
parent f125d17510
commit 2a42567bcf
6 changed files with 174 additions and 0 deletions

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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.

View File

@ -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;
}

View File

@ -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();

View File

@ -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);
}
}