diff --git a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Collection.cs b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Collection.cs index 89c3250..91eb205 100644 --- a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Collection.cs +++ b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Collection.cs @@ -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; } diff --git a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Lookup.cs b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Lookup.cs new file mode 100644 index 0000000..807c88c --- /dev/null +++ b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Lookup.cs @@ -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(in left)).SequenceEqual(MemoryMarshal.AsBytes(new ReadOnlySpan(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; + } +} diff --git a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Tracking.cs b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Tracking.cs index 20d7798..91a8bc7 100644 --- a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Tracking.cs +++ b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Tracking.cs @@ -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. diff --git a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.cs b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.cs index 2391fc6..ab60dd9 100644 --- a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.cs +++ b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.cs @@ -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; } diff --git a/src/SharpEmu.Libs/VideoOut/RenderPhaseProfile.cs b/src/SharpEmu.Libs/VideoOut/RenderPhaseProfile.cs index 8d0fd84..8821b5d 100644 --- a/src/SharpEmu.Libs/VideoOut/RenderPhaseProfile.cs +++ b/src/SharpEmu.Libs/VideoOut/RenderPhaseProfile.cs @@ -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(); diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Lookup.cs b/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Lookup.cs new file mode 100644 index 0000000..2561134 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Lookup.cs @@ -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); + } +}