[Gpu] Age cached images by frame, not by guest submission
The image collector runs after every completed guest submission as well as at each flip, and every run advanced the age it compares against. A title submits dozens of command buffers per frame, so "unused for 16 ticks" meant a fraction of a frame: every texture sampled once a frame was deleted and recreated, and Superliminal uploaded 3,591 textures (2.2 GB) every 5 seconds. Deleting a GPU-written image the cache cannot read back also dropped its contents. Only a flip now advances the age. Collection after a submission still runs, so memory pressure is relieved during long stretches without a flip. Superliminal's per-frame uploads went from ~55 to none, and its intro scene from 12 to 45 fps.
This commit is contained in:
parent
0675cfa892
commit
ca76f7c8c8
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue