diff --git a/osu.Framework/Graphics/Containers/CullableFlowContainer.cs b/osu.Framework/Graphics/Containers/CullableFlowContainer.cs
new file mode 100644
index 0000000000..a780239585
--- /dev/null
+++ b/osu.Framework/Graphics/Containers/CullableFlowContainer.cs
@@ -0,0 +1,155 @@
+using System;
+using System.Collections.Generic;
+
+namespace osu.Framework.Graphics.Containers
+{
+ ///
+ /// This is vertical only
+ ///
+ ///
+ public partial class CullableFlowContainer : Container where T : Drawable
+ {
+ public ScrollContainer? ScrollContainer { get; set; }
+
+ public float Spacing { get; set; } = 8;
+ public float CullPadding { get; set; } = 250;
+
+ public float ItemSize { get; set; } = 40;
+
+ private readonly List items = new();
+ private readonly HashSet filteredItems = new();
+ public readonly HashSet VisibleItems = new();
+
+ public IEnumerable Items => items;
+
+ public CullableFlowContainer()
+ {
+ // TODO: Maybe make one that supports having a Direction prop in the future
+
+ // do NOT use AutoSizeAxes = Axes.Y
+ // we manually compute the height
+ RelativeSizeAxes = Axes.X;
+ }
+
+ public void Sort(Comparison comparison) => items.Sort(comparison);
+
+ public void SetFiltered(T drawable, bool filtered)
+ {
+ if (filtered)
+ {
+ filteredItems.Add(drawable);
+
+ if (VisibleItems.Contains(drawable))
+ {
+ base.Remove(drawable, false);
+ VisibleItems.Remove(drawable);
+ }
+ }
+ else
+ {
+ filteredItems.Remove(drawable);
+ }
+ }
+
+ public override void Add(T drawable) => items.Add(drawable);
+
+ public new void AddRange(IEnumerable drawables) => items.AddRange(drawables);
+
+ public override bool Remove(T drawable, bool disposeImmediately)
+ {
+ items.Remove(drawable);
+ filteredItems.Remove(drawable);
+
+ if (VisibleItems.Contains(drawable))
+ {
+ VisibleItems.Remove(drawable);
+ return base.Remove(drawable, disposeImmediately);
+ }
+
+ if (disposeImmediately) drawable.Dispose();
+ return true;
+ }
+
+ public override void Clear(bool disposeChildren)
+ {
+ if (disposeChildren)
+ {
+ foreach (var item in items)
+ item.Dispose();
+ }
+
+ items.Clear();
+ filteredItems.Clear();
+ VisibleItems.Clear();
+ base.Clear(false);
+ }
+
+ private ScrollContainer? getScrollContainer()
+ {
+ if (ScrollContainer != null) return ScrollContainer;
+
+ CompositeDrawable parent = Parent;
+
+ while (parent != null)
+ {
+ if (parent is ScrollContainer scroll)
+ return ScrollContainer = scroll;
+
+ parent = parent.Parent;
+ }
+
+ return null;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ var scroll = getScrollContainer();
+ if (scroll == null) return;
+
+ float current = (float)scroll.Current;
+ float scrollHeight = scroll.DrawHeight;
+
+ float pos = 0f;
+
+ for (int i = 0; i < items.Count; i++)
+ {
+ var item = items[i];
+
+ if (filteredItems.Contains(item))
+ continue;
+
+ // ItemSize here is only a fallback
+ float size = item.DrawHeight > 0 ? item.DrawHeight : (item.Height > 0 ? item.Height : ItemSize);
+
+ item.Y = pos;
+ pos += size + Spacing;
+
+ bool inBounds = item.Y + size >= current - CullPadding &&
+ item.Y <= current + scrollHeight + CullPadding;
+
+ if (inBounds)
+ {
+ if (!VisibleItems.Contains(item))
+ {
+ base.Add(item);
+ VisibleItems.Add(item);
+ }
+ }
+ else
+ {
+ if (VisibleItems.Contains(item))
+ {
+ base.Remove(item, false);
+ VisibleItems.Remove(item);
+ }
+ }
+ }
+
+ if (pos > 0) pos -= Spacing;
+
+ Height = pos;
+ }
+ }
+}
diff --git a/osu.Framework/Graphics/Containers/DelayedLoadUnloadWrapper.cs b/osu.Framework/Graphics/Containers/DelayedLoadUnloadWrapper.cs
index 22e5058615..0b1bbc4057 100644
--- a/osu.Framework/Graphics/Containers/DelayedLoadUnloadWrapper.cs
+++ b/osu.Framework/Graphics/Containers/DelayedLoadUnloadWrapper.cs
@@ -14,6 +14,8 @@ namespace osu.Framework.Graphics.Containers
{
public partial class DelayedLoadUnloadWrapper : DelayedLoadWrapper
{
+ public bool AllowUnloading { get; set; } = true;
+
private readonly double timeBeforeUnload;
public DelayedLoadUnloadWrapper(Func createContentFunction, double timeBeforeLoad = 500, double timeBeforeUnload = 1000)
@@ -30,7 +32,7 @@ public DelayedLoadUnloadWrapper(Func createContentFunction, double tim
private ScheduledDelegate unloadSchedule;
- protected bool ShouldUnloadContent => timeBeforeUnload == 0 || timeHidden > timeBeforeUnload;
+ protected bool ShouldUnloadContent => AllowUnloading && (timeBeforeUnload == 0 || timeHidden > timeBeforeUnload);
private ScheduledDelegate scheduledUnloadCheckRegistration;
@@ -104,7 +106,7 @@ private void checkForUnload()
Debug.Assert(Content.LoadState >= LoadState.Ready);
// This code can be expensive, so only run if we haven't yet loaded.
- if (IsIntersecting)
+ if (IsIntersecting || !AllowUnloading)
timeHidden = 0;
else
timeHidden += unloadClock.ElapsedFrameTime;
diff --git a/osu.Framework/Graphics/Transforms/DuringTransform.cs b/osu.Framework/Graphics/Transforms/DuringTransform.cs
new file mode 100644
index 0000000000..035b3b84b0
--- /dev/null
+++ b/osu.Framework/Graphics/Transforms/DuringTransform.cs
@@ -0,0 +1,29 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+
+namespace osu.Framework.Graphics.Transforms
+{
+ internal class DuringTransform : Transform
+ where T : class, ITransformable
+ {
+ private static ulong id;
+
+ private readonly Action action;
+
+ public override string TargetMember { get; } = $"During_{System.Threading.Interlocked.Increment(ref id)}";
+
+ public DuringTransform(Action action)
+ {
+ this.action = action;
+
+ StartValue = false;
+ EndValue = true;
+ }
+
+ protected override void Apply(T d, double time) => action(d);
+
+ protected override void ReadIntoStartValue(T d) { }
+ }
+}
diff --git a/osu.Framework/Graphics/Transforms/TransformSequence.cs b/osu.Framework/Graphics/Transforms/TransformSequence.cs
index d3536e5744..ca2ec2e419 100644
--- a/osu.Framework/Graphics/Transforms/TransformSequence.cs
+++ b/osu.Framework/Graphics/Transforms/TransformSequence.cs
@@ -391,6 +391,54 @@ public TransformSequence Delay(double delay, params Generator[] childGenerato
return Append(childGenerators);
}
+ public TransformSequence During(Action action)
+ {
+ ArgumentNullException.ThrowIfNull(action);
+
+ return During(_ => action());
+ }
+
+ public TransformSequence During(Action action)
+ {
+ ArgumentNullException.ThrowIfNull(action);
+
+ var transform = new DuringTransform(action);
+
+ using (origin.BeginAbsoluteSequence(currentTime, false))
+ {
+ origin.PopulateTransform(transform, true, Math.Max(0, endTime - currentTime), Easing.None);
+
+ origin.AddTransform(transform);
+ }
+
+ Add(transform);
+ return this;
+ }
+
+ public TransformSequence During(double duration, Action action)
+ {
+ ArgumentNullException.ThrowIfNull(action);
+
+ return During(duration, _ => action());
+ }
+
+ public TransformSequence During(double duration, Action action)
+ {
+ ArgumentNullException.ThrowIfNull(action);
+
+ var transform = new DuringTransform(action);
+
+ using (origin.BeginAbsoluteSequence(currentTime, false))
+ {
+ origin.PopulateTransform(transform, true, duration, Easing.None);
+
+ origin.AddTransform(transform);
+ }
+
+ Add(transform);
+ return this;
+ }
+
///
/// Registers a callback which is triggered once all s in this
/// complete successfully.
diff --git a/osu.Framework/Graphics/Video/FFmpegFuncs.cs b/osu.Framework/Graphics/Video/FFmpegFuncs.cs
index ccdc8de6ad..50c12975af 100644
--- a/osu.Framework/Graphics/Video/FFmpegFuncs.cs
+++ b/osu.Framework/Graphics/Video/FFmpegFuncs.cs
@@ -60,6 +60,8 @@ public unsafe class FFmpegFuncs
public delegate AVCodecContext* AvcodecAllocContext3Delegate(AVCodec* codec);
+ public delegate AVBufferRef* AvBufferRefDelegate(AVBufferRef* buf);
+
public delegate void AvcodecFreeContextDelegate(AVCodecContext** avctx);
public delegate int AvcodecParametersToContextDelegate(AVCodecContext* codec, AVCodecParameters* par);
@@ -120,6 +122,7 @@ public unsafe class FFmpegFuncs
public AvCodecIsDecoderDelegate av_codec_is_decoder;
public AvcodecGetHwConfigDelegate avcodec_get_hw_config;
public AvcodecAllocContext3Delegate avcodec_alloc_context3;
+ public AvBufferRefDelegate av_buffer_ref;
public AvcodecFreeContextDelegate avcodec_free_context;
public AvcodecParametersToContextDelegate avcodec_parameters_to_context;
public AvcodecOpen2Delegate avcodec_open2;
diff --git a/osu.Framework/Graphics/Video/VideoDecoder.cs b/osu.Framework/Graphics/Video/VideoDecoder.cs
index 6b8638a805..87c846eca5 100644
--- a/osu.Framework/Graphics/Video/VideoDecoder.cs
+++ b/osu.Framework/Graphics/Video/VideoDecoder.cs
@@ -71,6 +71,8 @@ public unsafe class VideoDecoder : IDisposable
///
public readonly Bindable TargetHardwareVideoDecoders = new Bindable();
+ private static readonly Dictionary hw_device_contexts = new();
+
// libav-context-related
private AVFormatContext* formatContext;
private AVIOContext* ioContext;
@@ -111,23 +113,57 @@ public unsafe class VideoDecoder : IDisposable
static VideoDecoder()
{
- if (RuntimeInfo.OS == RuntimeInfo.Platform.Linux)
+ PreloadLibraries();
+ }
+
+ public static void PreloadLibraries()
+ {
+ string[] libraries = ["avutil", "avcodec", "avformat", "swscale"];
+
+ // FFmpeg.AutoGen doesn't load libraries as RTLD_GLOBAL, so we must load them ourselves to fix inter-library dependencies
+ // otherwise they would fallback to the system-installed libraries that can differ in version installed.
+ foreach (string name in libraries)
{
- void loadVersionedLibraryGlobally(string name)
+ int version = FFmpeg.AutoGen.ffmpeg.LibraryVersionMap[name];
+
+ switch (RuntimeInfo.OS)
{
- int version = FFmpeg.AutoGen.ffmpeg.LibraryVersionMap[name];
- Library.Load($"lib{name}.so.{version}", Library.LoadFlags.RTLD_LAZY | Library.LoadFlags.RTLD_GLOBAL);
+ case RuntimeInfo.Platform.Linux:
+ Library.Load($"lib{name}.so.{version}", Library.LoadFlags.RTLD_LAZY | Library.LoadFlags.RTLD_GLOBAL);
+ break;
+
+ case RuntimeInfo.Platform.macOS:
+ NativeLibrary.Load($"{name}.{version}", RuntimeInfo.EntryAssembly, DllImportSearchPath.UseDllDirectoryForDependencies | DllImportSearchPath.SafeDirectories);
+ break;
+
+ case RuntimeInfo.Platform.Windows:
+ NativeLibrary.Load($"{name}-{version}", RuntimeInfo.EntryAssembly, DllImportSearchPath.UseDllDirectoryForDependencies | DllImportSearchPath.SafeDirectories);
+ break;
}
+ }
+
+ foreach (AVHWDeviceType hwDeviceType in Enum.GetValues())
+ {
+ if (hwDeviceType == AVHWDeviceType.AV_HWDEVICE_TYPE_NONE)
+ continue;
- // FFmpeg.AutoGen doesn't load libraries as RTLD_GLOBAL, so we must load them ourselves to fix inter-library dependencies
- // otherwise they would fallback to the system-installed libraries that can differ in version installed.
- loadVersionedLibraryGlobally("avutil");
- loadVersionedLibraryGlobally("avcodec");
- loadVersionedLibraryGlobally("avformat");
- loadVersionedLibraryGlobally("swscale");
+ AVBufferRef* hwDeviceCtx = null;
+ if (FFmpeg.AutoGen.ffmpeg.av_hwdevice_ctx_create(&hwDeviceCtx, hwDeviceType, null, null, 0) == 0)
+ hw_device_contexts[hwDeviceType] = (IntPtr)hwDeviceCtx;
}
}
+ public static void UnloadLibraries()
+ {
+ foreach (var (_, ptr) in hw_device_contexts)
+ {
+ var ctx = (AVBufferRef*)ptr;
+ FFmpeg.AutoGen.ffmpeg.av_buffer_unref(&ctx);
+ }
+
+ hw_device_contexts.Clear();
+ }
+
///
/// Creates a new video decoder that decodes the given video file.
///
@@ -418,15 +454,19 @@ private void recreateCodecContext()
// initialize hardware decode context.
if (hwDeviceType != AVHWDeviceType.AV_HWDEVICE_TYPE_NONE)
{
- int hwDeviceCreateResult = ffmpeg.av_hwdevice_ctx_create(&codecContext->hw_device_ctx, hwDeviceType, null, null, 0);
-
- if (hwDeviceCreateResult < 0)
+ if (!hw_device_contexts.TryGetValue(hwDeviceType, out var cachedCtx))
{
- Logger.Log($"Couldn't create hardware video decoder context {hwDeviceType} for codec {decoder.Name}: {getErrorMessage(hwDeviceCreateResult)}");
+ Logger.Log($"Couldn't create hardware device context found for {hwDeviceType}, skipping {decoder.Name}");
continue;
}
- Logger.Log($"Successfully opened hardware video decoder context {hwDeviceType} for codec {decoder.Name}");
+ codecContext->hw_device_ctx = ffmpeg.av_buffer_ref((AVBufferRef*)cachedCtx);
+
+ if (codecContext->hw_device_ctx == null)
+ {
+ Logger.Log($"Failed to reference hardware device context for {hwDeviceType}, skipping {decoder.Name}");
+ continue;
+ }
}
int openCodecResult = ffmpeg.avcodec_open2(codecContext, decoder.Pointer, null);
@@ -873,6 +913,7 @@ protected virtual FFmpegFuncs CreateFuncs()
av_codec_is_decoder = FFmpeg.AutoGen.ffmpeg.av_codec_is_decoder,
avcodec_get_hw_config = FFmpeg.AutoGen.ffmpeg.avcodec_get_hw_config,
avcodec_alloc_context3 = FFmpeg.AutoGen.ffmpeg.avcodec_alloc_context3,
+ av_buffer_ref = FFmpeg.AutoGen.ffmpeg.av_buffer_ref,
avcodec_free_context = FFmpeg.AutoGen.ffmpeg.avcodec_free_context,
avcodec_parameters_to_context = FFmpeg.AutoGen.ffmpeg.avcodec_parameters_to_context,
avcodec_open2 = FFmpeg.AutoGen.ffmpeg.avcodec_open2,