diff --git a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Collection.cs b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Collection.cs index c68ce45..9b8e456 100644 --- a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Collection.cs +++ b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.Collection.cs @@ -8,10 +8,16 @@ namespace SharpEmu.Libs.Gpu.Images; // Garbage collection by recency and memory pressure, and the scheduled readback flush. public sealed partial class GuestImageCache { - public void RunGarbageCollector() + public void RunGarbageCollector() => RunGarbageCollector(endsFrame: true); + + // Image ages are counted in frames. Collection also runs after guest submissions, so memory + // pressure is relieved during long stretches without a flip, but only a frame advances the + // age: a title submits dozens of command buffers per frame, and counting those made every + // texture it samples once per frame look stale, so it was deleted and uploaded again. + public void RunGarbageCollector(bool endsFrame) { using var held = _lock.Hold(); - var tick = _collectionTick++; + var tick = endsFrame ? _collectionTick++ : _collectionTick; if (_totalUsedMemory < _collectionStartBytes) { return; diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs index e366a3f..1d74473 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs @@ -305,7 +305,7 @@ internal static unsafe partial class VulkanVideoPresenter CollectCompletedGuestSubmissions(waitForOldest: false); } - public void RunGarbageCollector() => RunGuestCacheCollection(); + public void RunGarbageCollector() => RunGuestCacheCollection(endsFrame: false); public void EmitGlobalBarrier() { @@ -536,7 +536,7 @@ internal static unsafe partial class VulkanVideoPresenter } FlushBatchedGuestCommands(); - RunGuestCacheCollection(); + RunGuestCacheCollection(endsFrame: true); EnsureGuestSubmissionCapacity(); long version; lock (_gate) diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.RenderLoop.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.RenderLoop.cs index fdc9d67..307d6f9 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.RenderLoop.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.RenderLoop.cs @@ -26,13 +26,13 @@ internal static unsafe partial class VulkanVideoPresenter } } - private void RunGuestCacheCollection() + private void RunGuestCacheCollection(bool endsFrame) { ProcessGuestCacheReadbacks(); using (RenderPhaseProfile.Measure(RenderPhaseProfile.Phase.ImageCollect)) { - _imageCache.RunGarbageCollector(); + _imageCache.RunGarbageCollector(endsFrame); } using (RenderPhaseProfile.Measure(RenderPhaseProfile.Phase.BufferCollect)) diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Readback.cs b/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Readback.cs index ca38dc7..5e7baf3 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Readback.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Readback.cs @@ -93,6 +93,38 @@ public sealed partial class GuestImageCacheTests harness.Shutdown(); } + // A title submits many command buffers per frame; collection after each of them must not age + // the textures it samples once a frame, or they are deleted and uploaded again every frame. + [Fact] + public void GarbageCollector_AgesImagesByFrameNotBySubmission() + { + if (!GatePrerequisites.Ready(_vulkan)) return; + using var harness = new CacheHarness(_vulkan); + var address = harness.MapBacked(0x400000, ReadWrite); + var request = Color32(address + 0x330000); + ResourceSlotIdentifier[] images = [harness.Find(ref request)]; + harness.Worker.Run(() => + { + harness.Images.SetCollectionThresholds(0, ulong.MaxValue, ulong.MaxValue, 1); + harness.Images.ResetRecency(images, 1); + for (var submission = 0; submission < 64; submission++) + { + harness.Images.RunGarbageCollector(endsFrame: false); + } + }); + Assert.True(harness.Images.Contains(images[0])); + + harness.Worker.Run(() => + { + for (var frame = 0; frame < 64; frame++) + { + harness.Images.RunGarbageCollector(endsFrame: true); + } + }); + Assert.False(harness.Images.Contains(images[0])); + harness.Shutdown(); + } + [Fact] public void ScheduledReadback_PublishesAfterTheTickAndKeepsTheImage() {