diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs index 91f5f93905..a30e09cd29 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs @@ -56,13 +56,13 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor [Test] public void TestDefaultSkin() { - AddStep("set default skin", () => skins.CurrentSkinInfo.Value = DefaultSkin.CreateInfo().ToLive()); + AddStep("set default skin", () => skins.CurrentSkinInfo.Value = DefaultSkin.CreateInfo().ToLiveUnmanaged()); } [Test] public void TestLegacySkin() { - AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.CreateInfo().ToLive()); + AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.CreateInfo().ToLiveUnmanaged()); } } } diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs index 1d197791a4..06cb5a3607 100644 --- a/osu.Game.Tests/Database/RealmLiveTests.cs +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -22,9 +22,9 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realmFactory, _) => { - ILive beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive(); + ILive beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive(realmFactory); - ILive beatmap2 = realmFactory.CreateContext().All().First().ToLive(); + ILive beatmap2 = realmFactory.CreateContext().All().First().ToLive(realmFactory); Assert.AreEqual(beatmap, beatmap2); }); @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Database { context.Write(r => r.Add(beatmap)); - liveBeatmap = beatmap.ToLive(); + liveBeatmap = beatmap.ToLive(realmFactory); } using (var migratedStorage = new TemporaryNativeStorage("realm-test-migration-target")) @@ -64,7 +64,7 @@ namespace osu.Game.Tests.Database { var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()); - var liveBeatmap = beatmap.ToLive(); + var liveBeatmap = beatmap.ToLive(realmFactory); using (var context = realmFactory.CreateContext()) context.Write(r => r.Add(beatmap)); @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Database public void TestAccessNonManaged() { var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()); - var liveBeatmap = beatmap.ToLive(); + var liveBeatmap = beatmap.ToLiveUnmanaged(); Assert.IsFalse(beatmap.Hidden); Assert.IsFalse(liveBeatmap.Value.Hidden); @@ -102,7 +102,7 @@ namespace osu.Game.Tests.Database { var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); - liveBeatmap = beatmap.ToLive(); + liveBeatmap = beatmap.ToLive(realmFactory); } }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); @@ -131,7 +131,7 @@ namespace osu.Game.Tests.Database { var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); - liveBeatmap = beatmap.ToLive(); + liveBeatmap = beatmap.ToLive(realmFactory); } }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); @@ -151,7 +151,7 @@ namespace osu.Game.Tests.Database RunTestWithRealm((realmFactory, _) => { var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()); - var liveBeatmap = beatmap.ToLive(); + var liveBeatmap = beatmap.ToLive(realmFactory); Assert.DoesNotThrow(() => { @@ -173,7 +173,7 @@ namespace osu.Game.Tests.Database { var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); - liveBeatmap = beatmap.ToLive(); + liveBeatmap = beatmap.ToLive(realmFactory); } }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); @@ -211,7 +211,7 @@ namespace osu.Game.Tests.Database { var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); - liveBeatmap = beatmap.ToLive(); + liveBeatmap = beatmap.ToLive(realmFactory); } }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); @@ -250,7 +250,7 @@ namespace osu.Game.Tests.Database // not just a refresh from the resolved Live. threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); - liveBeatmap = beatmap.ToLive(); + liveBeatmap = beatmap.ToLive(realmFactory); } }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs index bdd1b92c8d..4762d3cded 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs @@ -123,7 +123,7 @@ namespace osu.Game.Tests.Visual.Background private void setCustomSkin() { // feign a skin switch. this doesn't do anything except force CurrentSkin to become a LegacySkin. - AddStep("set custom skin", () => skins.CurrentSkinInfo.Value = new SkinInfo().ToLive()); + AddStep("set custom skin", () => skins.CurrentSkinInfo.Value = new SkinInfo().ToLiveUnmanaged()); } private void setDefaultSkin() => AddStep("set default skin", () => skins.CurrentSkinInfo.SetDefault()); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index cccc962a3f..c5f56cae9e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("setup skins", () => { - skinManager.CurrentSkinInfo.Value = gameCurrentSkin.ToLive(); + skinManager.CurrentSkinInfo.Value = gameCurrentSkin.ToLiveUnmanaged(); currentBeatmapSkin = getBeatmapSkin(); }); }); diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index c376d5d503..4f7bdf93e4 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -24,14 +24,22 @@ namespace osu.Game.Database /// private readonly T data; + private readonly RealmContextFactory? realmFactory; + /// /// Construct a new instance of live realm data. /// /// The realm data. - public RealmLive(T data) + /// The realm factory the data was sourced from. May be null for an unmanaged object. + public RealmLive(T data, RealmContextFactory? realmFactory) { this.data = data; + if (IsManaged && realmFactory == null) + throw new ArgumentException(@"Realm factory must be provided for a managed instance", nameof(realmFactory)); + + this.realmFactory = realmFactory; + ID = data.ID; } @@ -47,7 +55,10 @@ namespace osu.Game.Database return; } - using (var realm = Realm.GetInstance(data.Realm.Config)) + if (realmFactory == null) + throw new ArgumentException(@"Realm factory must be provided for a managed instance", nameof(realmFactory)); + + using (var realm = realmFactory.CreateContext()) perform(realm.Find(ID)); } @@ -58,12 +69,15 @@ namespace osu.Game.Database public TReturn PerformRead(Func perform) { if (typeof(RealmObjectBase).IsAssignableFrom(typeof(TReturn))) - throw new InvalidOperationException($"Realm live objects should not exit the scope of {nameof(PerformRead)}."); + throw new InvalidOperationException(@$"Realm live objects should not exit the scope of {nameof(PerformRead)}."); if (!IsManaged) return perform(data); - using (var realm = Realm.GetInstance(data.Realm.Config)) + if (realmFactory == null) + throw new ArgumentException(@"Realm factory must be provided for a managed instance", nameof(realmFactory)); + + using (var realm = realmFactory.CreateContext()) return perform(realm.Find(ID)); } @@ -74,7 +88,7 @@ namespace osu.Game.Database public void PerformWrite(Action perform) { if (!IsManaged) - throw new InvalidOperationException("Can't perform writes on a non-managed underlying value"); + throw new InvalidOperationException(@"Can't perform writes on a non-managed underlying value"); PerformRead(t => { @@ -94,11 +108,7 @@ namespace osu.Game.Database if (!ThreadSafety.IsUpdateThread) throw new InvalidOperationException($"Can't use {nameof(Value)} on managed objects from non-update threads"); - // When using Value, we rely on garbage collection for the realm instance used to retrieve the instance. - // As we are sure that this is on the update thread, there should always be an open and constantly refreshing realm instance to ensure file size growth is a non-issue. - var realm = Realm.GetInstance(data.Realm.Config); - - return realm.Find(ID); + return realmFactory!.Context.Find(ID); } } diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index b38e21453c..c546a70fae 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -53,16 +53,28 @@ namespace osu.Game.Database return mapper.Map(item); } - public static List> ToLive(this IEnumerable realmList) + public static List> ToLiveUnmanaged(this IEnumerable realmList) where T : RealmObject, IHasGuidPrimaryKey { - return realmList.Select(l => new RealmLive(l)).Cast>().ToList(); + return realmList.Select(l => new RealmLive(l, null)).Cast>().ToList(); } - public static ILive ToLive(this T realmObject) + public static ILive ToLiveUnmanaged(this T realmObject) where T : RealmObject, IHasGuidPrimaryKey { - return new RealmLive(realmObject); + return new RealmLive(realmObject, null); + } + + public static List> ToLive(this IEnumerable realmList, RealmContextFactory realmContextFactory) + where T : RealmObject, IHasGuidPrimaryKey + { + return realmList.Select(l => new RealmLive(l, realmContextFactory)).Cast>().ToList(); + } + + public static ILive ToLive(this T realmObject, RealmContextFactory realmContextFactory) + where T : RealmObject, IHasGuidPrimaryKey + { + return new RealmLive(realmObject, realmContextFactory); } /// diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index a35191613c..9c379de683 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -255,10 +255,10 @@ namespace osu.Game if (skinInfo == null) { if (guid == SkinInfo.CLASSIC_SKIN) - skinInfo = DefaultLegacySkin.CreateInfo().ToLive(); + skinInfo = DefaultLegacySkin.CreateInfo().ToLiveUnmanaged(); } - SkinManager.CurrentSkinInfo.Value = skinInfo ?? DefaultSkin.CreateInfo().ToLive(); + SkinManager.CurrentSkinInfo.Value = skinInfo ?? DefaultSkin.CreateInfo().ToLiveUnmanaged(); }; configSkin.TriggerChange(); diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index b1582d7bee..0fa6d78d4b 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -32,14 +32,14 @@ namespace osu.Game.Overlays.Settings.Sections Icon = FontAwesome.Solid.PaintBrush }; - private readonly Bindable> dropdownBindable = new Bindable> { Default = DefaultSkin.CreateInfo().ToLive() }; + private readonly Bindable> dropdownBindable = new Bindable> { Default = DefaultSkin.CreateInfo().ToLiveUnmanaged() }; private readonly Bindable configBindable = new Bindable(); private static readonly ILive random_skin_info = new SkinInfo { ID = SkinInfo.RANDOM_SKIN, Name = "", - }.ToLive(); + }.ToLiveUnmanaged(); private List> skinItems; @@ -133,7 +133,7 @@ namespace osu.Game.Overlays.Settings.Sections { int protectedCount = realmSkins.Count(s => s.Protected); - skinItems = realmSkins.ToLive(); + skinItems = realmSkins.ToLive(realmFactory); skinItems.Insert(protectedCount, random_skin_info); diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 54fc2340f1..ee92b6b40a 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -43,7 +43,7 @@ namespace osu.Game.Skinning protected Skin(SkinInfo skin, IStorageResourceProvider resources, [CanBeNull] Stream configurationStream = null) { - SkinInfo = skin.ToLive(); + SkinInfo = skin.ToLive(resources.RealmContextFactory); this.resources = resources; configurationStream ??= getConfigurationStream(); diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 5134632fb1..bb2f0a37b4 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -47,9 +47,9 @@ namespace osu.Game.Skinning public readonly Bindable CurrentSkin = new Bindable(); - public readonly Bindable> CurrentSkinInfo = new Bindable>(Skinning.DefaultSkin.CreateInfo().ToLive()) + public readonly Bindable> CurrentSkinInfo = new Bindable>(Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged()) { - Default = Skinning.DefaultSkin.CreateInfo().ToLive() + Default = Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged() }; private readonly SkinModelManager skinModelManager; @@ -119,13 +119,13 @@ namespace osu.Game.Skinning if (randomChoices.Length == 0) { - CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLive(); + CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged(); return; } var chosen = randomChoices.ElementAt(RNG.Next(0, randomChoices.Length)); - CurrentSkinInfo.Value = chosen.ToLive(); + CurrentSkinInfo.Value = chosen.ToLive(contextFactory); } } @@ -182,7 +182,7 @@ namespace osu.Game.Skinning public ILive Query(Expression> query) { using (var context = contextFactory.CreateContext()) - return context.All().FirstOrDefault(query)?.ToLive(); + return context.All().FirstOrDefault(query)?.ToLive(contextFactory); } public event Action SourceChanged; @@ -237,6 +237,7 @@ namespace osu.Game.Skinning AudioManager IStorageResourceProvider.AudioManager => audio; IResourceStore IStorageResourceProvider.Resources => resources; IResourceStore IStorageResourceProvider.Files => userFiles; + RealmContextFactory IStorageResourceProvider.RealmContextFactory => contextFactory; IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host.CreateTextureLoaderStore(underlyingStore); #endregion @@ -302,7 +303,7 @@ namespace osu.Game.Skinning Guid currentUserSkin = CurrentSkinInfo.Value.ID; if (items.Any(s => s.ID == currentUserSkin)) - scheduler.Add(() => CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLive()); + scheduler.Add(() => CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged()); skinModelManager.Delete(items.ToList(), silent); } diff --git a/osu.Game/Stores/RealmArchiveModelImporter.cs b/osu.Game/Stores/RealmArchiveModelImporter.cs index 1681dad750..4aca079e2e 100644 --- a/osu.Game/Stores/RealmArchiveModelImporter.cs +++ b/osu.Game/Stores/RealmArchiveModelImporter.cs @@ -352,7 +352,7 @@ namespace osu.Game.Stores transaction.Commit(); } - return existing.ToLive(); + return existing.ToLive(ContextFactory); } LogForModel(item, @"Found existing (optimised) but failed pre-check."); @@ -387,7 +387,7 @@ namespace osu.Game.Stores existing.DeletePending = false; transaction.Commit(); - return existing.ToLive(); + return existing.ToLive(ContextFactory); } LogForModel(item, @"Found existing but failed re-use check."); @@ -416,7 +416,7 @@ namespace osu.Game.Stores throw; } - return item.ToLive(); + return item.ToLive(ContextFactory); } }