From 858ab19b863d8ea60b5395c84a3bdd6e5f5f7b54 Mon Sep 17 00:00:00 2001 From: menvae Date: Mon, 27 Apr 2026 19:47:41 +0300 Subject: [PATCH 1/4] eagerly create device contexts for available devices for VideoDecoder --- osu.Framework/Graphics/Video/FFmpegFuncs.cs | 3 + osu.Framework/Graphics/Video/VideoDecoder.cs | 71 +++++++++++++++----- 2 files changed, 59 insertions(+), 15 deletions(-) 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, From a0517eb07f515fd11cf502fb4c9827ccafe91921 Mon Sep 17 00:00:00 2001 From: menvae Date: Mon, 27 Apr 2026 19:50:39 +0300 Subject: [PATCH 2/4] add CullableFlowContainer --- .../Containers/CullableFlowContainer.cs | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 osu.Framework/Graphics/Containers/CullableFlowContainer.cs 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; + } + } +} From 59fe2b818b4a8284a0f97f62d98774a7d6521266 Mon Sep 17 00:00:00 2001 From: menvae Date: Mon, 27 Apr 2026 19:51:26 +0300 Subject: [PATCH 3/4] add "During" transforms --- .../Graphics/Transforms/DuringTransform.cs | 29 +++++++++++ .../Graphics/Transforms/TransformSequence.cs | 48 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 osu.Framework/Graphics/Transforms/DuringTransform.cs 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. From da0e575f4d69fdd5129c31bf2bca4e1c9fd84469 Mon Sep 17 00:00:00 2001 From: menvae Date: Mon, 25 May 2026 11:46:49 +0300 Subject: [PATCH 4/4] allow not unloading in loadunloadwrappers --- .../Graphics/Containers/DelayedLoadUnloadWrapper.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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;