mirror of
https://github.com/SK-la/osu-framework.git
synced 2026-03-15 03:20:30 +00:00
279 lines
10 KiB
C#
279 lines
10 KiB
C#
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
// See the LICENCE file in the repository root for full licence text.
|
|
|
|
#nullable disable
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using NUnit.Framework;
|
|
using osu.Framework.Allocation;
|
|
using osu.Framework.Graphics;
|
|
using osu.Framework.Graphics.Containers;
|
|
using osu.Framework.Graphics.Sprites;
|
|
using osu.Framework.Graphics.Textures;
|
|
using osu.Framework.IO.Stores;
|
|
using osu.Framework.Platform;
|
|
using osu.Framework.Testing;
|
|
|
|
namespace osu.Framework.Tests.Visual.Sprites
|
|
{
|
|
public class TestSceneTextures : FrameworkTestScene
|
|
{
|
|
private BlockingStoreProvidingContainer spriteContainer;
|
|
|
|
[SetUp]
|
|
public void Setup() => Schedule(() =>
|
|
{
|
|
Child = spriteContainer = new BlockingStoreProvidingContainer { RelativeSizeAxes = Axes.Both };
|
|
});
|
|
|
|
[TearDownSteps]
|
|
public void TearDownSteps()
|
|
{
|
|
AddStep("reset", () => spriteContainer.BlockingOnlineStore.Reset());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that a ref-counted texture is disposed when all references are lost.
|
|
/// </summary>
|
|
[Test]
|
|
public void TestRefCountTextureDisposal()
|
|
{
|
|
Avatar avatar1 = null;
|
|
Avatar avatar2 = null;
|
|
Texture texture = null;
|
|
|
|
AddStep("add disposable sprite", () => avatar1 = addSprite("1"));
|
|
AddStep("add disposable sprite", () => avatar2 = addSprite("1"));
|
|
|
|
AddUntilStep("wait for texture load", () => avatar1.Texture != null && avatar2.Texture != null);
|
|
AddAssert("both textures are RefCount", () => avatar1.Texture is TextureWithRefCount && avatar2.Texture is TextureWithRefCount);
|
|
|
|
AddAssert("textures share gl texture", () => avatar1.Texture.TextureGL == avatar2.Texture.TextureGL);
|
|
AddAssert("textures have different refcount textures", () => avatar1.Texture != avatar2.Texture);
|
|
|
|
AddStep("dispose children", () =>
|
|
{
|
|
texture = avatar1.Texture;
|
|
|
|
Clear();
|
|
avatar1.Dispose();
|
|
avatar2.Dispose();
|
|
});
|
|
|
|
assertAvailability(() => texture, false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests the case where multiple lookups occur for different textures, which shouldn't block each other.
|
|
/// </summary>
|
|
[Test]
|
|
public void TestFetchContentionDifferentLookup()
|
|
{
|
|
Avatar avatar1 = null;
|
|
Avatar avatar2 = null;
|
|
|
|
AddStep("begin blocking load", () => spriteContainer.BlockingOnlineStore.StartBlocking("1"));
|
|
|
|
AddStep("get first", () => avatar1 = addSprite("1"));
|
|
AddUntilStep("wait for first to begin loading", () => spriteContainer.BlockingOnlineStore.TotalInitiatedLookups == 1);
|
|
|
|
AddStep("get second", () => avatar2 = addSprite("2"));
|
|
AddUntilStep("wait for avatar2 load", () => avatar2.Texture != null);
|
|
|
|
AddAssert("avatar1 not loaded", () => avatar1.Texture == null);
|
|
AddAssert("only one lookup occurred", () => spriteContainer.BlockingOnlineStore.TotalCompletedLookups == 1);
|
|
|
|
AddStep("unblock load", () => spriteContainer.BlockingOnlineStore.AllowLoad());
|
|
|
|
AddUntilStep("wait for texture load", () => avatar1.Texture != null);
|
|
AddAssert("two lookups occurred", () => spriteContainer.BlockingOnlineStore.TotalCompletedLookups == 2);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests the case where multiple lookups occur which overlap each other, for the same texture.
|
|
/// </summary>
|
|
[Test]
|
|
public void TestFetchContentionSameLookup()
|
|
{
|
|
Avatar avatar1 = null;
|
|
Avatar avatar2 = null;
|
|
|
|
AddStep("begin blocking load", () => spriteContainer.BlockingOnlineStore.StartBlocking());
|
|
AddStep("get first", () => avatar1 = addSprite("1"));
|
|
AddStep("get second", () => avatar2 = addSprite("1"));
|
|
|
|
AddAssert("neither are loaded", () => avatar1.Texture == null && avatar2.Texture == null);
|
|
|
|
AddStep("unblock load", () => spriteContainer.BlockingOnlineStore.AllowLoad());
|
|
AddUntilStep("wait for texture load", () => avatar1.Texture != null && avatar2.Texture != null);
|
|
|
|
AddAssert("only one lookup occurred", () => spriteContainer.BlockingOnlineStore.TotalInitiatedLookups == 1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that a ref-counted texture gets put in a non-available state when disposed.
|
|
/// </summary>
|
|
[Test]
|
|
public void TestRefCountTextureAvailability()
|
|
{
|
|
Texture texture = null;
|
|
|
|
AddStep("get texture", () => texture = spriteContainer.LargeStore.Get("1"));
|
|
AddStep("dispose texture", () => texture.Dispose());
|
|
|
|
assertAvailability(() => texture, false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that a non-ref-counted texture remains in an available state when disposed.
|
|
/// </summary>
|
|
[Test]
|
|
public void TestTextureAvailability()
|
|
{
|
|
Texture texture = null;
|
|
|
|
AddStep("get texture", () => texture = spriteContainer.NormalStore.Get("1"));
|
|
AddStep("dispose texture", () => texture.Dispose());
|
|
|
|
AddAssert("texture is still available", () => texture.Available);
|
|
}
|
|
|
|
private void assertAvailability(Func<Texture> textureFunc, bool available)
|
|
=> AddAssert($"texture available = {available}", () => ((TextureWithRefCount)textureFunc()).IsDisposed == !available);
|
|
|
|
private Avatar addSprite(string url)
|
|
{
|
|
var avatar = new Avatar(url);
|
|
spriteContainer.Add(new DelayedLoadWrapper(avatar));
|
|
return avatar;
|
|
}
|
|
|
|
[LongRunningLoad]
|
|
private class Avatar : Sprite
|
|
{
|
|
private readonly string url;
|
|
|
|
public Avatar(string url)
|
|
{
|
|
this.url = url;
|
|
}
|
|
|
|
[BackgroundDependencyLoader]
|
|
private void load(LargeTextureStore textures)
|
|
{
|
|
Texture = textures.Get(url);
|
|
}
|
|
}
|
|
|
|
private class BlockingResourceStore : IResourceStore<byte[]>
|
|
{
|
|
/// <summary>
|
|
/// The total number of lookups requested on this store (including blocked lookups).
|
|
/// </summary>
|
|
public int TotalInitiatedLookups { get; private set; }
|
|
|
|
/// <summary>
|
|
/// The total number of completed lookups.
|
|
/// </summary>
|
|
public int TotalCompletedLookups { get; private set; }
|
|
|
|
private readonly IResourceStore<byte[]> baseStore;
|
|
private readonly ManualResetEventSlim resetEvent = new ManualResetEventSlim(true);
|
|
|
|
private string blockingName;
|
|
private bool blocking;
|
|
|
|
public BlockingResourceStore(IResourceStore<byte[]> baseStore)
|
|
{
|
|
this.baseStore = baseStore;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Block load until <see cref="AllowLoad"/> is called.
|
|
/// </summary>
|
|
/// <param name="blockingName">If not <c>null</c> or empty, only lookups for this particular name will be blocked.</param>
|
|
public void StartBlocking(string blockingName = null)
|
|
{
|
|
this.blockingName = blockingName;
|
|
|
|
blocking = true;
|
|
resetEvent.Reset();
|
|
}
|
|
|
|
public void AllowLoad()
|
|
{
|
|
blocking = false;
|
|
resetEvent.Set();
|
|
}
|
|
|
|
public byte[] Get(string name) => getWithBlocking(name, baseStore.Get);
|
|
|
|
public Task<byte[]> GetAsync(string name, CancellationToken cancellationToken = default) =>
|
|
getWithBlocking(name, name1 => baseStore.GetAsync(name1, cancellationToken));
|
|
|
|
public Stream GetStream(string name) => getWithBlocking(name, baseStore.GetStream);
|
|
|
|
private T getWithBlocking<T>(string name, Func<string, T> getFunc)
|
|
{
|
|
TotalInitiatedLookups++;
|
|
|
|
if (blocking && name == blockingName)
|
|
{
|
|
if (!resetEvent.Wait(10000))
|
|
throw new TimeoutException("Load was not allowed in a timely fashion.");
|
|
}
|
|
|
|
TotalCompletedLookups++;
|
|
return getFunc("sample-texture");
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
AllowLoad();
|
|
TotalInitiatedLookups = 0;
|
|
TotalCompletedLookups = 0;
|
|
}
|
|
|
|
public IEnumerable<string> GetAvailableResources() => Enumerable.Empty<string>();
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
|
|
private class BlockingStoreProvidingContainer : Container
|
|
{
|
|
[Cached]
|
|
public TextureStore NormalStore { get; private set; }
|
|
|
|
[Cached]
|
|
public LargeTextureStore LargeStore { get; private set; }
|
|
|
|
public BlockingResourceStore BlockingOnlineStore { get; private set; }
|
|
|
|
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
|
{
|
|
var game = parent.Get<Game>();
|
|
var host = parent.Get<GameHost>();
|
|
|
|
BlockingOnlineStore = new BlockingResourceStore(new NamespacedResourceStore<byte[]>(game.Resources, "Textures"));
|
|
NormalStore = new TextureStore(host.CreateTextureLoaderStore(BlockingOnlineStore));
|
|
LargeStore = new LargeTextureStore(host.CreateTextureLoaderStore(BlockingOnlineStore));
|
|
|
|
return base.CreateChildDependencies(parent);
|
|
}
|
|
|
|
protected override void Dispose(bool isDisposing)
|
|
{
|
|
base.Dispose(isDisposing);
|
|
BlockingOnlineStore?.Reset();
|
|
}
|
|
}
|
|
}
|
|
}
|