From d4639ba8b2697ae30c633a67c6840a91bc2ff1c8 Mon Sep 17 00:00:00 2001 From: indigo-san Date: Tue, 23 Jan 2024 20:51:01 +0900 Subject: [PATCH 01/25] =?UTF-8?q?=E5=86=8D=E7=94=9F=E6=99=82=E3=81=AB?= =?UTF-8?q?=E3=83=90=E3=83=83=E3=83=95=E3=82=A1=E3=82=92=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Beutl/ViewModels/BufferStatusViewModel.cs | 40 +++ src/Beutl/ViewModels/EditViewModel.cs | 5 + src/Beutl/ViewModels/PlayerViewModel.cs | 305 +++++++++++++----- src/Beutl/Views/Timeline.axaml | 2 + src/Beutl/Views/TimelineOverlay.cs | 2 + src/Beutl/Views/TimelineScale.cs | 35 +- 6 files changed, 311 insertions(+), 78 deletions(-) create mode 100644 src/Beutl/ViewModels/BufferStatusViewModel.cs diff --git a/src/Beutl/ViewModels/BufferStatusViewModel.cs b/src/Beutl/ViewModels/BufferStatusViewModel.cs new file mode 100644 index 000000000..ba19dbb8c --- /dev/null +++ b/src/Beutl/ViewModels/BufferStatusViewModel.cs @@ -0,0 +1,40 @@ +using Reactive.Bindings; + +namespace Beutl.ViewModels; + +public sealed class BufferStatusViewModel : IDisposable +{ + private readonly CompositeDisposable _disposables = []; + private readonly EditViewModel _editViewModel; + + public BufferStatusViewModel(EditViewModel editViewModel) + { + _editViewModel = editViewModel; + + Start = StartTime.CombineLatest(editViewModel.Scale) + .Select(v => v.First.ToPixel(v.Second)) + .ToReadOnlyReactivePropertySlim() + .DisposeWith(_disposables); + + IObservable length = EndTime.CombineLatest(StartTime) + .Select(v => v.First - v.Second); + + Width = length.CombineLatest(editViewModel.Scale) + .Select(v => Math.Max(0, v.First.ToPixel(v.Second))) + .ToReadOnlyReactivePropertySlim() + .DisposeWith(_disposables); + } + + public ReactivePropertySlim StartTime { get; } = new(); + + public ReactivePropertySlim EndTime { get; } = new(); + + public ReadOnlyReactivePropertySlim Start { get; } + + public ReadOnlyReactivePropertySlim Width { get; } + + public void Dispose() + { + _disposables.Dispose(); + } +} diff --git a/src/Beutl/ViewModels/EditViewModel.cs b/src/Beutl/ViewModels/EditViewModel.cs index 21ae24b32..c1d3c54af 100644 --- a/src/Beutl/ViewModels/EditViewModel.cs +++ b/src/Beutl/ViewModels/EditViewModel.cs @@ -69,6 +69,8 @@ public EditViewModel(Scene scene) Scale = Options.Select(o => o.Scale); Offset = Options.Select(o => o.Offset); + BufferStatus = new BufferStatusViewModel(this) + .DisposeWith(_disposables); RestoreState(); @@ -145,6 +147,8 @@ private void OnSelectedObjectDetachedFromHierarchy(object? sender, HierarchyAtta public PlayerViewModel Player { get; private set; } + public BufferStatusViewModel BufferStatus { get; private set; } + public CommandRecorder CommandRecorder { get; private set; } public EditorExtension Extension => SceneEditorExtension.Instance; @@ -171,6 +175,7 @@ public void Dispose() IsEnabled.Dispose(); Library = null!; Player = null!; + BufferStatus = null!; foreach (ToolTabViewModel item in BottomTabItems.GetMarshal().Value) { diff --git a/src/Beutl/ViewModels/PlayerViewModel.cs b/src/Beutl/ViewModels/PlayerViewModel.cs index f01a2991b..c2987f714 100644 --- a/src/Beutl/ViewModels/PlayerViewModel.cs +++ b/src/Beutl/ViewModels/PlayerViewModel.cs @@ -1,4 +1,6 @@ -using Avalonia.Media.Imaging; +using System.Collections.Concurrent; + +using Avalonia.Media.Imaging; using Avalonia.Platform; using Beutl.Audio.Platforms.OpenAL; @@ -28,6 +30,155 @@ namespace Beutl.ViewModels; +interface IPlayer : IDisposable +{ + public record struct Frame(Bitmap Bitmap, TimeSpan Time); + + void Start(); + + bool TryDequeue(out Frame frame); +} + +class BufferedPlayer : IPlayer +{ + private readonly ILogger _logger = Log.CreateLogger(); + private readonly ConcurrentQueue _queue = new(); + private readonly EditViewModel _editViewModel; + private readonly Scene _scene; + private readonly IReadOnlyReactiveProperty _isPlaying; + private readonly TaskCompletionSource _tsc; + private readonly int _rate; + private volatile CancellationTokenSource? _waitRenderToken; + private volatile TaskCompletionSource? _waitTimerTcs; + private readonly IDisposable _disposable; + private TimeSpan? _requestedFrame; + private bool _isDisposed; + + public BufferedPlayer( + EditViewModel editViewModel, + Scene scene, IReactiveProperty isPlaying, + TaskCompletionSource tsc, int rate) + { + _editViewModel = editViewModel; + _scene = scene; + _isPlaying = isPlaying; + _tsc = tsc; + _rate = rate; + + _disposable = isPlaying.Where(v => !v).Subscribe(_ => + { + _waitRenderToken?.Cancel(); + _waitTimerTcs?.TrySetResult(); + }); + } + + public void Start() + { + TimeSpan start = _scene.CurrentFrame; + TimeSpan tick = TimeSpan.FromSeconds(1d / _rate); + TimeSpan duration = _scene.Duration; + + RenderThread.Dispatcher.Dispatch(async () => + { + try + { + for (TimeSpan time = start; time < duration; time += tick) + { + if (!_isPlaying.Value) + break; + + time = time.RoundToRate(_rate); + + if (_queue.Count >= 120) + { + Debug.WriteLine("wait timer"); + await WaitTimer(); + } + + if (!_isPlaying.Value) + break; + + if (_scene.Renderer.Render(time)) + { + Debug.WriteLine($"{time} rendered."); + _queue.Enqueue(new(_scene.Renderer.Snapshot(), time)); + _waitRenderToken?.Cancel(); + + _editViewModel.BufferStatus.EndTime.Value = time; + } + + TimeSpan? requestedFrame = _requestedFrame; + if (requestedFrame.HasValue) + { + time = requestedFrame.Value + (tick * 2); + _requestedFrame = null; + } + } + } + catch (Exception ex) + { + NotificationService.ShowError(Message.AnUnexpectedErrorHasOccurred, Message.An_exception_occurred_while_drawing_frame); + _logger.LogError(ex, "An exception occurred while drawing the frame."); + } + finally + { + _tsc.TrySetResult(); + } + }); + } + + public bool TryDequeue(out IPlayer.Frame frame) + { + _waitTimerTcs?.TrySetResult(); + if (_queue.TryDequeue(out IPlayer.Frame f)) + { + frame = f; + return true; + } + + Debug.WriteLine("wait rendered"); + WaitRender(); + + return _queue.TryDequeue(out frame); + } + + private void WaitRender() + { + if (_isDisposed) return; + _waitRenderToken = new CancellationTokenSource(); + + _waitRenderToken.Token.WaitHandle.WaitOne(TimeSpan.FromSeconds(1d / _rate)); + _waitRenderToken = null; + } + + private async ValueTask WaitTimer() + { + if (_isDisposed) return; + _waitTimerTcs = new TaskCompletionSource(); + + await _waitTimerTcs.Task; + _waitTimerTcs = null; + } + + public void Skipped(TimeSpan requested) + { + Debug.WriteLine($"{requested} skipped"); + _requestedFrame = requested; + } + + public void Dispose() + { + _isDisposed = true; + _waitRenderToken?.Cancel(); + _waitTimerTcs?.TrySetResult(); + _disposable.Dispose(); + while (_queue.TryDequeue(out var f)) + { + f.Bitmap.Dispose(); + } + } +} + public sealed class PlayerViewModel : IDisposable { private static readonly TimeSpan s_second = TimeSpan.FromSeconds(1); @@ -36,7 +187,6 @@ public sealed class PlayerViewModel : IDisposable private readonly ReactivePropertySlim _isEnabled; private readonly EditViewModel _editViewModel; private CancellationTokenSource? _cts; - private bool _playingAndRendering; public PlayerViewModel(EditViewModel editViewModel) { @@ -154,57 +304,92 @@ public PlayerViewModel(EditViewModel editViewModel) public Rect LastSelectedRect { get; set; } - public async void Play() + public void Play() { - if (!_isEnabled.Value || Scene == null) - return; + Task.Run(async () => + { + if (!_isEnabled.Value || Scene == null) + return; - IRenderer renderer = Scene.Renderer; - renderer.RenderInvalidated -= Renderer_RenderInvalidated; + IRenderer renderer = Scene.Renderer; + renderer.RenderInvalidated -= Renderer_RenderInvalidated; - try - { - IsPlaying.Value = true; - int rate = GetFrameRate(); + try + { + IsPlaying.Value = true; + int rate = GetFrameRate(); - PlayAudio(Scene); + TimeSpan tick = TimeSpan.FromSeconds(1d / rate); + TimeSpan startFrame = Scene.CurrentFrame; + TimeSpan duration = Scene.Duration; + var tcs = new TaskCompletionSource(); + _editViewModel.BufferStatus.StartTime.Value = startFrame; + _editViewModel.BufferStatus.EndTime.Value = startFrame; - TimeSpan tick = TimeSpan.FromSeconds(1d / rate); - TimeSpan startFrame = Scene.CurrentFrame; - DateTime startTime = DateTime.Now; - TimeSpan duration = Scene.Duration; - var tcs = new TaskCompletionSource(); - using var timer = new System.Timers.Timer(tick); + using var playerImpl = new BufferedPlayer(_editViewModel, Scene, IsPlaying, tcs, rate); + playerImpl.Start(); - timer.Elapsed += (_, e) => - { - TimeSpan time = (e.SignalTime - startTime) + startFrame; - time = time.RoundToRate(rate); + PlayAudio(Scene); - if (time >= duration || !IsPlaying.Value) + using var timer = new System.Timers.Timer(tick); + bool timerProcessing = false; + timer.Elapsed += (_, e) => { - timer.Stop(); - tcs.SetResult(); - } - else - { - Render(renderer, time); - } - }; - timer.Start(); + startFrame += tick; + TimeSpan time = startFrame; + time = time.RoundToRate(rate); - await tcs.Task; - IsPlaying.Value = false; - } - catch (Exception ex) - { - // 本来ここには例外が来ないはず - _logger.LogError(ex, "An exception occurred during the playback process."); - } - finally - { - renderer.RenderInvalidated += Renderer_RenderInvalidated; - } + if (time >= duration || !IsPlaying.Value) + { + timer.Stop(); + tcs.TrySetResult(); + return; + } + + if (timerProcessing) + { + playerImpl.Skipped(time); + return; + } + + try + { + timerProcessing = true; + + if (playerImpl.TryDequeue(out IPlayer.Frame frame)) + { + using (frame.Bitmap) + { + UpdateImage(frame.Bitmap); + + if (Scene != null) + { + Scene.CurrentFrame = frame.Time; + _editViewModel.BufferStatus.StartTime.Value = frame.Time; + } + } + } + } + finally + { + timerProcessing = false; + } + }; + timer.Start(); + + await tcs.Task; + IsPlaying.Value = false; + } + catch (Exception ex) + { + // 本来ここには例外が来ないはず + _logger.LogError(ex, "An exception occurred during the playback process."); + } + finally + { + renderer.RenderInvalidated += Renderer_RenderInvalidated; + } + }); } private int GetFrameRate() @@ -390,38 +575,6 @@ public void Pause() IsPlaying.Value = false; } - private void Render(IRenderer renderer, TimeSpan timeSpan) - { - if (_playingAndRendering) - return; - _playingAndRendering = true; - - RenderThread.Dispatcher.Dispatch(() => - { - try - { - if (IsPlaying.Value && renderer.Render(timeSpan)) - { - using Bitmap bitmap = renderer.Snapshot(); - UpdateImage(bitmap); - - if (Scene != null) - Scene.CurrentFrame = timeSpan; - } - } - catch (Exception ex) - { - NotificationService.ShowError(Message.AnUnexpectedErrorHasOccurred, Message.An_exception_occurred_while_drawing_frame); - _logger.LogError(ex, "An exception occurred while drawing the frame."); - IsPlaying.Value = false; - } - finally - { - _playingAndRendering = false; - } - }); - } - private unsafe void UpdateImage(Bitmap source) { WriteableBitmap bitmap; diff --git a/src/Beutl/Views/Timeline.axaml b/src/Beutl/Views/Timeline.axaml index 14c605f91..2db407a83 100644 --- a/src/Beutl/Views/Timeline.axaml +++ b/src/Beutl/Views/Timeline.axaml @@ -28,6 +28,8 @@ Height="32" HorizontalAlignment="Stretch" VerticalAlignment="Top" + BufferLength="{Binding EditorContext.BufferStatus.Width.Value}" + BufferStart="{Binding EditorContext.BufferStatus.Start.Value}" EndingBarMargin="{CompiledBinding EndingBarMargin.Value}" PointerExited="TimelinePanel_PointerExited" PointerMoved="TimelinePanel_PointerMoved" diff --git a/src/Beutl/Views/TimelineOverlay.cs b/src/Beutl/Views/TimelineOverlay.cs index 6a7cefd89..d5257e526 100644 --- a/src/Beutl/Views/TimelineOverlay.cs +++ b/src/Beutl/Views/TimelineOverlay.cs @@ -11,6 +11,8 @@ public static class TimelineSharedObject public static readonly IPen BluePen; public static readonly IPen SelectionPen; public static readonly IBrush SelectionFillBrush = new ImmutableSolidColorBrush(Colors.CornflowerBlue, 0.3); + public static readonly IBrush BufferRangeFillBrush = new ImmutableSolidColorBrush(Colors.SkyBlue); + public static readonly IBrush DropFrameFillBrush = new ImmutableSolidColorBrush(Colors.Orange); static TimelineSharedObject() { diff --git a/src/Beutl/Views/TimelineScale.cs b/src/Beutl/Views/TimelineScale.cs index 3deeb8dc3..b034a53c2 100644 --- a/src/Beutl/Views/TimelineScale.cs +++ b/src/Beutl/Views/TimelineScale.cs @@ -1,8 +1,11 @@ -using Avalonia; +using System.Collections.Immutable; + +using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Media.TextFormatting; +using Avalonia.Threading; namespace Beutl.Views; @@ -26,6 +29,12 @@ public static readonly DirectProperty SeekBarMarginPro = AvaloniaProperty.RegisterDirect( nameof(SeekBarMargin), o => o.SeekBarMargin, (o, v) => o.SeekBarMargin = v); + public static readonly StyledProperty BufferStartProperty + = AvaloniaProperty.Register(nameof(BufferStart)); + + public static readonly StyledProperty BufferLengthProperty + = AvaloniaProperty.Register(nameof(BufferLength)); + private static readonly Typeface s_typeface = new(FontFamily.Default, FontStyle.Normal, FontWeight.Medium); private readonly Pen _pen; private IBrush _brush = Brushes.White; @@ -37,7 +46,13 @@ public static readonly DirectProperty SeekBarMarginPro static TimelineScale() { - AffectsRender(ScaleProperty, OffsetProperty, EndingBarMarginProperty, SeekBarMarginProperty); + AffectsRender( + ScaleProperty, + OffsetProperty, + EndingBarMarginProperty, + SeekBarMarginProperty, + BufferStartProperty, + BufferLengthProperty); } public TimelineScale() @@ -70,6 +85,18 @@ public Thickness SeekBarMargin set => SetAndRaise(SeekBarMarginProperty, ref _seekBarMargin, value); } + public double BufferStart + { + get => GetValue(BufferStartProperty); + set => SetValue(BufferStartProperty, value); + } + + public double BufferLength + { + get => GetValue(BufferLengthProperty); + set => SetValue(BufferLengthProperty, value); + } + protected override void OnLoaded(RoutedEventArgs e) { base.OnLoaded(e); @@ -139,6 +166,10 @@ public override void Render(DrawingContext context) } } + context.DrawRectangle( + TimelineSharedObject.BufferRangeFillBrush, null, + new RoundedRect(new Rect(BufferStart, Height - 4, BufferLength, 4))); + var size = new Size(1.25, height); var seekbar = new Point(_seekBarMargin.Left, 0); var endingbar = new Point(_endingBarMargin.Left, 0); From 8ab6cecb3aa8b8b8777622f5ea42bb1665352176 Mon Sep 17 00:00:00 2001 From: indigo-san Date: Sun, 28 Jan 2024 00:48:58 +0900 Subject: [PATCH 02/25] =?UTF-8?q?=E3=83=95=E3=83=AC=E3=83=BC=E3=83=A0?= =?UTF-8?q?=E3=81=94=E3=81=A8=E3=82=AD=E3=83=A3=E3=83=83=E3=82=B7=E3=83=A5?= =?UTF-8?q?=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E3=81=97?= =?UTF-8?q?=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Beutl/Models/BufferedPlayer.cs | 174 ++++++++++++ src/Beutl/Models/FrameCacheManager.cs | 256 ++++++++++++++++++ src/Beutl/Models/IPlayer.cs | 14 + src/Beutl/ViewModels/BufferStatusViewModel.cs | 38 ++- src/Beutl/ViewModels/EditViewModel.cs | 19 ++ src/Beutl/ViewModels/ElementViewModel.cs | 8 + src/Beutl/ViewModels/PlayerViewModel.cs | 235 +++++----------- src/Beutl/Views/Timeline.axaml | 3 +- src/Beutl/Views/TimelineOverlay.cs | 1 + src/Beutl/Views/TimelineScale.cs | 42 ++- 10 files changed, 599 insertions(+), 191 deletions(-) create mode 100644 src/Beutl/Models/BufferedPlayer.cs create mode 100644 src/Beutl/Models/FrameCacheManager.cs create mode 100644 src/Beutl/Models/IPlayer.cs diff --git a/src/Beutl/Models/BufferedPlayer.cs b/src/Beutl/Models/BufferedPlayer.cs new file mode 100644 index 000000000..62b9ce2db --- /dev/null +++ b/src/Beutl/Models/BufferedPlayer.cs @@ -0,0 +1,174 @@ +using System.Collections.Concurrent; +using Beutl.Logging; +using Beutl.Media; +using Beutl.Media.Pixel; +using Beutl.Media.Source; +using Beutl.ProjectSystem; +using Beutl.Rendering; +using Beutl.Services; +using Beutl.ViewModels; +using Microsoft.Extensions.Logging; + +using Reactive.Bindings; + +namespace Beutl.Models; + +public class BufferedPlayer : IPlayer +{ + private readonly ILogger _logger = Log.CreateLogger(); + private readonly ConcurrentQueue _queue = new(); + private readonly EditViewModel _editViewModel; + private readonly FrameCacheManager _frameCacheManager; + private readonly Scene _scene; + private readonly IReadOnlyReactiveProperty _isPlaying; + private readonly TaskCompletionSource _tsc; + private readonly int _rate; + private volatile CancellationTokenSource? _waitRenderToken; + private volatile TaskCompletionSource? _waitTimerTcs; + private readonly IDisposable _disposable; + private int? _requestedFrame; + private bool _isDisposed; + + public BufferedPlayer( + EditViewModel editViewModel, + Scene scene, IReactiveProperty isPlaying, + TaskCompletionSource tsc, int rate) + { + _editViewModel = editViewModel; + _frameCacheManager = editViewModel.FrameCacheManager; + _scene = scene; + _isPlaying = isPlaying; + _tsc = tsc; + _rate = rate; + + _disposable = isPlaying.Where(v => !v).Subscribe(_ => + { + _waitRenderToken?.Cancel(); + _waitTimerTcs?.TrySetResult(); + }); + } + + public void Start() + { + int startFrame = (int)_scene.CurrentFrame.ToFrameNumber(_rate); + int durationFrame = (int)Math.Ceiling(_scene.Duration.ToFrameNumber(_rate)); + + RenderThread.Dispatcher.Dispatch(async () => + { + try + { + for (int frame = startFrame; frame < durationFrame; frame++) + { + if (!_isPlaying.Value) + break; + + if (_queue.Count >= 120) + { + Debug.WriteLine("wait timer"); + await WaitTimer(); + } + + if (!_isPlaying.Value) + break; + + TimeSpan time = TimeSpanExtensions.ToTimeSpan(frame, _rate); + + // キャッシュを探す + // cacheは参照を既に追加されている + if (_frameCacheManager.TryGet(frame, out Ref>? cache)) + { + _queue.Enqueue(new(cache, frame)); + } + else + { + if (_scene.Renderer.Render(time)) + { + Debug.WriteLine($"{frame} rendered."); + using (Ref> bitmap = Ref>.Create(_scene.Renderer.Snapshot())) + { + _queue.Enqueue(new(bitmap.Clone(), frame)); + _frameCacheManager.Add(frame, bitmap); + } + } + else + { + var blank = new Bitmap(_scene.Width, _scene.Height); + _queue.Enqueue(new(Ref>.Create(blank), frame)); + } + } + + _waitRenderToken?.Cancel(); + if (_isPlaying.Value) + _editViewModel.BufferStatus.EndTime.Value = time; + + int? requestedFrame = _requestedFrame; + if (requestedFrame.HasValue && requestedFrame.Value > frame) + { + frame = requestedFrame.Value + 2; + _requestedFrame = null; + } + } + } + catch (Exception ex) + { + NotificationService.ShowError(Message.AnUnexpectedErrorHasOccurred, Message.An_exception_occurred_while_drawing_frame); + _logger.LogError(ex, "An exception occurred while drawing the frame."); + } + finally + { + _tsc.TrySetResult(); + } + }); + } + + public bool TryDequeue(out IPlayer.Frame frame) + { + _waitTimerTcs?.TrySetResult(); + if (_queue.TryDequeue(out IPlayer.Frame f)) + { + frame = f; + return true; + } + + Debug.WriteLine("wait rendered"); + WaitRender(); + + return _queue.TryDequeue(out frame); + } + + private void WaitRender() + { + if (_isDisposed) return; + _waitRenderToken = new CancellationTokenSource(); + + _waitRenderToken.Token.WaitHandle.WaitOne(TimeSpan.FromSeconds(1d / _rate)); + _waitRenderToken = null; + } + + private async ValueTask WaitTimer() + { + if (_isDisposed) return; + _waitTimerTcs = new TaskCompletionSource(); + + await _waitTimerTcs.Task; + _waitTimerTcs = null; + } + + public void Skipped(int requestedFrame) + { + Debug.WriteLine($"{requestedFrame} skipped"); + _requestedFrame = requestedFrame; + } + + public void Dispose() + { + _isDisposed = true; + _waitRenderToken?.Cancel(); + _waitTimerTcs?.TrySetResult(); + _disposable.Dispose(); + while (_queue.TryDequeue(out var f)) + { + f.Bitmap.Dispose(); + } + } +} diff --git a/src/Beutl/Models/FrameCacheManager.cs b/src/Beutl/Models/FrameCacheManager.cs new file mode 100644 index 000000000..191b81fae --- /dev/null +++ b/src/Beutl/Models/FrameCacheManager.cs @@ -0,0 +1,256 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using Beutl.Graphics; +using Beutl.Media; +using Beutl.Media.Pixel; +using Beutl.Media.Source; + +namespace Beutl.Models; + +// not thread safe +public sealed class FrameCacheManager : IDisposable +{ + private readonly SortedDictionary _entries = []; + private readonly object _lock = new(); + private readonly PixelSize _frameSize; + private ulong _size; + private const ulong MaxSize = 1024 * 1024 * 1024; + + public event Action? Added; + public event Action? Removed; + public event Action>? BlocksUpdated; + + public FrameCacheManager(PixelSize frameSize) + { + _frameSize = frameSize; + } + + public ImmutableArray<(int Start, int Length)> Blocks { get; private set; } + + public void Add(int frame, Ref> bitmap) + { + lock (_lock) + { + if (_entries.TryGetValue(frame, out CacheEntry? old)) + { + old.SetBitmap(bitmap); + } + else + { + _size += (uint)bitmap.Value.ByteCount; + _entries.Add(frame, new CacheEntry(bitmap)); + Added?.Invoke(frame); + } + } + + if (_size >= MaxSize) + { + Task.Run(Optimize); + } + } + + public bool TryGet(int frame, [MaybeNullWhen(false)] out Ref> bitmap) + { + lock (_lock) + { + if (_entries.TryGetValue(frame, out CacheEntry? e)) + { + bitmap = e.GetBitmap(); + return true; + } + else + { + bitmap = null; + return false; + } + } + } + + public bool RemoveRange(int start, int end) + { + lock (_lock) + { + int[] keys = _entries.Select(p => p.Key) + .SkipWhile(t => t < start) + .TakeWhile(t => t < end) + .ToArray(); + + foreach (int key in keys) + { + if (_entries.Remove(key, out CacheEntry? e)) + { + _size -= (uint)e.ByteCount; + e.Dispose(); + } + } + + if (keys.Length > 0) + Removed?.Invoke(keys); + + return keys.Length > 0; + } + } + + public void Dispose() + { + lock (_lock) + { + int[] keys = [.. _entries.Keys]; + foreach (CacheEntry item in _entries.Values) + { + item.Dispose(); + } + + _size = 0; + _entries.Clear(); + Removed?.Invoke(keys); + } + } + + // |oxxxxoooxoxxxo| + // GetBlock() -> [(1, 4), (8, 1), (10, 4)] + public ImmutableArray<(int Start, int Length)> CalculateBlocks() + { + lock (_lock) + { + var list = new List<(int Start, int Length)>(); + int start = -1; + int expect = 0; + int count = 0; + foreach (int key in _entries.Keys) + { + if (start == -1) + { + start = key; + expect = key; + } + + if (expect == key) + { + count++; + expect = key + 1; + } + else + { + list.Add((start, count)); + start = -1; + count = 0; + + start = key; + expect = key + 1; + count++; + } + } + + if (start != -1) + { + list.Add((start, count)); + } + + return [.. list]; + } + } + + public void UpdateBlocks() + { + lock (_lock) + { + Blocks = CalculateBlocks(); + BlocksUpdated?.Invoke(Blocks); + } + } + + private void Optimize() + { + lock (_lock) + { + if (_size >= MaxSize) + { + ulong excess = _size - MaxSize; + int sizePerCache = _frameSize.Width * _frameSize.Height * 4; + var targetCount = excess / (ulong)sizePerCache; + + var items = _entries + .OrderBy(v => v.Value.LastAccessTime) + .Take((int)targetCount) + .ToArray(); + foreach (KeyValuePair item in items) + { + if (_size < MaxSize) + break; + + item.Value.Dispose(); + _size -= (uint)item.Value.ByteCount; + _entries.Remove(item.Key); + } + + Removed?.Invoke(items.Select(i => i.Key).ToArray()); + } + } + } + + private class CacheEntry : IDisposable + { + private readonly int _width; + private readonly int _height; + private Ref> _bitmap; + + public CacheEntry(Ref> bitmap) + { + _bitmap = bitmap.Clone(); + _width = _bitmap.Value.Width; + _height = _bitmap.Value.Height; + ByteCount = bitmap.Value.ByteCount; + LastAccessTime = DateTime.UtcNow; + } + + public DateTime LastAccessTime { get; private set; } + + public int ByteCount { get; } + + public void SetBitmap(Ref> bitmap) + { + _bitmap?.Dispose(); + _bitmap = bitmap.Clone(); + LastAccessTime = DateTime.UtcNow; + } + + public Ref> GetBitmap() + { + LastAccessTime = DateTime.UtcNow; + return _bitmap.Clone(); + } + + public void Dispose() + { + _bitmap?.Dispose(); + _bitmap = null!; + } + } + + private sealed class KeyValuePairComparer(IComparer? keyComparer) : Comparer> + { + private readonly IComparer _keyComparer = keyComparer ?? Comparer.Default; + + public override int Compare(KeyValuePair x, KeyValuePair y) + { + return _keyComparer.Compare(x.Key, y.Key); + } + + public override bool Equals(object? obj) + { + if (obj is KeyValuePairComparer other) + { + return _keyComparer == other._keyComparer || _keyComparer.Equals(other._keyComparer); + } + + return false; + } + + public override int GetHashCode() + { + return _keyComparer.GetHashCode(); + } + } +} diff --git a/src/Beutl/Models/IPlayer.cs b/src/Beutl/Models/IPlayer.cs new file mode 100644 index 000000000..b029667e8 --- /dev/null +++ b/src/Beutl/Models/IPlayer.cs @@ -0,0 +1,14 @@ +using Beutl.Media; +using Beutl.Media.Pixel; +using Beutl.Media.Source; + +namespace Beutl.Models; + +public interface IPlayer : IDisposable +{ + public record struct Frame(Ref> Bitmap, int Time); + + void Start(); + + bool TryDequeue(out Frame frame); +} diff --git a/src/Beutl/ViewModels/BufferStatusViewModel.cs b/src/Beutl/ViewModels/BufferStatusViewModel.cs index ba19dbb8c..6dd19a524 100644 --- a/src/Beutl/ViewModels/BufferStatusViewModel.cs +++ b/src/Beutl/ViewModels/BufferStatusViewModel.cs @@ -1,4 +1,6 @@ -using Reactive.Bindings; +using System.Collections.Immutable; + +using Reactive.Bindings; namespace Beutl.ViewModels; @@ -16,13 +18,19 @@ public BufferStatusViewModel(EditViewModel editViewModel) .ToReadOnlyReactivePropertySlim() .DisposeWith(_disposables); - IObservable length = EndTime.CombineLatest(StartTime) - .Select(v => v.First - v.Second); - - Width = length.CombineLatest(editViewModel.Scale) - .Select(v => Math.Max(0, v.First.ToPixel(v.Second))) + End = EndTime.CombineLatest(editViewModel.Scale) + .Select(v => v.First.ToPixel(v.Second)) .ToReadOnlyReactivePropertySlim() .DisposeWith(_disposables); + + editViewModel.FrameCacheManager.BlocksUpdated += OnFrameCacheManagerBlocksUpdated; + _disposables.Add(Disposable.Create(editViewModel.FrameCacheManager, m => m.BlocksUpdated -= OnFrameCacheManagerBlocksUpdated)); + } + + private void OnFrameCacheManagerBlocksUpdated(ImmutableArray<(int Start, int Length)> obj) + { + CacheBlocks.Value = obj.SelectArray( + v => new CacheBlock(_editViewModel.Player.GetFrameRate(), v.Start, v.Length)); } public ReactivePropertySlim StartTime { get; } = new(); @@ -31,10 +39,26 @@ public BufferStatusViewModel(EditViewModel editViewModel) public ReadOnlyReactivePropertySlim Start { get; } - public ReadOnlyReactivePropertySlim Width { get; } + public ReadOnlyReactivePropertySlim End { get; } + + public ReactivePropertySlim CacheBlocks { get; } = new([]); + + public sealed class CacheBlock + { + public CacheBlock(int rate, int start, int length) + { + Start = TimeSpanExtensions.ToTimeSpan(start, rate); + Length = TimeSpanExtensions.ToTimeSpan(length, rate); + } + + public TimeSpan Start { get; } + + public TimeSpan Length { get; } + } public void Dispose() { _disposables.Dispose(); + CacheBlocks.Value = []; } } diff --git a/src/Beutl/ViewModels/EditViewModel.cs b/src/Beutl/ViewModels/EditViewModel.cs index c1d3c54af..401b0d8a8 100644 --- a/src/Beutl/ViewModels/EditViewModel.cs +++ b/src/Beutl/ViewModels/EditViewModel.cs @@ -61,6 +61,7 @@ public EditViewModel(Scene scene) .DisposeWith(_disposables); Commands = new KnownCommandsImpl(scene, this); CommandRecorder = new CommandRecorder(); + FrameCacheManager = new FrameCacheManager(new(scene.Width, scene.Height)); SelectedObject = new ReactiveProperty() .DisposeWith(_disposables); @@ -97,6 +98,20 @@ public EditViewModel(Scene scene) private void OnCommandRecorderExecuted(object? sender, CommandExecutedEventArgs e) { + int rate = Player.GetFrameRate(); + bool removedAnyCache = false; + foreach (Element item in e.Storables.OfType()) + { + int st = (int)item.Start.ToFrameNumber(rate); + int ed = (int)Math.Ceiling(item.Range.End.ToFrameNumber(rate)); + removedAnyCache |= FrameCacheManager.RemoveRange(st, ed); + } + + if (removedAnyCache) + { + FrameCacheManager.UpdateBlocks(); + } + if (GlobalConfiguration.Instance.EditorConfig.IsAutoSaveEnabled) { Dispatcher.UIThread.Invoke(() => @@ -151,6 +166,8 @@ private void OnSelectedObjectDetachedFromHierarchy(object? sender, HierarchyAtta public CommandRecorder CommandRecorder { get; private set; } + public FrameCacheManager FrameCacheManager { get; private set; } + public EditorExtension Extension => SceneEditorExtension.Instance; public string EdittingFile => Scene.FileName; @@ -195,6 +212,8 @@ public void Dispose() CommandRecorder.Executed -= OnCommandRecorderExecuted; CommandRecorder.Clear(); CommandRecorder = null!; + FrameCacheManager.Dispose(); + FrameCacheManager = null!; } public T? FindToolTab(Func condition) diff --git a/src/Beutl/ViewModels/ElementViewModel.cs b/src/Beutl/ViewModels/ElementViewModel.cs index 3edb6d38f..2775ceef2 100644 --- a/src/Beutl/ViewModels/ElementViewModel.cs +++ b/src/Beutl/ViewModels/ElementViewModel.cs @@ -270,6 +270,14 @@ public async Task SubmitViewModelChanges() TimeSpan start = BorderMargin.Value.Left.ToTimeSpan(scale).RoundToRate(rate); TimeSpan length = Width.Value.ToTimeSpan(scale).RoundToRate(rate); int zindex = Timeline.ToLayerNumber(Margin.Value); + + if (zindex == Model.ZIndex + && start == Model.Start + && length == Model.Length) + { + return; + } + Scene.MoveChild(zindex, start, length, Model) .DoAndRecord(recorder); diff --git a/src/Beutl/ViewModels/PlayerViewModel.cs b/src/Beutl/ViewModels/PlayerViewModel.cs index c2987f714..c0f03d5de 100644 --- a/src/Beutl/ViewModels/PlayerViewModel.cs +++ b/src/Beutl/ViewModels/PlayerViewModel.cs @@ -1,6 +1,4 @@ -using System.Collections.Concurrent; - -using Avalonia.Media.Imaging; +using Avalonia.Media.Imaging; using Avalonia.Platform; using Beutl.Audio.Platforms.OpenAL; @@ -13,6 +11,8 @@ using Beutl.Media.Music; using Beutl.Media.Music.Samples; using Beutl.Media.Pixel; +using Beutl.Media.Source; +using Beutl.Models; using Beutl.ProjectSystem; using Beutl.Rendering; using Beutl.Rendering.Cache; @@ -30,155 +30,6 @@ namespace Beutl.ViewModels; -interface IPlayer : IDisposable -{ - public record struct Frame(Bitmap Bitmap, TimeSpan Time); - - void Start(); - - bool TryDequeue(out Frame frame); -} - -class BufferedPlayer : IPlayer -{ - private readonly ILogger _logger = Log.CreateLogger(); - private readonly ConcurrentQueue _queue = new(); - private readonly EditViewModel _editViewModel; - private readonly Scene _scene; - private readonly IReadOnlyReactiveProperty _isPlaying; - private readonly TaskCompletionSource _tsc; - private readonly int _rate; - private volatile CancellationTokenSource? _waitRenderToken; - private volatile TaskCompletionSource? _waitTimerTcs; - private readonly IDisposable _disposable; - private TimeSpan? _requestedFrame; - private bool _isDisposed; - - public BufferedPlayer( - EditViewModel editViewModel, - Scene scene, IReactiveProperty isPlaying, - TaskCompletionSource tsc, int rate) - { - _editViewModel = editViewModel; - _scene = scene; - _isPlaying = isPlaying; - _tsc = tsc; - _rate = rate; - - _disposable = isPlaying.Where(v => !v).Subscribe(_ => - { - _waitRenderToken?.Cancel(); - _waitTimerTcs?.TrySetResult(); - }); - } - - public void Start() - { - TimeSpan start = _scene.CurrentFrame; - TimeSpan tick = TimeSpan.FromSeconds(1d / _rate); - TimeSpan duration = _scene.Duration; - - RenderThread.Dispatcher.Dispatch(async () => - { - try - { - for (TimeSpan time = start; time < duration; time += tick) - { - if (!_isPlaying.Value) - break; - - time = time.RoundToRate(_rate); - - if (_queue.Count >= 120) - { - Debug.WriteLine("wait timer"); - await WaitTimer(); - } - - if (!_isPlaying.Value) - break; - - if (_scene.Renderer.Render(time)) - { - Debug.WriteLine($"{time} rendered."); - _queue.Enqueue(new(_scene.Renderer.Snapshot(), time)); - _waitRenderToken?.Cancel(); - - _editViewModel.BufferStatus.EndTime.Value = time; - } - - TimeSpan? requestedFrame = _requestedFrame; - if (requestedFrame.HasValue) - { - time = requestedFrame.Value + (tick * 2); - _requestedFrame = null; - } - } - } - catch (Exception ex) - { - NotificationService.ShowError(Message.AnUnexpectedErrorHasOccurred, Message.An_exception_occurred_while_drawing_frame); - _logger.LogError(ex, "An exception occurred while drawing the frame."); - } - finally - { - _tsc.TrySetResult(); - } - }); - } - - public bool TryDequeue(out IPlayer.Frame frame) - { - _waitTimerTcs?.TrySetResult(); - if (_queue.TryDequeue(out IPlayer.Frame f)) - { - frame = f; - return true; - } - - Debug.WriteLine("wait rendered"); - WaitRender(); - - return _queue.TryDequeue(out frame); - } - - private void WaitRender() - { - if (_isDisposed) return; - _waitRenderToken = new CancellationTokenSource(); - - _waitRenderToken.Token.WaitHandle.WaitOne(TimeSpan.FromSeconds(1d / _rate)); - _waitRenderToken = null; - } - - private async ValueTask WaitTimer() - { - if (_isDisposed) return; - _waitTimerTcs = new TaskCompletionSource(); - - await _waitTimerTcs.Task; - _waitTimerTcs = null; - } - - public void Skipped(TimeSpan requested) - { - Debug.WriteLine($"{requested} skipped"); - _requestedFrame = requested; - } - - public void Dispose() - { - _isDisposed = true; - _waitRenderToken?.Cancel(); - _waitTimerTcs?.TrySetResult(); - _disposable.Dispose(); - while (_queue.TryDequeue(out var f)) - { - f.Bitmap.Dispose(); - } - } -} - public sealed class PlayerViewModel : IDisposable { private static readonly TimeSpan s_second = TimeSpan.FromSeconds(1); @@ -320,11 +171,13 @@ public void Play() int rate = GetFrameRate(); TimeSpan tick = TimeSpan.FromSeconds(1d / rate); - TimeSpan startFrame = Scene.CurrentFrame; - TimeSpan duration = Scene.Duration; + TimeSpan startTime = Scene.CurrentFrame; + TimeSpan durationTime = Scene.Duration; + int startFrame = (int)startTime.ToFrameNumber(rate); + int durationFrame = (int)Math.Ceiling(durationTime.ToFrameNumber(rate)); var tcs = new TaskCompletionSource(); - _editViewModel.BufferStatus.StartTime.Value = startFrame; - _editViewModel.BufferStatus.EndTime.Value = startFrame; + _editViewModel.BufferStatus.StartTime.Value = startTime; + _editViewModel.BufferStatus.EndTime.Value = startTime; using var playerImpl = new BufferedPlayer(_editViewModel, Scene, IsPlaying, tcs, rate); playerImpl.Start(); @@ -335,11 +188,9 @@ public void Play() bool timerProcessing = false; timer.Elapsed += (_, e) => { - startFrame += tick; - TimeSpan time = startFrame; - time = time.RoundToRate(rate); + startFrame++; - if (time >= duration || !IsPlaying.Value) + if (startFrame >= durationFrame || !IsPlaying.Value) { timer.Stop(); tcs.TrySetResult(); @@ -348,7 +199,7 @@ public void Play() if (timerProcessing) { - playerImpl.Skipped(time); + playerImpl.Skipped(startFrame); return; } @@ -358,14 +209,14 @@ public void Play() if (playerImpl.TryDequeue(out IPlayer.Frame frame)) { + // 所有権が移転したので using (frame.Bitmap) { - UpdateImage(frame.Bitmap); + UpdateImage(frame.Bitmap.Value); if (Scene != null) { - Scene.CurrentFrame = frame.Time; - _editViewModel.BufferStatus.StartTime.Value = frame.Time; + Scene.CurrentFrame = TimeSpanExtensions.ToTimeSpan(frame.Time, rate); } } } @@ -378,7 +229,10 @@ public void Play() timer.Start(); await tcs.Task; + _editViewModel.FrameCacheManager.UpdateBlocks(); IsPlaying.Value = false; + _editViewModel.BufferStatus.StartTime.Value = TimeSpan.Zero; + _editViewModel.BufferStatus.EndTime.Value = TimeSpan.Zero; } catch (Exception ex) { @@ -392,7 +246,7 @@ public void Play() }); } - private int GetFrameRate() + public int GetFrameRate() { int rate = Project?.GetFrameRate() ?? 30; if (rate <= 0) @@ -603,7 +457,7 @@ private unsafe void UpdateImage(Bitmap source) PreviewInvalidated?.Invoke(this, EventArgs.Empty); } - private void DrawBoundaries(Renderer renderer) + private void DrawBoundaries(Renderer renderer, ImmediateCanvas canvas) { int? selected = _editViewModel.SelectedLayerNumber.Value; if (selected.HasValue) @@ -613,14 +467,13 @@ private void DrawBoundaries(Renderer renderer) if (scale == 0) scale = 1; - ImmediateCanvas canvas = Renderer.GetInternalCanvas(renderer); Rect[] boundary = renderer.RenderScene[selected.Value].GetBoundaries(); if (boundary.Length > 0) { - var pen = new Media.Immutable.ImmutablePen(Media.Brushes.White, null, 0, 1 / scale); + var pen = new Media.Immutable.ImmutablePen(Brushes.White, null, 0, 1 / scale); bool exactBounds = GlobalConfiguration.Instance.ViewConfig.ShowExactBoundaries; - foreach (Rect item in renderer.RenderScene[selected.Value].GetBoundaries()) + foreach (Rect item in boundary) { Rect rect = item; if (!exactBounds) @@ -642,13 +495,47 @@ void RenderOnRenderThread() { try { - if (Scene is { Renderer: Renderer renderer } - && renderer.Render(Scene.CurrentFrame)) + if (Scene is not { Renderer: SceneRenderer renderer }) return; + int rate = GetFrameRate(); + TimeSpan time = Scene.CurrentFrame; + int frame = (int)Math.Round(time.ToFrameNumber(rate), MidpointRounding.AwayFromZero); + time = TimeSpanExtensions.ToTimeSpan(frame, rate); + Bitmap? bitmap = null; + + if (_editViewModel.FrameCacheManager.TryGet(frame, out var cache)) + { + using (cache) + { + renderer.GraphicsEvaluator.Evaluate(); + + ImmediateCanvas canvas = Renderer.GetInternalCanvas(renderer); + canvas.Clear(); + canvas.DrawBitmap(cache.Value, Brushes.White, null); + DrawBoundaries(renderer, canvas); + + bitmap = renderer.Snapshot(); + } + } + else if (renderer.Render(time)) { - DrawBoundaries(renderer); + using (var forCache = Ref>.Create(renderer.Snapshot())) + { + _editViewModel.FrameCacheManager.Add(frame, forCache); + _editViewModel.FrameCacheManager.UpdateBlocks(); + } + + ImmediateCanvas canvas = Renderer.GetInternalCanvas(renderer); + DrawBoundaries(renderer, canvas); - using Media.Bitmap bitmap = renderer.Snapshot(); - UpdateImage(bitmap); + bitmap = renderer.Snapshot(); + } + + if (bitmap != null) + { + using (bitmap) + { + UpdateImage(bitmap); + } } } catch (Exception ex) diff --git a/src/Beutl/Views/Timeline.axaml b/src/Beutl/Views/Timeline.axaml index 2db407a83..36d8ca297 100644 --- a/src/Beutl/Views/Timeline.axaml +++ b/src/Beutl/Views/Timeline.axaml @@ -28,8 +28,9 @@ Height="32" HorizontalAlignment="Stretch" VerticalAlignment="Top" - BufferLength="{Binding EditorContext.BufferStatus.Width.Value}" + BufferEnd="{Binding EditorContext.BufferStatus.End.Value}" BufferStart="{Binding EditorContext.BufferStatus.Start.Value}" + CacheBlocks="{Binding EditorContext.BufferStatus.CacheBlocks.Value}" EndingBarMargin="{CompiledBinding EndingBarMargin.Value}" PointerExited="TimelinePanel_PointerExited" PointerMoved="TimelinePanel_PointerMoved" diff --git a/src/Beutl/Views/TimelineOverlay.cs b/src/Beutl/Views/TimelineOverlay.cs index d5257e526..7c2246040 100644 --- a/src/Beutl/Views/TimelineOverlay.cs +++ b/src/Beutl/Views/TimelineOverlay.cs @@ -12,6 +12,7 @@ public static class TimelineSharedObject public static readonly IPen SelectionPen; public static readonly IBrush SelectionFillBrush = new ImmutableSolidColorBrush(Colors.CornflowerBlue, 0.3); public static readonly IBrush BufferRangeFillBrush = new ImmutableSolidColorBrush(Colors.SkyBlue); + public static readonly IBrush CacheBlockFillBrush = new ImmutableSolidColorBrush(Colors.LightGreen); public static readonly IBrush DropFrameFillBrush = new ImmutableSolidColorBrush(Colors.Orange); static TimelineSharedObject() diff --git a/src/Beutl/Views/TimelineScale.cs b/src/Beutl/Views/TimelineScale.cs index b034a53c2..c399245ab 100644 --- a/src/Beutl/Views/TimelineScale.cs +++ b/src/Beutl/Views/TimelineScale.cs @@ -7,6 +7,8 @@ using Avalonia.Media.TextFormatting; using Avalonia.Threading; +using static Beutl.ViewModels.BufferStatusViewModel; + namespace Beutl.Views; public sealed class TimelineScale : Control @@ -32,8 +34,11 @@ public static readonly DirectProperty SeekBarMarginPro public static readonly StyledProperty BufferStartProperty = AvaloniaProperty.Register(nameof(BufferStart)); - public static readonly StyledProperty BufferLengthProperty - = AvaloniaProperty.Register(nameof(BufferLength)); + public static readonly StyledProperty BufferEndProperty + = AvaloniaProperty.Register(nameof(BufferEnd)); + + public static readonly StyledProperty CacheBlocksProperty + = AvaloniaProperty.Register(nameof(CacheBlocks)); private static readonly Typeface s_typeface = new(FontFamily.Default, FontStyle.Normal, FontWeight.Medium); private readonly Pen _pen; @@ -52,7 +57,7 @@ static TimelineScale() EndingBarMarginProperty, SeekBarMarginProperty, BufferStartProperty, - BufferLengthProperty); + BufferEndProperty); } public TimelineScale() @@ -91,10 +96,16 @@ public double BufferStart set => SetValue(BufferStartProperty, value); } - public double BufferLength + public double BufferEnd + { + get => GetValue(BufferEndProperty); + set => SetValue(BufferEndProperty, value); + } + + public CacheBlock[]? CacheBlocks { - get => GetValue(BufferLengthProperty); - set => SetValue(BufferLengthProperty, value); + get => GetValue(CacheBlocksProperty); + set => SetValue(CacheBlocksProperty, value); } protected override void OnLoaded(RoutedEventArgs e) @@ -166,9 +177,22 @@ public override void Render(DrawingContext context) } } - context.DrawRectangle( - TimelineSharedObject.BufferRangeFillBrush, null, - new RoundedRect(new Rect(BufferStart, Height - 4, BufferLength, 4))); + if (CacheBlocks != null) + { + foreach (CacheBlock item in CacheBlocks) + { + context.DrawRectangle( + TimelineSharedObject.CacheBlockFillBrush, null, + new RoundedRect(new Rect(item.Start.ToPixel(Scale), Height - 4, item.Length.ToPixel(Scale), 4))); + } + } + + if (BufferEnd != BufferStart) + { + context.DrawRectangle( + TimelineSharedObject.BufferRangeFillBrush, null, + new RoundedRect(new Rect(BufferStart, Height - 4, BufferEnd - BufferStart, 4))); + } var size = new Size(1.25, height); var seekbar = new Point(_seekBarMargin.Left, 0); From 7616febd702e317275c730687852a72d5190a55e Mon Sep 17 00:00:00 2001 From: indigo-san Date: Sun, 28 Jan 2024 01:48:35 +0900 Subject: [PATCH 03/25] =?UTF-8?q?=E3=82=AD=E3=83=A3=E3=83=83=E3=82=B7?= =?UTF-8?q?=E3=83=A5=E3=82=B5=E3=82=A4=E3=82=BA=E3=82=92=E8=A8=AD=E5=AE=9A?= =?UTF-8?q?=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E3=81=97?= =?UTF-8?q?=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Beutl.Configuration/EditorConfig.cs | 72 ++++++++++++++++++- src/Beutl/Models/FrameCacheManager.cs | 13 ++-- src/Beutl/ViewModels/BufferStatusViewModel.cs | 11 ++- src/Beutl/ViewModels/EditViewModel.cs | 12 ++++ src/Beutl/Views/TimelineScale.cs | 3 +- 5 files changed, 102 insertions(+), 9 deletions(-) diff --git a/src/Beutl.Configuration/EditorConfig.cs b/src/Beutl.Configuration/EditorConfig.cs index 08f2ffecb..70787158a 100644 --- a/src/Beutl.Configuration/EditorConfig.cs +++ b/src/Beutl.Configuration/EditorConfig.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.Diagnostics; using Beutl.Collections; using Beutl.Serialization; @@ -16,6 +17,7 @@ public sealed class EditorConfig : ConfigurationBase public static readonly CoreProperty AutoAdjustSceneDurationProperty; public static readonly CoreProperty EnablePointerLockInPropertyProperty; public static readonly CoreProperty IsAutoSaveEnabledProperty; + public static readonly CoreProperty FrameCacheMaxSizeProperty; static EditorConfig() { @@ -30,6 +32,16 @@ static EditorConfig() IsAutoSaveEnabledProperty = ConfigureProperty(nameof(IsAutoSaveEnabled)) .DefaultValue(true) .Register(); + + ulong memSize = OperatingSystem.IsWindows() ? GetWindowsMemoryCapacity() + : OperatingSystem.IsLinux() ? GetLinuxMemoryCapacity() + : 1024 * 1024 * 1024; + double memSizeInMG = memSize / (1024d * 1024d); + + // デフォルトはメモリ容量の半分にする + FrameCacheMaxSizeProperty = ConfigureProperty(nameof(FrameCacheMaxSize)) + .DefaultValue(memSizeInMG / 2) + .Register(); } public EditorConfig() @@ -48,13 +60,19 @@ public bool EnablePointerLockInProperty get => GetValue(EnablePointerLockInPropertyProperty); set => SetValue(EnablePointerLockInPropertyProperty, value); } - + public bool IsAutoSaveEnabled { get => GetValue(IsAutoSaveEnabledProperty); set => SetValue(IsAutoSaveEnabledProperty, value); } + public double FrameCacheMaxSize + { + get => GetValue(FrameCacheMaxSizeProperty); + set => SetValue(FrameCacheMaxSizeProperty, value); + } + public CoreDictionary LibraryTabDisplayModes { get; } = new() { ["Search"] = LibraryTabDisplayMode.Show, @@ -94,4 +112,56 @@ public override void Deserialize(ICoreSerializationContext context) } } } + + private static ulong GetWindowsMemoryCapacity() + { + // wmic memorychip get capacity + using var process = Process.Start(new ProcessStartInfo("wmic", "memorychip get capacity") + { + CreateNoWindow = true, + RedirectStandardOutput = true + }); + + if (process != null && process.WaitForExit(500)) + { + ulong value = 0; + while (process.StandardOutput.ReadLine() is string line) + { + if (ulong.TryParse(line, out ulong v)) + { + value += v; + } + } + + return value; + } + + // https://www.microsoft.com/ja-jp/windows/windows-11-specifications + return 4L * 1024 * 1024 * 1024; + } + + private static ulong GetLinuxMemoryCapacity() + { + const string FileName = "/proc/meminfo"; + // このifは多分無駄 + if (File.Exists(FileName)) + { + foreach (string item in File.ReadLines(FileName)) + { + if (item.StartsWith("MemTotal:")) + { + string? s = item.Split(' ', StringSplitOptions.RemoveEmptyEntries).ElementAtOrDefault(1); + if (ulong.TryParse(s, out ulong v)) + { + // kBで固定されている + // https://github.com/torvalds/linux/blob/3a5879d495b226d0404098e3564462d5f1daa33b/fs/proc/meminfo.c#L31 + return v * 1024; + } + } + } + } + + // https://help.ubuntu.com/community/Installation/SystemRequirements + return 4L * 1024 * 1024 * 1024; + } } diff --git a/src/Beutl/Models/FrameCacheManager.cs b/src/Beutl/Models/FrameCacheManager.cs index 191b81fae..7a4c45e67 100644 --- a/src/Beutl/Models/FrameCacheManager.cs +++ b/src/Beutl/Models/FrameCacheManager.cs @@ -1,6 +1,8 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; + +using Beutl.Configuration; using Beutl.Graphics; using Beutl.Media; using Beutl.Media.Pixel; @@ -14,8 +16,8 @@ public sealed class FrameCacheManager : IDisposable private readonly SortedDictionary _entries = []; private readonly object _lock = new(); private readonly PixelSize _frameSize; + private readonly ulong _maxSize; private ulong _size; - private const ulong MaxSize = 1024 * 1024 * 1024; public event Action? Added; public event Action? Removed; @@ -24,6 +26,7 @@ public sealed class FrameCacheManager : IDisposable public FrameCacheManager(PixelSize frameSize) { _frameSize = frameSize; + _maxSize = (ulong)(GlobalConfiguration.Instance.EditorConfig.FrameCacheMaxSize * 1024 * 1024); } public ImmutableArray<(int Start, int Length)> Blocks { get; private set; } @@ -44,7 +47,7 @@ public void Add(int frame, Ref> bitmap) } } - if (_size >= MaxSize) + if (_size >= _maxSize) { Task.Run(Optimize); } @@ -165,9 +168,9 @@ private void Optimize() { lock (_lock) { - if (_size >= MaxSize) + if (_size >= _maxSize) { - ulong excess = _size - MaxSize; + ulong excess = _size - _maxSize; int sizePerCache = _frameSize.Width * _frameSize.Height * 4; var targetCount = excess / (ulong)sizePerCache; @@ -177,7 +180,7 @@ private void Optimize() .ToArray(); foreach (KeyValuePair item in items) { - if (_size < MaxSize) + if (_size < _maxSize) break; item.Value.Dispose(); diff --git a/src/Beutl/ViewModels/BufferStatusViewModel.cs b/src/Beutl/ViewModels/BufferStatusViewModel.cs index 6dd19a524..925b59e24 100644 --- a/src/Beutl/ViewModels/BufferStatusViewModel.cs +++ b/src/Beutl/ViewModels/BufferStatusViewModel.cs @@ -1,5 +1,7 @@ using System.Collections.Immutable; +using Avalonia.Threading; + using Reactive.Bindings; namespace Beutl.ViewModels; @@ -8,6 +10,7 @@ public sealed class BufferStatusViewModel : IDisposable { private readonly CompositeDisposable _disposables = []; private readonly EditViewModel _editViewModel; + private CancellationTokenSource? _cts; public BufferStatusViewModel(EditViewModel editViewModel) { @@ -29,8 +32,12 @@ public BufferStatusViewModel(EditViewModel editViewModel) private void OnFrameCacheManagerBlocksUpdated(ImmutableArray<(int Start, int Length)> obj) { - CacheBlocks.Value = obj.SelectArray( - v => new CacheBlock(_editViewModel.Player.GetFrameRate(), v.Start, v.Length)); + _cts?.Cancel(); + _cts = new CancellationTokenSource(); + Dispatcher.UIThread.InvokeAsync( + () => CacheBlocks.Value = obj.SelectArray(v => new CacheBlock(_editViewModel.Player.GetFrameRate(), v.Start, v.Length)), + DispatcherPriority.Background, + _cts.Token); } public ReactivePropertySlim StartTime { get; } = new(); diff --git a/src/Beutl/ViewModels/EditViewModel.cs b/src/Beutl/ViewModels/EditViewModel.cs index 401b0d8a8..9f997bac4 100644 --- a/src/Beutl/ViewModels/EditViewModel.cs +++ b/src/Beutl/ViewModels/EditViewModel.cs @@ -75,6 +75,18 @@ public EditViewModel(Scene scene) RestoreState(); + scene.GetPropertyChangedObservable(Scene.RendererProperty) + .Subscribe(e => + { + if (e.NewValue != null) + { + FrameCacheManager old = FrameCacheManager; + FrameCacheManager = new FrameCacheManager(e.NewValue.FrameSize); + old.Dispose(); + } + }) + .DisposeWith(_disposables); + SelectedObject.CombineWithPrevious() .Subscribe(v => { diff --git a/src/Beutl/Views/TimelineScale.cs b/src/Beutl/Views/TimelineScale.cs index c399245ab..129955470 100644 --- a/src/Beutl/Views/TimelineScale.cs +++ b/src/Beutl/Views/TimelineScale.cs @@ -57,7 +57,8 @@ static TimelineScale() EndingBarMarginProperty, SeekBarMarginProperty, BufferStartProperty, - BufferEndProperty); + BufferEndProperty, + CacheBlocksProperty); } public TimelineScale() From 4c80b9f0a2c8d190b5b863ad255219f4562d0a2a Mon Sep 17 00:00:00 2001 From: indigo-san Date: Sun, 28 Jan 2024 15:05:12 +0900 Subject: [PATCH 04/25] =?UTF-8?q?=E3=82=B3=E3=83=9E=E3=83=B3=E3=83=89?= =?UTF-8?q?=E3=81=8B=E3=82=89=E3=82=AD=E3=83=A3=E3=83=83=E3=82=B7=E3=83=A5?= =?UTF-8?q?=E3=81=AE=E7=84=A1=E5=8A=B9=E5=8C=96=E7=AF=84=E5=9B=B2=E3=82=92?= =?UTF-8?q?=E5=8F=96=E5=BE=97=E3=81=99=E3=82=8B=E3=82=A4=E3=83=B3=E3=82=BF?= =?UTF-8?q?=E3=83=BC=E3=83=95=E3=82=A7=E3=83=BC=E3=82=B9=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../IAffectsTimelineCommand.cs | 10 ++++++++ .../ProjectSystem/Scene.cs | 15 +++++++++++- src/Beutl/Models/FrameCacheManager.cs | 18 ++++++++++++++ src/Beutl/Program.cs | 5 ++++ src/Beutl/ViewModels/EditViewModel.cs | 24 +++++++++---------- 5 files changed, 58 insertions(+), 14 deletions(-) create mode 100644 src/Beutl.ProjectSystem/IAffectsTimelineCommand.cs diff --git a/src/Beutl.ProjectSystem/IAffectsTimelineCommand.cs b/src/Beutl.ProjectSystem/IAffectsTimelineCommand.cs new file mode 100644 index 000000000..579033ca5 --- /dev/null +++ b/src/Beutl.ProjectSystem/IAffectsTimelineCommand.cs @@ -0,0 +1,10 @@ +using System.Collections.Immutable; + +using Beutl.Media; + +namespace Beutl; + +public interface IAffectsTimelineCommand : IRecordableCommand +{ + ImmutableArray GetAffectedRange(); +} diff --git a/src/Beutl.ProjectSystem/ProjectSystem/Scene.cs b/src/Beutl.ProjectSystem/ProjectSystem/Scene.cs index 006cd4684..66f92d308 100644 --- a/src/Beutl.ProjectSystem/ProjectSystem/Scene.cs +++ b/src/Beutl.ProjectSystem/ProjectSystem/Scene.cs @@ -837,7 +837,7 @@ private sealed class MoveCommand( Element element, TimeSpan newStart, TimeSpan oldStart, TimeSpan newLength, TimeSpan oldLength, - Scene scene) : IRecordableCommand + Scene scene) : IRecordableCommand, IAffectsTimelineCommand { private readonly int _oldZIndex = element.ZIndex; private readonly TimeSpan _oldSceneDuration = scene.Duration; @@ -845,6 +845,9 @@ private sealed class MoveCommand( public ImmutableArray GetStorables() => [scene, element]; + public ImmutableArray GetAffectedRange() + => [new TimeRange(newStart, newLength), new TimeRange(oldStart, oldLength)]; + public void Do() { TimeSpan newEnd = newStart + newLength; @@ -922,6 +925,7 @@ private sealed class MultipleMoveCommand : IRecordableCommand private readonly bool _adjustSceneDuration; private readonly TimeSpan _oldSceneDuration; private readonly TimeSpan _newSceneDuration; + private readonly ImmutableArray _affectedRange; public MultipleMoveCommand( Scene scene, @@ -964,6 +968,13 @@ public MultipleMoveCommand( _newSceneDuration = maxEndingTime; } } + + if (!_conflict) + { + _affectedRange = elements + .SelectMany(v => new[] { v.Range, v.Range.AddStart(_deltaTime) }) + .ToImmutableArray(); + } } private bool HasConflict(Scene scene, int deltaZIndex, TimeSpan deltaTime) @@ -1027,6 +1038,8 @@ private bool HasConflict(Scene scene, int deltaZIndex, TimeSpan deltaTime) return [_scene, .. _elements]; } + public ImmutableArray GetAffectedRange() => _affectedRange; + public void Do() { if (!_conflict) diff --git a/src/Beutl/Models/FrameCacheManager.cs b/src/Beutl/Models/FrameCacheManager.cs index 7a4c45e67..e9c61f096 100644 --- a/src/Beutl/Models/FrameCacheManager.cs +++ b/src/Beutl/Models/FrameCacheManager.cs @@ -95,6 +95,24 @@ public bool RemoveRange(int start, int end) } } + public void RemoveAndUpdateBlocks(IEnumerable<(int Start, int End)> timeRanges) + { + lock (_lock) + { + bool removedAnyCache = false; + + foreach ((int Start, int End) in timeRanges) + { + removedAnyCache |= RemoveRange(Start, End); + } + + if (removedAnyCache) + { + UpdateBlocks(); + } + } + } + public void Dispose() { lock (_lock) diff --git a/src/Beutl/Program.cs b/src/Beutl/Program.cs index caa1e498f..95400a1f6 100644 --- a/src/Beutl/Program.cs +++ b/src/Beutl/Program.cs @@ -33,6 +33,11 @@ public static void Main(string[] args) UnhandledExceptionHandler.Initialize(); + RenderThread.Dispatcher.Dispatch(() => + { + Thread.CurrentThread.Name = "Beutl.RenderThread"; + Thread.CurrentThread.IsBackground = true; + }, Threading.DispatchPriority.High); RenderThread.Dispatcher.Dispatch(SharedGPUContext.Create, Threading.DispatchPriority.High); BuildAvaloniaApp() diff --git a/src/Beutl/ViewModels/EditViewModel.cs b/src/Beutl/ViewModels/EditViewModel.cs index 9f997bac4..611ccf6f8 100644 --- a/src/Beutl/ViewModels/EditViewModel.cs +++ b/src/Beutl/ViewModels/EditViewModel.cs @@ -1,4 +1,5 @@ -using System.Numerics; +using System.Collections.Immutable; +using System.Numerics; using System.Text.Json.Nodes; using System.Windows.Input; @@ -110,19 +111,16 @@ public EditViewModel(Scene scene) private void OnCommandRecorderExecuted(object? sender, CommandExecutedEventArgs e) { - int rate = Player.GetFrameRate(); - bool removedAnyCache = false; - foreach (Element item in e.Storables.OfType()) + Task.Run(() => { - int st = (int)item.Start.ToFrameNumber(rate); - int ed = (int)Math.Ceiling(item.Range.End.ToFrameNumber(rate)); - removedAnyCache |= FrameCacheManager.RemoveRange(st, ed); - } - - if (removedAnyCache) - { - FrameCacheManager.UpdateBlocks(); - } + int rate = Player.GetFrameRate(); + IEnumerable affectedRange = e.Command is IAffectsTimelineCommand affectsTimeline + ? affectsTimeline.GetAffectedRange() + : e.Storables.OfType().Select(v => v.Range); + + FrameCacheManager.RemoveAndUpdateBlocks(affectedRange + .Select(item => (Start: (int)item.Start.ToFrameNumber(rate), End: (int)Math.Ceiling(item.End.ToFrameNumber(rate))))); + }); if (GlobalConfiguration.Instance.EditorConfig.IsAutoSaveEnabled) { From 8ff234de574c02f866dc1d203ae5356461c2bb8d Mon Sep 17 00:00:00 2001 From: indigo-san Date: Sun, 28 Jan 2024 15:06:30 +0900 Subject: [PATCH 05/25] =?UTF-8?q?BufferedPlayer.Start=E3=81=AE=E3=83=AB?= =?UTF-8?q?=E3=83=BC=E3=83=97=E3=81=A7await=E3=81=97=E3=81=AA=E3=81=84?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Beutl/Models/BufferedPlayer.cs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Beutl/Models/BufferedPlayer.cs b/src/Beutl/Models/BufferedPlayer.cs index 62b9ce2db..e8db2be6a 100644 --- a/src/Beutl/Models/BufferedPlayer.cs +++ b/src/Beutl/Models/BufferedPlayer.cs @@ -24,7 +24,7 @@ public class BufferedPlayer : IPlayer private readonly TaskCompletionSource _tsc; private readonly int _rate; private volatile CancellationTokenSource? _waitRenderToken; - private volatile TaskCompletionSource? _waitTimerTcs; + private volatile CancellationTokenSource? _waitTimerToken; private readonly IDisposable _disposable; private int? _requestedFrame; private bool _isDisposed; @@ -44,7 +44,7 @@ public BufferedPlayer( _disposable = isPlaying.Where(v => !v).Subscribe(_ => { _waitRenderToken?.Cancel(); - _waitTimerTcs?.TrySetResult(); + _waitTimerToken?.Cancel(); }); } @@ -53,7 +53,7 @@ public void Start() int startFrame = (int)_scene.CurrentFrame.ToFrameNumber(_rate); int durationFrame = (int)Math.Ceiling(_scene.Duration.ToFrameNumber(_rate)); - RenderThread.Dispatcher.Dispatch(async () => + RenderThread.Dispatcher.Dispatch(() => { try { @@ -65,7 +65,7 @@ public void Start() if (_queue.Count >= 120) { Debug.WriteLine("wait timer"); - await WaitTimer(); + WaitTimer(); } if (!_isPlaying.Value) @@ -118,12 +118,12 @@ public void Start() { _tsc.TrySetResult(); } - }); + }, Threading.DispatchPriority.High); } public bool TryDequeue(out IPlayer.Frame frame) { - _waitTimerTcs?.TrySetResult(); + _waitTimerToken?.Cancel(); if (_queue.TryDequeue(out IPlayer.Frame f)) { frame = f; @@ -145,13 +145,13 @@ private void WaitRender() _waitRenderToken = null; } - private async ValueTask WaitTimer() + private void WaitTimer() { if (_isDisposed) return; - _waitTimerTcs = new TaskCompletionSource(); + _waitTimerToken = new CancellationTokenSource(); - await _waitTimerTcs.Task; - _waitTimerTcs = null; + _waitTimerToken.Token.WaitHandle.WaitOne(); + _waitTimerToken = null; } public void Skipped(int requestedFrame) @@ -164,7 +164,7 @@ public void Dispose() { _isDisposed = true; _waitRenderToken?.Cancel(); - _waitTimerTcs?.TrySetResult(); + _waitTimerToken?.Cancel(); _disposable.Dispose(); while (_queue.TryDequeue(out var f)) { From a1391357de00aef6a8a6e404767ee45469a7be9d Mon Sep 17 00:00:00 2001 From: indigo-san Date: Sun, 28 Jan 2024 17:06:43 +0900 Subject: [PATCH 06/25] =?UTF-8?q?=E6=9C=80=E5=BE=8C=E3=81=BE=E3=81=A7?= =?UTF-8?q?=E5=86=8D=E7=94=9F=E3=81=95=E3=82=8C=E3=81=AA=E3=81=84=E3=81=AE?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Beutl/Models/BufferedPlayer.cs | 15 +++++---------- src/Beutl/ViewModels/PlayerViewModel.cs | 2 +- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/Beutl/Models/BufferedPlayer.cs b/src/Beutl/Models/BufferedPlayer.cs index e8db2be6a..f0b3e5ccf 100644 --- a/src/Beutl/Models/BufferedPlayer.cs +++ b/src/Beutl/Models/BufferedPlayer.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; + using Beutl.Logging; using Beutl.Media; using Beutl.Media.Pixel; @@ -7,13 +8,14 @@ using Beutl.Rendering; using Beutl.Services; using Beutl.ViewModels; + using Microsoft.Extensions.Logging; using Reactive.Bindings; namespace Beutl.Models; -public class BufferedPlayer : IPlayer +public sealed class BufferedPlayer : IPlayer { private readonly ILogger _logger = Log.CreateLogger(); private readonly ConcurrentQueue _queue = new(); @@ -21,7 +23,6 @@ public class BufferedPlayer : IPlayer private readonly FrameCacheManager _frameCacheManager; private readonly Scene _scene; private readonly IReadOnlyReactiveProperty _isPlaying; - private readonly TaskCompletionSource _tsc; private readonly int _rate; private volatile CancellationTokenSource? _waitRenderToken; private volatile CancellationTokenSource? _waitTimerToken; @@ -30,15 +31,13 @@ public class BufferedPlayer : IPlayer private bool _isDisposed; public BufferedPlayer( - EditViewModel editViewModel, - Scene scene, IReactiveProperty isPlaying, - TaskCompletionSource tsc, int rate) + EditViewModel editViewModel, Scene scene, + IReactiveProperty isPlaying, int rate) { _editViewModel = editViewModel; _frameCacheManager = editViewModel.FrameCacheManager; _scene = scene; _isPlaying = isPlaying; - _tsc = tsc; _rate = rate; _disposable = isPlaying.Where(v => !v).Subscribe(_ => @@ -114,10 +113,6 @@ public void Start() NotificationService.ShowError(Message.AnUnexpectedErrorHasOccurred, Message.An_exception_occurred_while_drawing_frame); _logger.LogError(ex, "An exception occurred while drawing the frame."); } - finally - { - _tsc.TrySetResult(); - } }, Threading.DispatchPriority.High); } diff --git a/src/Beutl/ViewModels/PlayerViewModel.cs b/src/Beutl/ViewModels/PlayerViewModel.cs index c0f03d5de..464b12f49 100644 --- a/src/Beutl/ViewModels/PlayerViewModel.cs +++ b/src/Beutl/ViewModels/PlayerViewModel.cs @@ -179,7 +179,7 @@ public void Play() _editViewModel.BufferStatus.StartTime.Value = startTime; _editViewModel.BufferStatus.EndTime.Value = startTime; - using var playerImpl = new BufferedPlayer(_editViewModel, Scene, IsPlaying, tcs, rate); + using var playerImpl = new BufferedPlayer(_editViewModel, Scene, IsPlaying, rate); playerImpl.Start(); PlayAudio(Scene); From 3388752b8a797e96fbada61c658e03678aceb9d0 Mon Sep 17 00:00:00 2001 From: indigo-san Date: Sun, 28 Jan 2024 17:08:14 +0900 Subject: [PATCH 07/25] =?UTF-8?q?=E6=89=8B=E5=8B=95=E3=81=A7=E3=82=AD?= =?UTF-8?q?=E3=83=A3=E3=83=83=E3=82=B7=E3=83=A5=E3=82=92=E5=89=8A=E9=99=A4?= =?UTF-8?q?=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E3=81=97?= =?UTF-8?q?=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Beutl/ViewModels/BufferStatusViewModel.cs | 14 +++---- src/Beutl/Views/Timeline.axaml | 10 ++++- src/Beutl/Views/Timeline.axaml.cs | 40 ++++++++++++++++++- src/Beutl/Views/TimelineScale.cs | 28 ++++++++++++- 4 files changed, 80 insertions(+), 12 deletions(-) diff --git a/src/Beutl/ViewModels/BufferStatusViewModel.cs b/src/Beutl/ViewModels/BufferStatusViewModel.cs index 925b59e24..debacb0fa 100644 --- a/src/Beutl/ViewModels/BufferStatusViewModel.cs +++ b/src/Beutl/ViewModels/BufferStatusViewModel.cs @@ -50,17 +50,15 @@ private void OnFrameCacheManagerBlocksUpdated(ImmutableArray<(int Start, int Len public ReactivePropertySlim CacheBlocks { get; } = new([]); - public sealed class CacheBlock + public sealed class CacheBlock(int rate, int start, int length) { - public CacheBlock(int rate, int start, int length) - { - Start = TimeSpanExtensions.ToTimeSpan(start, rate); - Length = TimeSpanExtensions.ToTimeSpan(length, rate); - } + public TimeSpan Start { get; } = TimeSpanExtensions.ToTimeSpan(start, rate); - public TimeSpan Start { get; } + public TimeSpan Length { get; } = TimeSpanExtensions.ToTimeSpan(length, rate); - public TimeSpan Length { get; } + public int StartFrame { get; } = start; + + public int LengthFrame { get; } = length; } public void Dispose() diff --git a/src/Beutl/Views/Timeline.axaml b/src/Beutl/Views/Timeline.axaml index 36d8ca297..b18af7ac3 100644 --- a/src/Beutl/Views/Timeline.axaml +++ b/src/Beutl/Views/Timeline.axaml @@ -38,7 +38,15 @@ PointerReleased="TimelinePanel_PointerReleased" Scale="{Binding Options.Value.Scale}" SeekBarMargin="{CompiledBinding SeekBarMargin.Value}" - Offset="{Binding #ContentScroll.Offset, Mode=OneWay}" /> + Offset="{Binding #ContentScroll.Offset, Mode=OneWay}"> + + + + + + diff --git a/src/Beutl/Views/Timeline.axaml.cs b/src/Beutl/Views/Timeline.axaml.cs index 8568499d4..ecbc7ca15 100644 --- a/src/Beutl/Views/Timeline.axaml.cs +++ b/src/Beutl/Views/Timeline.axaml.cs @@ -44,6 +44,7 @@ internal enum MouseFlags internal MouseFlags _mouseFlag = MouseFlags.Free; internal TimeSpan _pointerFrame; + private bool _rightButtonPressed; private readonly ILogger _logger = Log.CreateLogger(); private TimelineViewModel? _viewModel; private readonly CompositeDisposable _disposables = []; @@ -368,12 +369,12 @@ private void TimelinePanel_PointerMoved(object? sender, PointerEventArgs e) TimelineViewModel viewModel = ViewModel; PointerPoint pointerPt = e.GetCurrentPoint(TimelinePanel); int rate = viewModel.Scene.FindHierarchicalParent().GetFrameRate(); - _pointerFrame = pointerPt.Position.X.ToTimeSpan(viewModel.Options.Value.Scale).RoundToRate(rate); + _pointerFrame = pointerPt.Position.X.ToTimeSpan(viewModel.Options.Value.Scale).FloorToRate(rate); if (_pointerFrame >= viewModel.Scene.Duration) { _pointerFrame = viewModel.Scene.Duration - TimeSpan.FromSeconds(1d / rate); - _pointerFrame = _pointerFrame.RoundToRate(rate); + //_pointerFrame = _pointerFrame.RoundToRate(rate); } if (_mouseFlag == MouseFlags.SeekBarPressed) @@ -386,6 +387,21 @@ private void TimelinePanel_PointerMoved(object? sender, PointerEventArgs e) overlay.SelectionRange = new(rect.Position, pointerPt.Position); UpdateRangeSelection(); } + else + { + Point posScale = e.GetPosition(Scale); + + if (Scale.IsPointerOver && posScale.Y > Scale.Bounds.Height - 8) + { + BufferStatusViewModel.CacheBlock[] cacheBlocks = viewModel.EditorContext.BufferStatus.CacheBlocks.Value; + + Scale.HoveredCacheBlock = Array.Find(cacheBlocks, v => new TimeRange(v.Start, v.Length).Contains(_pointerFrame)); + } + else + { + Scale.HoveredCacheBlock = null; + } + } } // ポインターが放された @@ -403,6 +419,10 @@ private void TimelinePanel_PointerReleased(object? sender, PointerReleasedEventA _mouseFlag = MouseFlags.Free; } + else if (pointerPt.Properties.PointerUpdateKind == PointerUpdateKind.RightButtonReleased) + { + _rightButtonPressed = false; + } } private void UpdateRangeSelection() @@ -458,12 +478,19 @@ private void TimelinePanel_PointerPressed(object? sender, PointerPressedEventArg viewModel.Scene.CurrentFrame = viewModel.ClickedFrame; } } + + _rightButtonPressed = pointerPt.Properties.IsRightButtonPressed; } // ポインターが離れた private void TimelinePanel_PointerExited(object? sender, PointerEventArgs e) { _mouseFlag = MouseFlags.Free; + + if (!_rightButtonPressed) + { + Scale.HoveredCacheBlock = null; + } } // ドロップされた @@ -710,4 +737,13 @@ private async void ScrollTimelinePosition(TimeRange range, int zindex) } } } + + private void DeleteFrameCacheClick(object? sender, RoutedEventArgs e) + { + if (Scale.HoveredCacheBlock is { } block) + { + ViewModel.EditorContext.FrameCacheManager.RemoveAndUpdateBlocks( + new[] { (block.StartFrame, block.StartFrame + block.LengthFrame) }); + } + } } diff --git a/src/Beutl/Views/TimelineScale.cs b/src/Beutl/Views/TimelineScale.cs index 129955470..4ce81e39c 100644 --- a/src/Beutl/Views/TimelineScale.cs +++ b/src/Beutl/Views/TimelineScale.cs @@ -40,6 +40,9 @@ public static readonly StyledProperty BufferEndProperty public static readonly StyledProperty CacheBlocksProperty = AvaloniaProperty.Register(nameof(CacheBlocks)); + public static readonly StyledProperty HoveredCacheBlockProperty + = AvaloniaProperty.Register(nameof(HoveredCacheBlock)); + private static readonly Typeface s_typeface = new(FontFamily.Default, FontStyle.Normal, FontWeight.Medium); private readonly Pen _pen; private IBrush _brush = Brushes.White; @@ -58,7 +61,8 @@ static TimelineScale() SeekBarMarginProperty, BufferStartProperty, BufferEndProperty, - CacheBlocksProperty); + CacheBlocksProperty, + HoveredCacheBlockProperty); } public TimelineScale() @@ -109,6 +113,12 @@ public CacheBlock[]? CacheBlocks set => SetValue(CacheBlocksProperty, value); } + public CacheBlock? HoveredCacheBlock + { + get => GetValue(HoveredCacheBlockProperty); + set => SetValue(HoveredCacheBlockProperty, value); + } + protected override void OnLoaded(RoutedEventArgs e) { base.OnLoaded(e); @@ -180,14 +190,30 @@ public override void Render(DrawingContext context) if (CacheBlocks != null) { + TimeSpan left = originX.ToTimeSpan(Scale); + TimeSpan right = l.ToTimeSpan(Scale); + foreach (CacheBlock item in CacheBlocks) { + TimeSpan end = item.Start + item.Length; + if (end < left || item.Start > right) + { + continue; + } + context.DrawRectangle( TimelineSharedObject.CacheBlockFillBrush, null, new RoundedRect(new Rect(item.Start.ToPixel(Scale), Height - 4, item.Length.ToPixel(Scale), 4))); } } + if (HoveredCacheBlock is { } hover) + { + context.DrawRectangle( + TimelineSharedObject.CacheBlockFillBrush, null, + new RoundedRect(new Rect(hover.Start.ToPixel(Scale), Height - 6, hover.Length.ToPixel(Scale), 6))); + } + if (BufferEnd != BufferStart) { context.DrawRectangle( From a58c623763e78747f9e01b17a5fceaa10c19e3a2 Mon Sep 17 00:00:00 2001 From: indigo-san Date: Sun, 28 Jan 2024 18:14:50 +0900 Subject: [PATCH 08/25] =?UTF-8?q?=E3=82=AD=E3=83=A3=E3=83=83=E3=82=B7?= =?UTF-8?q?=E3=83=A5=E3=82=92=E5=9B=BA=E5=AE=9A=E3=81=97=E3=81=A6=E8=87=AA?= =?UTF-8?q?=E5=8B=95=E3=81=A7=E5=89=8A=E9=99=A4=E3=81=95=E3=82=8C=E3=81=AA?= =?UTF-8?q?=E3=81=84=E3=82=88=E3=81=86=E3=81=AB=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Beutl/Models/FrameCacheManager.cs | 78 ++++++++--- src/Beutl/ViewModels/BufferStatusViewModel.cs | 10 +- src/Beutl/ViewModels/TimelineViewModel.cs | 67 ++++++--- src/Beutl/Views/GraphEditorView.axaml | 5 + src/Beutl/Views/Timeline.axaml | 17 ++- src/Beutl/Views/Timeline.axaml.cs | 36 ++++- src/Beutl/Views/TimelineOverlay.cs | 56 ++++++-- src/Beutl/Views/TimelineScale.cs | 128 +++++++++++++----- 8 files changed, 310 insertions(+), 87 deletions(-) diff --git a/src/Beutl/Models/FrameCacheManager.cs b/src/Beutl/Models/FrameCacheManager.cs index e9c61f096..ecb9eaf58 100644 --- a/src/Beutl/Models/FrameCacheManager.cs +++ b/src/Beutl/Models/FrameCacheManager.cs @@ -1,9 +1,7 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; using Beutl.Configuration; -using Beutl.Graphics; using Beutl.Media; using Beutl.Media.Pixel; using Beutl.Media.Source; @@ -21,7 +19,7 @@ public sealed class FrameCacheManager : IDisposable public event Action? Added; public event Action? Removed; - public event Action>? BlocksUpdated; + public event Action>? BlocksUpdated; public FrameCacheManager(PixelSize frameSize) { @@ -29,7 +27,7 @@ public FrameCacheManager(PixelSize frameSize) _maxSize = (ulong)(GlobalConfiguration.Instance.EditorConfig.FrameCacheMaxSize * 1024 * 1024); } - public ImmutableArray<(int Start, int Length)> Blocks { get; private set; } + public ImmutableArray Blocks { get; private set; } public void Add(int frame, Ref> bitmap) { @@ -37,7 +35,8 @@ public void Add(int frame, Ref> bitmap) { if (_entries.TryGetValue(frame, out CacheEntry? old)) { - old.SetBitmap(bitmap); + if (!old.IsLocked) + old.SetBitmap(bitmap); } else { @@ -74,7 +73,8 @@ public bool RemoveRange(int start, int end) { lock (_lock) { - int[] keys = _entries.Select(p => p.Key) + int[] keys = _entries.Where(v => !v.Value.IsLocked) + .Select(p => p.Key) .SkipWhile(t => t < start) .TakeWhile(t => t < end) .ToArray(); @@ -95,6 +95,42 @@ public bool RemoveRange(int start, int end) } } + public void Lock(int start, int end) + { + lock (_lock) + { + foreach (KeyValuePair item in _entries + .SkipWhile(t => t.Key < start) + .TakeWhile(t => t.Key < end)) + { + if (!item.Value.IsLocked) + { + _size -= (uint)item.Value.ByteCount; + } + + item.Value.IsLocked = true; + } + } + } + + public void Unlock(int start, int end) + { + lock (_lock) + { + foreach (KeyValuePair item in _entries + .SkipWhile(t => t.Key < start) + .TakeWhile(t => t.Key < end)) + { + if (item.Value.IsLocked) + { + _size += (uint)item.Value.ByteCount; + } + + item.Value.IsLocked = false; + } + } + } + public void RemoveAndUpdateBlocks(IEnumerable<(int Start, int End)> timeRanges) { lock (_lock) @@ -114,6 +150,11 @@ public void RemoveAndUpdateBlocks(IEnumerable<(int Start, int End)> timeRanges) } public void Dispose() + { + Clear(); + } + + public void Clear() { lock (_lock) { @@ -126,39 +167,43 @@ public void Dispose() _size = 0; _entries.Clear(); Removed?.Invoke(keys); + BlocksUpdated?.Invoke([]); } } // |oxxxxoooxoxxxo| // GetBlock() -> [(1, 4), (8, 1), (10, 4)] - public ImmutableArray<(int Start, int Length)> CalculateBlocks() + public ImmutableArray CalculateBlocks() { lock (_lock) { - var list = new List<(int Start, int Length)>(); + var list = new List(); int start = -1; int expect = 0; int count = 0; - foreach (int key in _entries.Keys) + bool isLocked = false; + foreach ((int key, CacheEntry item) in _entries) { if (start == -1) { start = key; + isLocked = item.IsLocked; expect = key; } - if (expect == key) + if (expect == key && isLocked == item.IsLocked) { count++; expect = key + 1; } else { - list.Add((start, count)); + list.Add(new(start, count, isLocked)); start = -1; count = 0; start = key; + isLocked = item.IsLocked; expect = key + 1; count++; } @@ -166,7 +211,7 @@ public void Dispose() if (start != -1) { - list.Add((start, count)); + list.Add(new(start, count, isLocked)); } return [.. list]; @@ -193,6 +238,7 @@ private void Optimize() var targetCount = excess / (ulong)sizePerCache; var items = _entries + .Where(v => !v.Value.IsLocked) .OrderBy(v => v.Value.LastAccessTime) .Take((int)targetCount) .ToArray(); @@ -211,17 +257,15 @@ private void Optimize() } } + public record CacheBlock(int Start, int Length, bool IsLocked); + private class CacheEntry : IDisposable { - private readonly int _width; - private readonly int _height; private Ref> _bitmap; public CacheEntry(Ref> bitmap) { _bitmap = bitmap.Clone(); - _width = _bitmap.Value.Width; - _height = _bitmap.Value.Height; ByteCount = bitmap.Value.ByteCount; LastAccessTime = DateTime.UtcNow; } @@ -230,6 +274,8 @@ public CacheEntry(Ref> bitmap) public int ByteCount { get; } + public bool IsLocked { get; set; } + public void SetBitmap(Ref> bitmap) { _bitmap?.Dispose(); diff --git a/src/Beutl/ViewModels/BufferStatusViewModel.cs b/src/Beutl/ViewModels/BufferStatusViewModel.cs index debacb0fa..732e45b95 100644 --- a/src/Beutl/ViewModels/BufferStatusViewModel.cs +++ b/src/Beutl/ViewModels/BufferStatusViewModel.cs @@ -2,6 +2,8 @@ using Avalonia.Threading; +using Beutl.Models; + using Reactive.Bindings; namespace Beutl.ViewModels; @@ -30,12 +32,12 @@ public BufferStatusViewModel(EditViewModel editViewModel) _disposables.Add(Disposable.Create(editViewModel.FrameCacheManager, m => m.BlocksUpdated -= OnFrameCacheManagerBlocksUpdated)); } - private void OnFrameCacheManagerBlocksUpdated(ImmutableArray<(int Start, int Length)> obj) + private void OnFrameCacheManagerBlocksUpdated(ImmutableArray obj) { _cts?.Cancel(); _cts = new CancellationTokenSource(); Dispatcher.UIThread.InvokeAsync( - () => CacheBlocks.Value = obj.SelectArray(v => new CacheBlock(_editViewModel.Player.GetFrameRate(), v.Start, v.Length)), + () => CacheBlocks.Value = obj.SelectArray(v => new CacheBlock(_editViewModel.Player.GetFrameRate(), v.Start, v.Length, v.IsLocked)), DispatcherPriority.Background, _cts.Token); } @@ -50,7 +52,7 @@ private void OnFrameCacheManagerBlocksUpdated(ImmutableArray<(int Start, int Len public ReactivePropertySlim CacheBlocks { get; } = new([]); - public sealed class CacheBlock(int rate, int start, int length) + public sealed class CacheBlock(int rate, int start, int length, bool isLocked) { public TimeSpan Start { get; } = TimeSpanExtensions.ToTimeSpan(start, rate); @@ -59,6 +61,8 @@ public sealed class CacheBlock(int rate, int start, int length) public int StartFrame { get; } = start; public int LengthFrame { get; } = length; + + public bool IsLocked { get; } = isLocked; } public void Dispose() diff --git a/src/Beutl/ViewModels/TimelineViewModel.cs b/src/Beutl/ViewModels/TimelineViewModel.cs index d839e4e39..b135c3f60 100644 --- a/src/Beutl/ViewModels/TimelineViewModel.cs +++ b/src/Beutl/ViewModels/TimelineViewModel.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Reactive.Subjects; using System.Text.Json.Nodes; +using System.Windows.Input; using Avalonia; using Avalonia.Input; @@ -28,6 +29,8 @@ using Reactive.Bindings; using Reactive.Bindings.Extensions; +using static Beutl.ViewModels.BufferStatusViewModel; + namespace Beutl.ViewModels; public interface ITimelineOptionsProvider @@ -53,6 +56,7 @@ public TimelineViewModel(EditViewModel editViewModel) EditorContext = editViewModel; Scene = editViewModel.Scene; Player = editViewModel.Player; + FrameSelectionRange = new FrameSelectionRange(editViewModel.Scale).DisposeWith(_disposables); PanelWidth = Scene.GetObservable(Scene.DurationProperty) .CombineLatest(editViewModel.Scale) .Select(item => item.First.ToPixel(item.Second)) @@ -126,25 +130,17 @@ public TimelineViewModel(EditViewModel editViewModel) AutoAdjustSceneDuration = editorConfig.GetObservable(EditorConfig.AutoAdjustSceneDurationProperty).ToReactiveProperty(); AutoAdjustSceneDuration.Subscribe(b => editorConfig.AutoAdjustSceneDuration = b); + IsLockCacheButtonEnabled = HoveredCacheBlock.Select(v => v is { IsLocked: false }) + .ToReadOnlyReactivePropertySlim() + .DisposeWith(_disposables); + + IsUnlockCacheButtonEnabled = HoveredCacheBlock.Select(v => v is { IsLocked: true }) + .ToReadOnlyReactivePropertySlim() + .DisposeWith(_disposables); + // Todo: 設定からショートカットを変更できるようにする。 KeyBindings = []; - PlatformHotkeyConfiguration? keyConf = Application.Current?.PlatformSettings?.HotkeyConfiguration; - if (keyConf != null) - { - KeyBindings.AddRange(keyConf.Paste.Select(i => new KeyBinding - { - Command = Paste, - Gesture = i - })); - } - else - { - KeyBindings.Add(new KeyBinding - { - Command = Paste, - Gesture = new KeyGesture(Key.V, keyConf?.CommandModifiers ?? KeyModifiers.Control) - }); - } + ConfigureKeyBindings(); } private void OnAdjustDurationToPointer() @@ -205,6 +201,14 @@ private void OnAdjustDurationToCurrent() public ReactiveProperty AutoAdjustSceneDuration { get; } + public ReactivePropertySlim HoveredCacheBlock { get; } = new(); + + public ReadOnlyReactivePropertySlim IsLockCacheButtonEnabled { get; } + + public ReadOnlyReactivePropertySlim IsUnlockCacheButtonEnabled { get; } + + public FrameSelectionRange FrameSelectionRange { get; } + public TimeSpan ClickedFrame { get; set; } public Point ClickedPosition { get; set; } @@ -598,6 +602,35 @@ internal void RaiseLayerHeightChanged(LayerHeaderViewModel value) return Elements.FirstOrDefault(x => x.Model == element); } + // Todo: 設定からショートカットを変更できるようにする。 + private void ConfigureKeyBindings() + { + static KeyBinding KeyBinding(Key key, KeyModifiers modifiers, ICommand command) + { + return new KeyBinding + { + Gesture = new KeyGesture(key, modifiers), + Command = command + }; + } + + PlatformHotkeyConfiguration? keyConf = Application.Current?.PlatformSettings?.HotkeyConfiguration; + if (keyConf != null) + { + KeyBindings.AddRange(keyConf.Paste.Select(i => new KeyBinding + { + Command = Paste, + Gesture = i + })); + } + else + { + KeyBindings.Add(KeyBinding(Key.V, keyConf?.CommandModifiers ?? KeyModifiers.Control, Paste)); + } + + //KeyBindings.Add(KeyBinding(Key.)) + } + private sealed class TrackedLayerTopObservable(int layerNum, TimelineViewModel timeline) : LightweightObservableBase, IDisposable { private IDisposable? _disposable1; diff --git a/src/Beutl/Views/GraphEditorView.axaml b/src/Beutl/Views/GraphEditorView.axaml index e0d890848..1e78ce437 100644 --- a/src/Beutl/Views/GraphEditorView.axaml +++ b/src/Beutl/Views/GraphEditorView.axaml @@ -36,8 +36,11 @@ Height="32" HorizontalAlignment="Stretch" VerticalAlignment="Top" + EndingBarBrush="{DynamicResource SystemFillColorCriticalBrush}" EndingBarMargin="{Binding EndingBarMargin.Value}" Scale="{Binding Options.Value.Scale}" + ScaleBrush="{DynamicResource TextControlForeground}" + SeekBarBrush="{DynamicResource AccentFillColorDefaultBrush}" SeekBarMargin="{Binding SeekBarMargin.Value}" Offset="{Binding #scroll.Offset, Mode=OneWay}" /> @@ -286,7 +289,9 @@ + + @@ -161,7 +174,9 @@ new TimeRange(v.Start, v.Length).Contains(_pointerFrame)); + viewModel.HoveredCacheBlock.Value = Array.Find(cacheBlocks, v => new TimeRange(v.Start, v.Length).Contains(_pointerFrame)); } else { - Scale.HoveredCacheBlock = null; + viewModel.HoveredCacheBlock.Value = null; } } } @@ -489,7 +489,7 @@ private void TimelinePanel_PointerExited(object? sender, PointerEventArgs e) if (!_rightButtonPressed) { - Scale.HoveredCacheBlock = null; + ViewModel.HoveredCacheBlock.Value = null; } } @@ -740,10 +740,38 @@ private async void ScrollTimelinePosition(TimeRange range, int zindex) private void DeleteFrameCacheClick(object? sender, RoutedEventArgs e) { - if (Scale.HoveredCacheBlock is { } block) + if (ViewModel.HoveredCacheBlock.Value is { } block) { + if (block.IsLocked) + { + ViewModel.EditorContext.FrameCacheManager.Unlock( + block.StartFrame, block.StartFrame + block.LengthFrame); + } + ViewModel.EditorContext.FrameCacheManager.RemoveAndUpdateBlocks( new[] { (block.StartFrame, block.StartFrame + block.LengthFrame) }); } } + + private void LockFrameCacheClick(object? sender, RoutedEventArgs e) + { + if (ViewModel.HoveredCacheBlock.Value is { } block) + { + ViewModel.EditorContext.FrameCacheManager.Lock( + block.StartFrame, block.StartFrame + block.LengthFrame); + + ViewModel.EditorContext.FrameCacheManager.UpdateBlocks(); + } + } + + private void UnlockFrameCacheClick(object? sender, RoutedEventArgs e) + { + if (ViewModel.HoveredCacheBlock.Value is { } block) + { + ViewModel.EditorContext.FrameCacheManager.Unlock( + block.StartFrame, block.StartFrame + block.LengthFrame); + + ViewModel.EditorContext.FrameCacheManager.UpdateBlocks(); + } + } } diff --git a/src/Beutl/Views/TimelineOverlay.cs b/src/Beutl/Views/TimelineOverlay.cs index 7c2246040..0ffc69e10 100644 --- a/src/Beutl/Views/TimelineOverlay.cs +++ b/src/Beutl/Views/TimelineOverlay.cs @@ -7,18 +7,11 @@ namespace Beutl.Views; public static class TimelineSharedObject { - public static readonly IPen RedPen; - public static readonly IPen BluePen; public static readonly IPen SelectionPen; public static readonly IBrush SelectionFillBrush = new ImmutableSolidColorBrush(Colors.CornflowerBlue, 0.3); - public static readonly IBrush BufferRangeFillBrush = new ImmutableSolidColorBrush(Colors.SkyBlue); - public static readonly IBrush CacheBlockFillBrush = new ImmutableSolidColorBrush(Colors.LightGreen); - public static readonly IBrush DropFrameFillBrush = new ImmutableSolidColorBrush(Colors.Orange); static TimelineSharedObject() { - RedPen = new ImmutablePen(Brushes.Red, 1.25); - BluePen = new ImmutablePen(Brushes.Blue, 1.25); SelectionPen = new ImmutablePen(Brushes.CornflowerBlue, 0.5); } } @@ -45,15 +38,30 @@ public static readonly DirectProperty SeekBarMarginP = AvaloniaProperty.RegisterDirect( nameof(SeekBarMargin), o => o.SeekBarMargin, (o, v) => o.SeekBarMargin = v); + public static readonly StyledProperty SeekBarBrushProperty + = AvaloniaProperty.Register(nameof(SeekBarBrush)); + + public static readonly StyledProperty EndingBarBrushProperty + = AvaloniaProperty.Register(nameof(EndingBarBrush)); + private Vector _offset; private Thickness _endingBarMargin; private Thickness _seekBarMargin; private Size _viewport; private Rect _selectionRange; + private ImmutablePen? _seekBarPen; + private ImmutablePen? _endingBarPen; static TimelineOverlay() { - AffectsRender(OffsetProperty, ViewportProperty, SelectionRangeProperty, EndingBarMarginProperty, SeekBarMarginProperty); + AffectsRender( + OffsetProperty, + ViewportProperty, + SelectionRangeProperty, + EndingBarMarginProperty, + SeekBarMarginProperty, + SeekBarBrushProperty, + EndingBarBrushProperty); } public TimelineOverlay() @@ -91,9 +99,37 @@ public Thickness SeekBarMargin set => SetAndRaise(SeekBarMarginProperty, ref _seekBarMargin, value); } + public IBrush? SeekBarBrush + { + get => GetValue(SeekBarBrushProperty); + set => SetValue(SeekBarBrushProperty, value); + } + + public IBrush? EndingBarBrush + { + get => GetValue(EndingBarBrushProperty); + set => SetValue(EndingBarBrushProperty, value); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == SeekBarBrushProperty) + { + _seekBarPen = new ImmutablePen(SeekBarBrush?.ToImmutable(), 1.25); + } + else if (change.Property == EndingBarBrushProperty) + { + _endingBarPen = new ImmutablePen(EndingBarBrush?.ToImmutable(), 1.25); + } + } + public override void Render(DrawingContext context) { base.Render(context); + _seekBarPen ??= new ImmutablePen(SeekBarBrush?.ToImmutable(), 1.25); + _endingBarPen ??= new ImmutablePen(EndingBarBrush?.ToImmutable(), 1.25); + Rect rect = _selectionRange.Normalize(); context.FillRectangle(TimelineSharedObject.SelectionFillBrush, rect); @@ -106,8 +142,8 @@ public override void Render(DrawingContext context) var endingbar = new Point(_endingBarMargin.Left, 0); var bottom = new Point(0, height); - context.DrawLine(TimelineSharedObject.RedPen, seekbar, seekbar + bottom); - context.DrawLine(TimelineSharedObject.BluePen, endingbar, endingbar + bottom); + context.DrawLine(_seekBarPen, seekbar, seekbar + bottom); + context.DrawLine(_endingBarPen, endingbar, endingbar + bottom); } } } diff --git a/src/Beutl/Views/TimelineScale.cs b/src/Beutl/Views/TimelineScale.cs index 4ce81e39c..9c4841e09 100644 --- a/src/Beutl/Views/TimelineScale.cs +++ b/src/Beutl/Views/TimelineScale.cs @@ -1,11 +1,8 @@ -using System.Collections.Immutable; - -using Avalonia; +using Avalonia; using Avalonia.Controls; -using Avalonia.Interactivity; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Media.TextFormatting; -using Avalonia.Threading; using static Beutl.ViewModels.BufferStatusViewModel; @@ -43,14 +40,32 @@ public static readonly StyledProperty CacheBlocksProperty public static readonly StyledProperty HoveredCacheBlockProperty = AvaloniaProperty.Register(nameof(HoveredCacheBlock)); + public static readonly StyledProperty ScaleBrushProperty + = AvaloniaProperty.Register(nameof(ScaleBrush)); + + public static readonly StyledProperty SeekBarBrushProperty + = AvaloniaProperty.Register(nameof(SeekBarBrush)); + + public static readonly StyledProperty EndingBarBrushProperty + = AvaloniaProperty.Register(nameof(EndingBarBrush)); + + public static readonly StyledProperty CacheBlockBrushProperty + = AvaloniaProperty.Register(nameof(CacheBlockBrush)); + + public static readonly StyledProperty LockedCacheBlockBrushProperty + = AvaloniaProperty.Register(nameof(LockedCacheBlockBrush)); + + public static readonly StyledProperty BufferBrushProperty + = AvaloniaProperty.Register(nameof(BufferBrush)); + private static readonly Typeface s_typeface = new(FontFamily.Default, FontStyle.Normal, FontWeight.Medium); - private readonly Pen _pen; - private IBrush _brush = Brushes.White; private float _scale = 1; private Vector _offset; private Thickness _endingBarMargin; private Thickness _seekBarMargin; - private IDisposable? _disposable; + private ImmutablePen? _pen; + private ImmutablePen? _seekBarPen; + private ImmutablePen? _endingBarPen; static TimelineScale() { @@ -62,13 +77,18 @@ static TimelineScale() BufferStartProperty, BufferEndProperty, CacheBlocksProperty, - HoveredCacheBlockProperty); + HoveredCacheBlockProperty, + ScaleBrushProperty, + SeekBarBrushProperty, + EndingBarBrushProperty, + CacheBlockBrushProperty, + LockedCacheBlockBrushProperty, + BufferBrushProperty); } public TimelineScale() { ClipToBounds = true; - _pen = new Pen(_brush, 1); } public float Scale @@ -119,29 +139,65 @@ public CacheBlock? HoveredCacheBlock set => SetValue(HoveredCacheBlockProperty, value); } - protected override void OnLoaded(RoutedEventArgs e) + public IBrush? ScaleBrush { - base.OnLoaded(e); - _disposable = this.GetResourceObservable("TextControlForeground").Subscribe(b => - { - if (b is IBrush brush) - { - _brush = brush; - _pen.Brush = brush; - InvalidateVisual(); - } - }); + get => GetValue(ScaleBrushProperty); + set => SetValue(ScaleBrushProperty, value); + } + + public IBrush? SeekBarBrush + { + get => GetValue(SeekBarBrushProperty); + set => SetValue(SeekBarBrushProperty, value); + } + + public IBrush? EndingBarBrush + { + get => GetValue(EndingBarBrushProperty); + set => SetValue(EndingBarBrushProperty, value); + } + + public IBrush? CacheBlockBrush + { + get => GetValue(CacheBlockBrushProperty); + set => SetValue(CacheBlockBrushProperty, value); + } + + public IBrush? LockedCacheBlockBrush + { + get => GetValue(LockedCacheBlockBrushProperty); + set => SetValue(LockedCacheBlockBrushProperty, value); } - protected override void OnUnloaded(RoutedEventArgs e) + public IBrush? BufferBrush { - base.OnUnloaded(e); - _disposable?.Dispose(); + get => GetValue(BufferBrushProperty); + set => SetValue(BufferBrushProperty, value); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == ScaleBrushProperty) + { + _pen = new ImmutablePen(ScaleBrush?.ToImmutable(), 1); + } + else if (change.Property == SeekBarBrushProperty) + { + _seekBarPen = new ImmutablePen(SeekBarBrush?.ToImmutable(), 1.25); + } + else if (change.Property == EndingBarBrushProperty) + { + _endingBarPen = new ImmutablePen(EndingBarBrush?.ToImmutable(), 1.25); + } } public override void Render(DrawingContext context) { base.Render(context); + _pen ??= new ImmutablePen(ScaleBrush?.ToImmutable(), 1); + _seekBarPen ??= new ImmutablePen(SeekBarBrush?.ToImmutable(), 1.25); + _endingBarPen ??= new ImmutablePen(EndingBarBrush?.ToImmutable(), 1.25); const int top = 16; @@ -168,7 +224,7 @@ public override void Render(DrawingContext context) context.DrawLine(_pen, new(x, 5), new(x, height)); } - using var text = new TextLayout(time.ToString("hh\\:mm\\:ss\\.ff"), s_typeface, 13, _brush); + using var text = new TextLayout(time.ToString("hh\\:mm\\:ss\\.ff"), s_typeface, 13, ScaleBrush); var textbounds = new Rect(x + 8, 0, text.Width, text.Height); if (viewport.Intersects(textbounds) && (recentPix == 0d || (x + 8) > recentPix)) @@ -188,6 +244,13 @@ public override void Render(DrawingContext context) } } + if (BufferEnd != BufferStart) + { + context.DrawRectangle( + BufferBrush, null, + new RoundedRect(new Rect(BufferStart, Height - 4, BufferEnd - BufferStart, 4))); + } + if (CacheBlocks != null) { TimeSpan left = originX.ToTimeSpan(Scale); @@ -202,7 +265,7 @@ public override void Render(DrawingContext context) } context.DrawRectangle( - TimelineSharedObject.CacheBlockFillBrush, null, + item.IsLocked ? LockedCacheBlockBrush : CacheBlockBrush, null, new RoundedRect(new Rect(item.Start.ToPixel(Scale), Height - 4, item.Length.ToPixel(Scale), 4))); } } @@ -210,25 +273,18 @@ public override void Render(DrawingContext context) if (HoveredCacheBlock is { } hover) { context.DrawRectangle( - TimelineSharedObject.CacheBlockFillBrush, null, + hover.IsLocked ? LockedCacheBlockBrush : CacheBlockBrush, null, new RoundedRect(new Rect(hover.Start.ToPixel(Scale), Height - 6, hover.Length.ToPixel(Scale), 6))); } - if (BufferEnd != BufferStart) - { - context.DrawRectangle( - TimelineSharedObject.BufferRangeFillBrush, null, - new RoundedRect(new Rect(BufferStart, Height - 4, BufferEnd - BufferStart, 4))); - } - var size = new Size(1.25, height); var seekbar = new Point(_seekBarMargin.Left, 0); var endingbar = new Point(_endingBarMargin.Left, 0); var bottom = new Point(0, height); - context.DrawLine(TimelineSharedObject.RedPen, seekbar, seekbar + bottom); + context.DrawLine(_seekBarPen, seekbar, seekbar + bottom); - context.DrawLine(TimelineSharedObject.BluePen, endingbar, endingbar + bottom); + context.DrawLine(_endingBarPen, endingbar, endingbar + bottom); } } } From 6654ff453fdd052d5c1d4066c35a43f473ac0414 Mon Sep 17 00:00:00 2001 From: indigo-san Date: Sun, 28 Jan 2024 18:15:23 +0900 Subject: [PATCH 09/25] =?UTF-8?q?=E7=B7=A8=E9=9B=86=E6=99=82=E3=81=AB?= =?UTF-8?q?=E3=82=AD=E3=83=A3=E3=83=83=E3=82=B7=E3=83=A5=E3=81=8C=E7=84=A1?= =?UTF-8?q?=E5=8A=B9=E3=81=AB=E3=81=AA=E3=82=89=E3=81=AA=E3=81=84=E3=81=AE?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModels/Editors/BaseEditorViewModel.cs | 19 +++++++++++++++++++ .../Tools/SourceOperatorViewModel.cs | 11 ++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/Beutl/ViewModels/Editors/BaseEditorViewModel.cs b/src/Beutl/ViewModels/Editors/BaseEditorViewModel.cs index de138e19f..f83cdcb0f 100644 --- a/src/Beutl/ViewModels/Editors/BaseEditorViewModel.cs +++ b/src/Beutl/ViewModels/Editors/BaseEditorViewModel.cs @@ -8,6 +8,7 @@ using Beutl.Animation; using Beutl.Animation.Easings; using Beutl.Controls.PropertyEditors; +using Beutl.Media; using Beutl.Operation; using Beutl.ProjectSystem; @@ -325,14 +326,32 @@ public void SetValue(T? oldValue, T? newValue) public T? SetCurrentValueAndGetCoerced(T? value) { + void InvalidateCache() + { + if (this.GetService() is { Player: { } player, FrameCacheManager: { } cacheManager }) + { + Task.Run(() => + { + int rate = player.GetFrameRate(); + ImmutableArray storables = GetStorables(); + IEnumerable affectedRange = storables.OfType().Select(v => v.Range); + + cacheManager.RemoveAndUpdateBlocks(affectedRange + .Select(item => (Start: (int)item.Start.ToFrameNumber(rate), End: (int)Math.Ceiling(item.End.ToFrameNumber(rate))))); + }); + } + } + if (EditingKeyFrame.Value != null) { EditingKeyFrame.Value.Value = value!; + InvalidateCache(); return EditingKeyFrame.Value.Value; } else { WrappedProperty.SetValue(value); + InvalidateCache(); return WrappedProperty.GetValue(); } } diff --git a/src/Beutl/ViewModels/Tools/SourceOperatorViewModel.cs b/src/Beutl/ViewModels/Tools/SourceOperatorViewModel.cs index 734206e90..6f506118b 100644 --- a/src/Beutl/ViewModels/Tools/SourceOperatorViewModel.cs +++ b/src/Beutl/ViewModels/Tools/SourceOperatorViewModel.cs @@ -26,7 +26,16 @@ public SourceOperatorViewModel(SourceOperator model, SourceOperatorsTabViewModel _parent = parent; IsEnabled = model.GetObservable(SourceOperator.IsEnabledProperty) .ToReactiveProperty(); - IsEnabled.Subscribe(v => Model.IsEnabled = v); + IsEnabled.Skip(1).Subscribe(v => + { + CommandRecorder? recorder = this.GetService(); + if (recorder!=null) + { + RecordableCommands.Edit(Model, SourceOperator.IsEnabledProperty, v) + .WithStoables([parent.Element.Value]) + .DoAndRecord(recorder); + } + }); Init(); From 094a1f29af45a36b9192d200ad3daec6acc3b3f7 Mon Sep 17 00:00:00 2001 From: indigo-san Date: Sun, 28 Jan 2024 19:06:32 +0900 Subject: [PATCH 10/25] =?UTF-8?q?=E3=82=AD=E3=83=A3=E3=83=83=E3=82=B7?= =?UTF-8?q?=E3=83=A5=E7=AF=84=E5=9B=B2=E3=82=92=E5=B7=A6=E3=82=AF=E3=83=AA?= =?UTF-8?q?=E3=83=83=E3=82=AF=E3=81=A7Tip=E3=82=92=E8=A1=A8=E7=A4=BA?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Beutl.Controls/Styles.axaml | 4 ++-- src/Beutl/Views/Timeline.axaml | 5 +++++ src/Beutl/Views/Timeline.axaml.cs | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Beutl.Controls/Styles.axaml b/src/Beutl.Controls/Styles.axaml index 7411406e2..837ef1b20 100644 --- a/src/Beutl.Controls/Styles.axaml +++ b/src/Beutl.Controls/Styles.axaml @@ -74,11 +74,11 @@ - + -->