diff --git a/src/Beutl.Engine/Graphics/Drawable.cs b/src/Beutl.Engine/Graphics/Drawable.cs index abccc7efd..c598b4570 100644 --- a/src/Beutl.Engine/Graphics/Drawable.cs +++ b/src/Beutl.Engine/Graphics/Drawable.cs @@ -194,11 +194,11 @@ internal Matrix GetTransformMatrix(Size availableSize, Size coreBounds) } } - public virtual void Render(ICanvas canvas) + public virtual void Render(GraphicsContext2D context) { if (IsVisible) { - Size availableSize = canvas.Size.ToSize(1); + Size availableSize = context.Size.ToSize(1); Size size = MeasureCore(availableSize); var rect = new Rect(size); if (_filterEffect != null && !rect.IsInvalid) @@ -208,13 +208,13 @@ public virtual void Render(ICanvas canvas) Matrix transform = GetTransformMatrix(availableSize, size); Rect transformedBounds = rect.IsInvalid ? Rect.Invalid : rect.TransformToAABB(transform); - using (canvas.PushBlendMode(BlendMode)) - using (canvas.PushTransform(transform)) - using (canvas.PushOpacity(Opacity / 100f)) - using (_filterEffect == null ? new() : canvas.PushFilterEffect(_filterEffect)) - using (OpacityMask == null ? new() : canvas.PushOpacityMask(OpacityMask, new Rect(size))) + using (context.PushBlendMode(BlendMode)) + using (context.PushTransform(transform)) + using (context.PushOpacity(Opacity / 100f)) + using (_filterEffect == null ? new() : context.PushFilterEffect(_filterEffect)) + using (OpacityMask == null ? new() : context.PushOpacityMask(OpacityMask, new Rect(size))) { - OnDraw(canvas); + OnDraw(context); } Bounds = transformedBounds; @@ -230,7 +230,7 @@ public override void ApplyAnimations(IClock clock) (OpacityMask as Animatable)?.ApplyAnimations(clock); } - protected abstract void OnDraw(ICanvas canvas); + protected abstract void OnDraw(GraphicsContext2D context); private Point CalculateTranslate(Size bounds, Size canvasSize) { diff --git a/src/Beutl.Engine/Graphics/DrawableDecorator.cs b/src/Beutl.Engine/Graphics/DrawableDecorator.cs index cf2dbdba0..e5acda9b8 100644 --- a/src/Beutl.Engine/Graphics/DrawableDecorator.cs +++ b/src/Beutl.Engine/Graphics/DrawableDecorator.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using Beutl.Graphics.Effects; +using Beutl.Graphics.Rendering; using Beutl.Media; using Beutl.Media.Immutable; @@ -60,11 +61,11 @@ private Rect PrivateMeasureCore(Size availableSize) } } - public override void Render(ICanvas canvas) + public override void Render(GraphicsContext2D context) { if (IsVisible) { - Size availableSize = canvas.Size.ToSize(1); + Size availableSize = context.Size.ToSize(1); Rect rect = PrivateMeasureCore(availableSize); if (FilterEffect != null && !rect.IsInvalid) { @@ -73,23 +74,23 @@ public override void Render(ICanvas canvas) Matrix transform = GetTransformMatrix(availableSize); Rect transformedBounds = rect.IsInvalid ? Rect.Invalid : rect.TransformToAABB(transform); - using (canvas.PushBlendMode(BlendMode)) - using (canvas.PushTransform(transform)) - using (FilterEffect == null ? new() : canvas.PushFilterEffect(FilterEffect)) - using (OpacityMask == null ? new() : canvas.PushOpacityMask(OpacityMask, new Rect(rect.Size))) + using (context.PushBlendMode(BlendMode)) + using (context.PushTransform(transform)) + using (FilterEffect == null ? new() : context.PushFilterEffect(FilterEffect)) + using (OpacityMask == null ? new() : context.PushOpacityMask(OpacityMask, new Rect(rect.Size))) { - OnDraw(canvas); + OnDraw(context); } Bounds = transformedBounds; } } - protected override void OnDraw(ICanvas canvas) + protected override void OnDraw(GraphicsContext2D context) { if (Child != null) { - canvas.DrawDrawable(Child); + context.DrawDrawable(Child); } } diff --git a/src/Beutl.Engine/Graphics/DrawableGroup.cs b/src/Beutl.Engine/Graphics/DrawableGroup.cs index 96a8e1544..4d3ac315f 100644 --- a/src/Beutl.Engine/Graphics/DrawableGroup.cs +++ b/src/Beutl.Engine/Graphics/DrawableGroup.cs @@ -1,5 +1,6 @@ using System.Text.Json.Nodes; using Beutl.Graphics.Effects; +using Beutl.Graphics.Rendering; using Beutl.Serialization; namespace Beutl.Graphics; @@ -70,11 +71,11 @@ private Rect PrivateMeasureCore(Size availableSize) return rect; } - public override void Render(ICanvas canvas) + public override void Render(GraphicsContext2D context) { if (IsVisible) { - Size availableSize = canvas.Size.ToSize(1); + Size availableSize = context.Size.ToSize(1); Rect rect = PrivateMeasureCore(availableSize); if (FilterEffect != null && !rect.IsInvalid) { @@ -84,25 +85,25 @@ public override void Render(ICanvas canvas) Matrix transform = GetTransformMatrix(availableSize); Rect transformedBounds = rect.IsInvalid ? Rect.Invalid : rect.TransformToAABB(transform); - using (canvas.PushBlendMode(BlendMode)) - using (canvas.PushLayer(transformedBounds.IsInvalid ? default : transformedBounds)) - using (canvas.PushTransform(transform)) - using (FilterEffect == null ? new() : canvas.PushFilterEffect(FilterEffect)) - using (OpacityMask == null ? new() : canvas.PushOpacityMask(OpacityMask, new Rect(rect.Size))) - using (canvas.PushLayer()) + using (context.PushBlendMode(BlendMode)) + using (context.PushLayer(transformedBounds.IsInvalid ? default : transformedBounds)) + using (context.PushTransform(transform)) + using (FilterEffect == null ? new() : context.PushFilterEffect(FilterEffect)) + using (OpacityMask == null ? new() : context.PushOpacityMask(OpacityMask, new Rect(rect.Size))) + using (context.PushLayer()) { - OnDraw(canvas); + OnDraw(context); } Bounds = transformedBounds; } } - protected override void OnDraw(ICanvas canvas) + protected override void OnDraw(GraphicsContext2D context) { foreach (Drawable item in _children.GetMarshal().Value) { - canvas.DrawDrawable(item); + context.DrawDrawable(item); } } diff --git a/src/Beutl.Engine/Graphics/DummyDrawable.cs b/src/Beutl.Engine/Graphics/DummyDrawable.cs index 16468bc89..ebf7a1b69 100644 --- a/src/Beutl.Engine/Graphics/DummyDrawable.cs +++ b/src/Beutl.Engine/Graphics/DummyDrawable.cs @@ -1,6 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Nodes; - +using Beutl.Graphics.Rendering; using Beutl.Serialization; namespace Beutl.Graphics; @@ -29,7 +29,7 @@ protected override Size MeasureCore(Size availableSize) return Size.Empty; } - protected override void OnDraw(ICanvas canvas) + protected override void OnDraw(GraphicsContext2D context) { } diff --git a/src/Beutl.Engine/Graphics/FilterEffects/CombinedFilterEffect.cs b/src/Beutl.Engine/Graphics/FilterEffects/CombinedFilterEffect.cs index 8e1a50130..ef9b726b0 100644 --- a/src/Beutl.Engine/Graphics/FilterEffects/CombinedFilterEffect.cs +++ b/src/Beutl.Engine/Graphics/FilterEffects/CombinedFilterEffect.cs @@ -1,8 +1,8 @@ -using System.ComponentModel; -using Beutl.Animation; +using Beutl.Animation; namespace Beutl.Graphics.Effects; +[Obsolete("Use FilterEffectGroup instead.")] public sealed class CombinedFilterEffect : FilterEffect { public static readonly CoreProperty FirstProperty; diff --git a/src/Beutl.Engine/Graphics/FilterEffects/CustomFilterEffectContext.cs b/src/Beutl.Engine/Graphics/FilterEffects/CustomFilterEffectContext.cs index 7193f3736..64353f5dd 100644 --- a/src/Beutl.Engine/Graphics/FilterEffects/CustomFilterEffectContext.cs +++ b/src/Beutl.Engine/Graphics/FilterEffects/CustomFilterEffectContext.cs @@ -1,7 +1,4 @@ -using System.Collections.Immutable; - -using Beutl.Media.Source; - +using Beutl.Media.Source; using SkiaSharp; namespace Beutl.Graphics.Effects; @@ -9,16 +6,11 @@ namespace Beutl.Graphics.Effects; public class CustomFilterEffectContext { internal readonly IImmediateCanvasFactory _factory; - internal readonly ImmutableArray _history; - internal CustomFilterEffectContext( - IImmediateCanvasFactory canvas, - EffectTargets targets, - ImmutableArray history) + internal CustomFilterEffectContext(IImmediateCanvasFactory canvas, EffectTargets targets) { Targets = targets; _factory = canvas; - _history = history; } public EffectTargets Targets { get; } @@ -65,17 +57,11 @@ public EffectTarget CreateTarget(Rect bounds) if (surface != null) { using var surfaceRef = Ref.Create(surface); - var obj = new EffectTarget(surfaceRef, bounds); - - obj._history.AddRange(_history); - return obj; + return new EffectTarget(surfaceRef, bounds); } else { - var obj = new EffectTarget(); - - obj._history.AddRange(_history); - return obj; + return new EffectTarget(); } } diff --git a/src/Beutl.Engine/Graphics/FilterEffects/EffectTarget.cs b/src/Beutl.Engine/Graphics/FilterEffects/EffectTarget.cs index 19a12d335..04eb4056c 100644 --- a/src/Beutl.Engine/Graphics/FilterEffects/EffectTarget.cs +++ b/src/Beutl.Engine/Graphics/FilterEffects/EffectTarget.cs @@ -1,9 +1,7 @@ using System.ComponentModel; - using Beutl.Collections.Pooled; using Beutl.Graphics.Rendering; using Beutl.Media.Source; - using SkiaSharp; namespace Beutl.Graphics.Effects; @@ -15,9 +13,7 @@ public sealed class EffectTarget : IDisposable private object? _target; - internal readonly PooledList _history = []; - - public EffectTarget(IGraphicNode node) + public EffectTarget(RenderNodeOperation node) { _target = node; OriginalBounds = node.Bounds; @@ -39,11 +35,11 @@ public EffectTarget() public Rect Bounds { get; set; } - [Obsolete()] + [Obsolete] [EditorBrowsable(EditorBrowsableState.Never)] public Size Size => Bounds.Size; - public IGraphicNode? Node => _target as IGraphicNode; + public RenderNodeOperation? NodeOperation => _target as RenderNodeOperation; public Ref? Surface => _target as Ref; @@ -51,18 +47,9 @@ public EffectTarget() public EffectTarget Clone() { - if (Node != null) - { - return this; - } - else if (Surface != null) + if (Surface != null) { - var obj = new EffectTarget(Surface, OriginalBounds) - { - Bounds = Bounds - }; - obj._history.AddRange(_history.Select(v => v.Inherit())); - return obj; + return new EffectTarget(Surface, OriginalBounds) { Bounds = Bounds }; } else { @@ -73,20 +60,20 @@ public EffectTarget Clone() public void Dispose() { Surface?.Dispose(); + NodeOperation?.Dispose(); _target = null; OriginalBounds = default; - _history.Dispose(); } public void Draw(ImmediateCanvas canvas) { - if (Node != null) + if (Surface != null) { - canvas.DrawNode(Node); + canvas.DrawSurface(Surface.Value, default); } - else if (Surface != null) + else if (NodeOperation != null) { - canvas.DrawSurface(Surface.Value, default); + NodeOperation.Render(canvas); } } } diff --git a/src/Beutl.Engine/Graphics/FilterEffects/FEImpl.cs b/src/Beutl.Engine/Graphics/FilterEffects/FEImpl.cs index d5765987b..2081a80ea 100644 --- a/src/Beutl.Engine/Graphics/FilterEffects/FEImpl.cs +++ b/src/Beutl.Engine/Graphics/FilterEffects/FEImpl.cs @@ -1,6 +1,4 @@ -using System.Collections.Immutable; - -using SkiaSharp; +using SkiaSharp; namespace Beutl.Graphics.Effects; @@ -53,7 +51,7 @@ public void Accepts(CustomFilterEffectContext context) { using (target) { - var innerContext = new FilterEffectCustomOperationContext(context._factory, target, [.. target._history]); + var innerContext = new FilterEffectCustomOperationContext(context._factory, target); Action.Invoke(Data, innerContext); innerContext.Target.Bounds = TransformBounds!(Data, innerContext.Target.Bounds); diff --git a/src/Beutl.Engine/Graphics/FilterEffects/FilterEffectActivator.cs b/src/Beutl.Engine/Graphics/FilterEffects/FilterEffectActivator.cs index 68024e84b..60fe6ce37 100644 --- a/src/Beutl.Engine/Graphics/FilterEffects/FilterEffectActivator.cs +++ b/src/Beutl.Engine/Graphics/FilterEffects/FilterEffectActivator.cs @@ -1,6 +1,4 @@ -using System.Diagnostics; - -using Beutl.Media.Source; +using Beutl.Media.Source; using SkiaSharp; @@ -8,11 +6,9 @@ namespace Beutl.Graphics.Effects; public sealed class FilterEffectActivator(EffectTargets targets, SKImageFilterBuilder builder, IImmediateCanvasFactory factory) : IDisposable { - private readonly IImmediateCanvasFactory _factory = factory; - public SKImageFilterBuilder Builder { get; } = builder; - public EffectTargets CurrentTargets { get; private set; } = targets; + public EffectTargets CurrentTargets { get; } = targets; public void Dispose() { @@ -22,21 +18,19 @@ public void Flush(bool force = true) { if (force || Builder.HasFilter() - || (CurrentTargets.Count == 1 && CurrentTargets[0].Node != null)) + || CurrentTargets is [{ NodeOperation: not null }]) { - using var paint = new SKPaint - { - ImageFilter = Builder.GetFilter(), - }; + using var paint = new SKPaint(); + paint.ImageFilter = Builder.GetFilter(); for (int i = 0; i < CurrentTargets.Count; i++) { EffectTarget target = CurrentTargets[i]; - SKSurface? surface = _factory.CreateRenderTarget((int)target.OriginalBounds.Width, (int)target.OriginalBounds.Height); + SKSurface? surface = factory.CreateRenderTarget((int)target.OriginalBounds.Width, (int)target.OriginalBounds.Height); if (surface != null) { - using ImmediateCanvas canvas = _factory.CreateCanvas(surface, true); + using ImmediateCanvas canvas = factory.CreateCanvas(surface, true); using (canvas.PushTransform(Matrix.CreateTranslation(-target.OriginalBounds.X, -target.OriginalBounds.Y))) using (canvas.PushPaint(paint)) @@ -49,7 +43,6 @@ public void Flush(bool force = true) { OriginalBounds = target.OriginalBounds }; - newTarget._history.AddRange(target._history.Select(v => v.Inherit())); CurrentTargets[i] = newTarget; target.Dispose(); } @@ -68,270 +61,94 @@ public void Flush(bool force = true) } // 最小単位である'IFEItem'の数がわからないので 'count'は'nullable' - public void Apply(FilterEffectContext context, int offset, int? count) + public void Apply(FilterEffectContext context) { if (CurrentTargets.Count == 0) return; - int takeCount; - if (count.HasValue) - { - takeCount = Math.Min(count.Value, context._items.Count); - } - else - { - takeCount = Math.Max(context._items.Count - offset, 0); - } - foreach (FEItemWrapper item in context._items.Skip(offset).Take(takeCount)) - { - if (item.Item is IFEItem_Skia skia) - { - skia.Accepts(this, Builder); - foreach (EffectTarget t in CurrentTargets) - { - t._history.Add(item); - t.Bounds = item.Item.TransformBounds(t.Bounds); - t.OriginalBounds = item.Item.TransformBounds(t.OriginalBounds); - } - } - else if (item.Item is IFEItem_Custom custom) - { - Flush(true); - if (CurrentTargets.Count == 0) return; - - var customContext = new CustomFilterEffectContext( - _factory, - CurrentTargets, - [.. CurrentTargets[0]._history.Select(v => v.Inherit())]); - custom.Accepts(customContext); - - foreach (EffectTarget t in CurrentTargets) - { - t._history.Add(item); - //t.Bounds = item.TransformBounds(t.Bounds); - t.OriginalBounds = t.Bounds.WithX(0).WithY(0); - } - } - } - - // 適用したIFEItemの数だけずらす - if (count.HasValue) - { - count = count.Value - takeCount; - } - - offset = Math.Max(offset - context._items.Count, 0); - - if (context._renderTimeItems.Count > 0) + foreach (FEItemWrapper item in context._items) { - Flush(false); - if (CurrentTargets.Count == 0) return; - - FEItemWrapper? deferral = null; - object[]? deferralItems = null; - bool deferred = false; - - // 後の `if(deferred)` で使う - int offset_ = 0; - int? count_ = null; - - for (int i = 0; i < CurrentTargets.Count; i++) + switch (item.Item) { - EffectTarget t = CurrentTargets[i]; - - using (var ctx = new FilterEffectContext(t.Bounds)) - { - foreach (object item in context._renderTimeItems) + case IFEItem_Skia skia: { - if (item is FilterEffect fe) - { - ctx.Apply(fe); - } - else if (item is FEItemWrapper feitem) + skia.Accepts(this, Builder); + foreach (EffectTarget t in CurrentTargets) { - ctx._items.Add(feitem); + t.Bounds = item.Item.TransformBounds(t.Bounds); + t.OriginalBounds = item.Item.TransformBounds(t.OriginalBounds); } - } - if (i == 0) - { - deferred = ctx.Bounds.IsInvalid; - if (deferred) - { - deferral = ctx._items[^1]; - deferralItems = [.. ctx._renderTimeItems]; - } + break; } - - Debug.Assert(deferred == ctx.Bounds.IsInvalid); - - if (ctx.Bounds.IsInvalid) + case IFEItem_Custom custom: { - // boundsが無効な場合 - // 無効になる手前まで、エフェクトを適用する - Rect b = t.Bounds; - for (int ii = 0; ii < ctx._items.Count - 1; ii++) - { - b = ctx._items[ii].Item.TransformBounds(b); - } + Flush(); + if (CurrentTargets.Count == 0) return; - using (FilterEffectContext safeContext = ctx.Clone()) - using (var builder = new SKImageFilterBuilder()) - using (var activator = new FilterEffectActivator([t], builder, _factory)) - { - safeContext.Bounds = b; - safeContext._items.RemoveAt(safeContext._items.Count - 1); - safeContext._renderTimeItems.Clear(); - - activator.Apply(safeContext, offset, count); - activator.Flush(false); - - CurrentTargets.RemoveAt(i); - CurrentTargets.InsertRange(i, activator.CurrentTargets); - i += activator.CurrentTargets.Count - 1; + var customContext = new CustomFilterEffectContext(factory, CurrentTargets); + custom.Accepts(customContext); - if (i == 0) - { - int takeCount_; - if (count.HasValue) - { - takeCount_ = Math.Min(count.Value, safeContext._items.Count); - count_ = count.Value - takeCount_; - } - else - { - takeCount_ = Math.Max(safeContext._items.Count - offset, 0); - } - - offset_ = Math.Max(offset - safeContext._items.Count, 0); - } - } - } - else - { - using (var builder = new SKImageFilterBuilder()) - using (var activator = new FilterEffectActivator([t], builder, _factory)) + foreach (EffectTarget t in CurrentTargets) { - activator.Apply(ctx, offset, count); - activator.Flush(false); - - CurrentTargets.RemoveAt(i); - CurrentTargets.InsertRange(i, activator.CurrentTargets); - i += activator.CurrentTargets.Count - 1; - - if (i == 0) - { - int takeCount_; - if (count.HasValue) - { - takeCount_ = Math.Min(count.Value, ctx._items.Count); - count_ = count.Value - takeCount_; - } - else - { - takeCount_ = Math.Max(ctx._items.Count - offset, 0); - } - - offset_ = Math.Max(offset - ctx._items.Count, 0); - } + //t.Bounds = item.TransformBounds(t.Bounds); + t.OriginalBounds = t.Bounds.WithX(0).WithY(0); } - } - } - } - - offset = offset_; - count = count_; - - if (deferred && (!count.HasValue || count > 0) && CurrentTargets.Count > 0) - { - if (offset == 0) - { - Flush(false); - var customContext = new CustomFilterEffectContext( - _factory, - CurrentTargets, - [.. CurrentTargets[0]._history.Select(v => v.Inherit())]); - ((IFEItem_Custom)deferral!.Item).Accepts(customContext); - foreach (EffectTarget t in CurrentTargets) - { - // deferral - t._history.Add(deferral); - //t.Bounds = item.TransformBounds(t.Bounds); - t.OriginalBounds = t.Bounds.WithX(0).WithY(0); + break; } - } - - if (count.HasValue) - { - count = count.Value - 1; - } - offset = Math.Max(offset - 1, 0); + } + } - using (var builder = new SKImageFilterBuilder()) - using (var activator = new FilterEffectActivator(CurrentTargets, builder, _factory)) - using (var ctx = new FilterEffectContext(Rect.Invalid)) - { - foreach (object item in deferralItems!) - { - if (item is FilterEffect fe) - { - ctx.Apply(fe); - } - else if (item is FEItemWrapper feitem) - { - ctx._items.Add(feitem); - } - } + if (context._renderTimeItems.Count <= 0) return; - activator.Apply(ctx, offset, count); - activator.Flush(false); + Flush(false); + if (CurrentTargets.Count == 0) return; + using var ctx = new FilterEffectContext(CurrentTargets.CalculateBounds()); - // `activator.CurrentTargets` と `this.CurrentTargets` は同じインスタンス - //CurrentTargets.Clear(); - //CurrentTargets.AddRange(activator.CurrentTargets); - } + foreach (object item in context._renderTimeItems) + { + switch (item) + { + case FilterEffect fe: + ctx.Apply(fe); + break; + case FEItemWrapper feitem: + ctx._items.Add(feitem); + break; } } - } - public void Apply(FilterEffectContext context) - { - Apply(context, 0, null); + Apply(ctx); } public SKImageFilter? Activate(FilterEffectContext context) { - SKImageFilter? filter; Flush(false); - using (EffectTargets cloned = CurrentTargets.Clone()) - using (var builder = new SKImageFilterBuilder()) - using (var activator = new FilterEffectActivator(cloned, builder, _factory)) + + using EffectTargets cloned = CurrentTargets.Clone(); + using var builder = new SKImageFilterBuilder(); + using var activator = new FilterEffectActivator(cloned, builder, factory); + + activator.Apply(context); + activator.Flush(false); + + SKImageFilter? filter = builder.GetFilter(); + if (filter != null) return filter; + + foreach (EffectTarget t in activator.CurrentTargets) { - activator.Apply(context); + if (t.Surface == null) continue; - activator.Flush(false); + SKSurface innerSurface = t.Surface.Value; + using SKImage skImage = innerSurface.Snapshot(); - filter = builder.GetFilter(); if (filter == null) { - foreach (EffectTarget t in activator.CurrentTargets) - { - if (t.Surface != null) - { - SKSurface innerSurface = t.Surface.Value; - using (SKImage skImage = innerSurface.Snapshot()) - { - if (filter == null) - { - filter = SKImageFilter.CreateImage(skImage); - } - else - { - filter = SKImageFilter.CreateCompose(filter, SKImageFilter.CreateImage(skImage)); - } - } - } - } + filter = SKImageFilter.CreateImage(skImage); + } + else + { + filter = SKImageFilter.CreateCompose(filter, SKImageFilter.CreateImage(skImage)); } } diff --git a/src/Beutl.Engine/Graphics/FilterEffects/FilterEffectCustomOperationContext.cs b/src/Beutl.Engine/Graphics/FilterEffects/FilterEffectCustomOperationContext.cs index afb815b74..104fc2227 100644 --- a/src/Beutl.Engine/Graphics/FilterEffects/FilterEffectCustomOperationContext.cs +++ b/src/Beutl.Engine/Graphics/FilterEffects/FilterEffectCustomOperationContext.cs @@ -12,17 +12,12 @@ namespace Beutl.Graphics.Effects; public class FilterEffectCustomOperationContext { private readonly IImmediateCanvasFactory _factory; - private readonly ImmutableArray _history; private EffectTarget _target; - internal FilterEffectCustomOperationContext( - IImmediateCanvasFactory canvas, - EffectTarget target, - ImmutableArray history) + internal FilterEffectCustomOperationContext(IImmediateCanvasFactory canvas, EffectTarget target) { _target = target.Clone(); _factory = canvas; - _history = history; } public EffectTarget Target @@ -47,17 +42,11 @@ public EffectTarget CreateTarget(int width, int height) if (surface != null) { using var surfaceRef = Ref.Create(surface); - var obj = new EffectTarget(surfaceRef, new Rect(_target.Bounds.X, _target.Bounds.Y, width, height)); - - obj._history.AddRange(_history); - return obj; + return new EffectTarget(surfaceRef, new Rect(_target.Bounds.X, _target.Bounds.Y, width, height)); } else { - var obj = new EffectTarget(); - - obj._history.AddRange(_history); - return obj; + return new EffectTarget(); } } diff --git a/src/Beutl.Engine/Graphics/FilterEffects/FilterEffectNodeComparer.cs b/src/Beutl.Engine/Graphics/FilterEffects/FilterEffectNodeComparer.cs deleted file mode 100644 index db4a6eb6c..000000000 --- a/src/Beutl.Engine/Graphics/FilterEffects/FilterEffectNodeComparer.cs +++ /dev/null @@ -1,138 +0,0 @@ -using System.Collections.Immutable; -using System.Diagnostics; - -using Beutl.Collections; -using Beutl.Graphics.Rendering; -using Beutl.Graphics.Rendering.Cache; - -namespace Beutl.Graphics.Effects; - -public class FilterEffectNodeComparer -{ - // Todo: すべてのEffectTargetのhistoryを記録 - private ImmutableArray _current = []; - private int? _prevVersion; - - public FilterEffectNodeComparer(FilterEffectNode node) - { - Node = node; - } - - public FilterEffectNode Node { get; } - - private static int CountEquals(IReadOnlyList left, IReadOnlyList right) - { - int minLength = Math.Min(left.Count, right.Count); - for (int i = 0; i < minLength; i++) - { - if (!left[i].Item.Equals(right[i].Item)) - { - return i; - } - } - - return minLength; - } - - public void OnRender(FilterEffectActivator activator) - { - if (activator.CurrentTargets.Count > 0) - { - var captured = activator.CurrentTargets[0]._history.ToImmutableArray(); - _current = captured; - } - else - { - _current = []; - } - } - - public void OnRender(FilterEffectActivator activator, int offset, int? count) - { - if (activator.CurrentTargets.Count > 0) - { - if (!count.HasValue) - { - if (offset == 0) - { - _current = [.. activator.CurrentTargets[0]._history]; - } - else - { - if (_current.Length < offset) - { - Debug.Fail("_current.Length < offset"); - return; - } - - ReadOnlySpan old = _current.AsSpan().Slice(0, offset); - _current = [.. old, .. activator.CurrentTargets[0]._history]; - } - } - } - else - { - _current = []; - } - } - - public void Accepts(RenderCache cache) - { - if (_prevVersion != Node.FilterEffect.Version) - { - using (var context = new FilterEffectContext(Node.Children.Count == 1 ? Node.OriginalBounds : Rect.Invalid)) - { - context.Apply(Node.FilterEffect); - - Compare: - int minLength = Math.Min(_current.Length, context._items.Count); - int d = CountEquals(_current, context._items); - - if (context._renderTimeItems.Count > 0) - { - object[] items = [.. context._renderTimeItems]; - context._renderTimeItems.Clear(); - foreach (object item in items) - { - if (context.Bounds.IsInvalid) - { - if (context._items.Count < _current.Length) - { - context.Bounds = _current[context._items.Count].SourceBounds; - } - else - { - // エフェクトが追加されたとき - d = CountEquals(_current, context._items); - cache.ReportSameNumber(d, context._items.Count + 1); - _prevVersion = Node.FilterEffect.Version; - return; - } - } - - if (item is FilterEffect fe) - { - context.Apply(fe); - } - else if (item is FEItemWrapper feitem) - { - context._items.Add(feitem); - } - } - - goto Compare; - } - else - { - cache.ReportSameNumber(d, context._items.Count); - } - } - } - else - { - cache.ReportSameNumber(_current.Length, _current.Length); - } - - _prevVersion = Node.FilterEffect.Version; - } -} diff --git a/src/Beutl.Engine/Graphics/FilterEffects/TransformEffect.cs b/src/Beutl.Engine/Graphics/FilterEffects/TransformEffect.cs index 86d83434a..bab7f439d 100644 --- a/src/Beutl.Engine/Graphics/FilterEffects/TransformEffect.cs +++ b/src/Beutl.Engine/Graphics/FilterEffects/TransformEffect.cs @@ -7,6 +7,7 @@ namespace Beutl.Graphics.Effects; +// TODO: EffectTargetが複数の場合に対応する public sealed class TransformEffect : FilterEffect { public static readonly CoreProperty TransformProperty; diff --git a/src/Beutl.Engine/Graphics/ICanvas.cs b/src/Beutl.Engine/Graphics/ICanvas.cs index 8c1d2ecb5..51ddd2597 100644 --- a/src/Beutl.Engine/Graphics/ICanvas.cs +++ b/src/Beutl.Engine/Graphics/ICanvas.cs @@ -7,7 +7,7 @@ namespace Beutl.Graphics; -public interface ICanvas : IDisposable +public interface ICanvas : IDisposable, IPopable { PixelSize Size { get; } @@ -33,7 +33,7 @@ public interface ICanvas : IDisposable void DrawDrawable(Drawable drawable); - void DrawNode(IGraphicNode node); + void DrawNode(RenderNode node); void DrawBackdrop(IBackdrop backdrop); @@ -41,8 +41,6 @@ public interface ICanvas : IDisposable Bitmap GetBitmap(); - void Pop(int count = -1); - PushedState Push(); PushedState PushLayer(Rect limit = default); diff --git a/src/Beutl.Engine/Graphics/IImmediateCanvasFactory.cs b/src/Beutl.Engine/Graphics/IImmediateCanvasFactory.cs index ad5288ac0..7bb6e4f97 100644 --- a/src/Beutl.Engine/Graphics/IImmediateCanvasFactory.cs +++ b/src/Beutl.Engine/Graphics/IImmediateCanvasFactory.cs @@ -5,7 +5,7 @@ namespace Beutl.Graphics; public interface IImmediateCanvasFactory { - RenderCacheContext? GetCacheContext(); + RenderNodeCacheContext? GetCacheContext(); ImmediateCanvas CreateCanvas(SKSurface surface, bool leaveOpen); diff --git a/src/Beutl.Engine/Graphics/ImmediateCanvas.cs b/src/Beutl.Engine/Graphics/ImmediateCanvas.cs index fb0bc2adc..4543418ed 100644 --- a/src/Beutl.Engine/Graphics/ImmediateCanvas.cs +++ b/src/Beutl.Engine/Graphics/ImmediateCanvas.cs @@ -7,7 +7,6 @@ using Beutl.Media.TextFormatting; using Beutl.Threading; using SkiaSharp; -using SkiaSharp.HarfBuzz; namespace Beutl.Graphics; @@ -66,7 +65,7 @@ public Matrix Transform internal SKCanvas Canvas { get; } - public RenderCacheContext? GetCacheContext() + public RenderNodeCacheContext? GetCacheContext() { return Factory?.GetCacheContext(); } @@ -175,48 +174,17 @@ public void DrawSurface(SKSurface surface, Point point) public void DrawDrawable(Drawable drawable) { - drawable.Render(this); + using var node = new DrawableRenderNode(drawable); + using var context = new GraphicsContext2D(node, Size); + drawable.Render(context); + var processor = new RenderNodeProcessor(node, this, true); + processor.Render(this); } - public void DrawNode(IGraphicNode node) + public void DrawNode(RenderNode node) { - if (GetCacheContext() is { } context) - { - RenderCache cache = context.GetCache(node); - // RenderLayer.Renderでキャッシュの有効性を確認しているのでチェックを省く - if (cache.IsCached) - { - if (node is ISupportRenderCache supportCache) - { - supportCache.RenderWithCache(this, cache); - return; - } - else - { - if (cache.CacheCount == 1) - { - using (Ref surface = cache.UseCache(out Rect bounds)) - { - DrawSurface(surface.Value, bounds.Position); - } - } - else - { - foreach ((Ref Surface, Rect Bounds) item in cache.UseCache()) - { - using (item.Surface) - { - DrawSurface(item.Surface.Value, item.Bounds.Position); - } - } - } - - return; - } - } - } - - node.Render(this); + var processor = new RenderNodeProcessor(node, this, true); + processor.Render(this); } public void DrawBackdrop(IBackdrop backdrop) diff --git a/src/Beutl.Engine/Graphics/PushedState.cs b/src/Beutl.Engine/Graphics/PushedState.cs index 62487a029..376254893 100644 --- a/src/Beutl.Engine/Graphics/PushedState.cs +++ b/src/Beutl.Engine/Graphics/PushedState.cs @@ -1,10 +1,15 @@ namespace Beutl.Graphics; +public interface IPopable +{ + void Pop(int count); +} + public readonly record struct PushedState : IDisposable { - public PushedState(ICanvas canvas, int level) + public PushedState(IPopable popable, int level) { - Canvas = canvas; + Popable = popable; Count = level; } @@ -13,12 +18,12 @@ public PushedState() Count = -1; } - public ICanvas? Canvas { get; init; } + public IPopable? Popable { get; init; } public int Count { get; init; } public void Dispose() { - Canvas?.Pop(Count); + Popable?.Pop(Count); } } diff --git a/src/Beutl.Engine/Graphics/Rendering/BlendModeNode.cs b/src/Beutl.Engine/Graphics/Rendering/BlendModeNode.cs deleted file mode 100644 index 80aef8a34..000000000 --- a/src/Beutl.Engine/Graphics/Rendering/BlendModeNode.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Beutl.Graphics.Rendering.Cache; -using Beutl.Media.Source; -using SkiaSharp; - -namespace Beutl.Graphics.Rendering; - -public sealed class BlendModeNode(BlendMode blendMode) : ContainerNode, ISupportRenderCache -{ - public BlendMode BlendMode { get; } = blendMode; - - public bool Equals(BlendMode blendMode) - { - return BlendMode == blendMode; - } - - public override void Render(ImmediateCanvas canvas) - { - using (canvas.PushBlendMode(BlendMode)) - { - base.Render(canvas); - } - } - - void ISupportRenderCache.Accepts(RenderCache cache) - { - if (BlendMode == BlendMode.SrcOver) - { - cache.IncrementRenderCount(); - } - else - { - cache.ReportRenderCount(0); - } - } - - void ISupportRenderCache.CreateCache(IImmediateCanvasFactory factory, RenderCache cache, RenderCacheContext context) - { - if (BlendMode != BlendMode.SrcOver) - throw new InvalidOperationException("SrcOver以外のブレンドモードはキャッシュ用に描画できません"); - - context.CreateDefaultCache(this, cache, factory); - } - - void ISupportRenderCache.RenderWithCache(ImmediateCanvas canvas, RenderCache cache) - { - using (Ref surface = cache.UseCache(out Rect cacheBounds)) - { - using (canvas.PushBlendMode(BlendMode)) - { - canvas.DrawSurface(surface.Value, cacheBounds.Position); - } - } - } -} diff --git a/src/Beutl.Engine/Graphics/Rendering/BlendModeRenderNode.cs b/src/Beutl.Engine/Graphics/Rendering/BlendModeRenderNode.cs new file mode 100644 index 000000000..62544f839 --- /dev/null +++ b/src/Beutl.Engine/Graphics/Rendering/BlendModeRenderNode.cs @@ -0,0 +1,26 @@ +namespace Beutl.Graphics.Rendering; + +public sealed class BlendModeRenderNode(BlendMode blendMode) : ContainerRenderNode +{ + public BlendMode BlendMode { get; } = blendMode; + + public bool Equals(BlendMode blendMode) + { + return BlendMode == blendMode; + } + + public override RenderNodeOperation[] Process(RenderNodeContext context) + { + context.IsRenderCacheEnabled = BlendMode == BlendMode.SrcOver; + return context.Input.Select(r => + { + return RenderNodeOperation.CreateDecorator(r, canvas => + { + using (canvas.PushBlendMode(BlendMode)) + { + r.Render(canvas); + } + }); + }).ToArray(); + } +} diff --git a/src/Beutl.Engine/Graphics/Rendering/BrushDrawNode.cs b/src/Beutl.Engine/Graphics/Rendering/BrushRenderNode.cs similarity index 74% rename from src/Beutl.Engine/Graphics/Rendering/BrushDrawNode.cs rename to src/Beutl.Engine/Graphics/Rendering/BrushRenderNode.cs index 5cf426610..4d2d53cf6 100644 --- a/src/Beutl.Engine/Graphics/Rendering/BrushDrawNode.cs +++ b/src/Beutl.Engine/Graphics/Rendering/BrushRenderNode.cs @@ -2,10 +2,9 @@ namespace Beutl.Graphics.Rendering; -public abstract class BrushDrawNode : DrawNode +public abstract class BrushRenderNode : RenderNode { - protected BrushDrawNode(IBrush? fill, IPen? pen, Rect bounds) - : base(bounds) + protected BrushRenderNode(IBrush? fill, IPen? pen) { Fill = (fill as IMutableBrush)?.ToImmutable() ?? fill; Pen = (pen as IMutablePen)?.ToImmutable() ?? pen; diff --git a/src/Beutl.Engine/Graphics/Rendering/Cache/ISupportRenderCache.cs b/src/Beutl.Engine/Graphics/Rendering/Cache/ISupportRenderCache.cs deleted file mode 100644 index 9f8227703..000000000 --- a/src/Beutl.Engine/Graphics/Rendering/Cache/ISupportRenderCache.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Beutl.Graphics.Rendering.Cache; - -public interface ISupportRenderCache -{ - void Accepts(RenderCache cache); - - void RenderWithCache(ImmediateCanvas canvas, RenderCache cache); - - void CreateCache(IImmediateCanvasFactory factory, RenderCache cache, RenderCacheContext context); -} diff --git a/src/Beutl.Engine/Graphics/Rendering/Cache/RenderCache.cs b/src/Beutl.Engine/Graphics/Rendering/Cache/RenderCache.cs deleted file mode 100644 index 2b07830e1..000000000 --- a/src/Beutl.Engine/Graphics/Rendering/Cache/RenderCache.cs +++ /dev/null @@ -1,333 +0,0 @@ -using System.Diagnostics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using Beutl.Media.Source; -using Beutl.Threading; -using SkiaSharp; - -namespace Beutl.Graphics.Rendering.Cache; - -public sealed class RenderCache(IGraphicNode node) : IDisposable -{ - private readonly WeakReference _node = new(node); - private readonly List<(Ref, Rect)> _cache = new(1); - - private int _count; - - // キャッシュしたときの進捗の値 - private int _cachedAt = -1; - // 前回のフレームと比べたときに同じだった操作の数(進捗) - private FixedArrayAccessor? _accessor; - private int _denum; - - ~RenderCache() - { - if (!IsDisposed) - Dispose(); - } - - private FixedArrayAccessor Accessor => _accessor ??= new(); - - public bool IsCached => _cache.Count != 0; - - public int CacheCount => _cache.Count; - - public DateTime LastAccessedTime { get; private set; } - - public bool IsDisposed { get; private set; } - - public List>? Children { get; private set; } - - public void ReportRenderCount(int count) - { - _count = count; - } - - public void IncrementRenderCount() - { - _count++; - } - - // 一つのノードで処理が別れている場合、どこまで同じかを報告する - public void ReportSameNumber(int value, int count) - { - _denum = count; - - Accessor.Set(value); - Accessor.IncrementIndex(); - - // キャッシュしたときのpがvalueより大きい場合、キャッシュを無効化 - // 例えば、キャッシュ時には三つのエフェクトが含まれている状態だったが、<- (1) - // 最後の一つだけ変わったなど。 - if (_cachedAt > value) - { - Invalidate(); - } - // `GetMinNumber()` と `_cachedAt`がかけ離れている、 - // 例えば、上の (1) の状況で、三フレーム以上、変わらないエフェクトが追加されたとき - else if (GetMinNumber() > _cachedAt) - { - Invalidate(); - } - } - - public int GetMinNumber() - { - return _accessor?.Minimum() ?? 0; - } - - public void CaptureChildren() - { - if (_node.TryGetTarget(out IGraphicNode? node) - && node is ContainerNode container) - { - if (Children == null) - { - Children = new List>(container.Children.Count); - } - else - { - Children.EnsureCapacity(container.Children.Count); - } - - CollectionsMarshal.SetCount(Children, container.Children.Count); - Span> span = CollectionsMarshal.AsSpan(Children); - - for (int i = 0; i < container.Children.Count; i++) - { - IGraphicNode item = container.Children[i]; - ref WeakReference refrence = ref span[i]; - - if (refrence == null) - { - refrence = new WeakReference(item); - } - else - { - refrence.SetTarget(item); - } - } - } - } - - public bool SameChildren() - { - if (Children != null - && _node.TryGetTarget(out IGraphicNode? node) - && node is ContainerNode container) - { - if (Children.Count != container.Children.Count) - return false; - - for (int i = 0; i < Children.Count; i++) - { - WeakReference capturedRef = Children[i]; - IGraphicNode current = container.Children[i]; - if (!capturedRef.TryGetTarget(out IGraphicNode? captured) - || !ReferenceEquals(captured, current)) - { - return false; - } - } - - return true; - } - else - { - return true; - } - } - - public bool CanCache() - { - if (_count >= FixedArray.Count) - { - return true; - } - else if (_accessor != null) - { - for (int i = 0; i < FixedArray.Count; i++) - { - if (_accessor.Get(i) != _denum) - { - return false; - } - } - - return true; - } - else - { - return false; - } - } - - public bool CanCacheBoundary() - { - return GetMinNumber() >= 1 || _count >= FixedArray.Count; - } - - public void Invalidate() - { - RenderThread.Dispatcher.CheckAccess(); -#if DEBUG - if (_cache.Count != 0) - { - Debug.WriteLine($"[RenderCache:Invalildated] '{(_node.TryGetTarget(out IGraphicNode? node) ? node : null)}'"); - } -#endif - - foreach ((Ref, Rect) item in _cache) - { - item.Item1.Dispose(); - } - _cache.Clear(); - _cachedAt = -1; - } - - public void Dispose() - { - void DisposeOnRenderThread() - { - if (_cache.Count != 0) - { - (Ref, Rect)[] tmp = [.. _cache]; - _cache.Clear(); - - RenderThread.Dispatcher.Dispatch(() => - { - foreach ((Ref, Rect) item in tmp) - { - item.Item1.Dispose(); - } - }, DispatchPriority.Low); - } - - IsDisposed = true; - } - - if (!IsDisposed) - { - if (RenderThread.Dispatcher.CheckAccess()) - { - foreach ((Ref, Rect) item in _cache) - { - item.Item1.Dispose(); - } - _cache.Clear(); - IsDisposed = true; - } - else - { - DisposeOnRenderThread(); - } - - GC.SuppressFinalize(this); - } - } - - public Ref UseCache(out Rect bounds) - { - if (_cache.Count == 0) - { - throw new Exception("キャッシュはありません"); - } - - (Ref, Rect) c = _cache[0]; - bounds = c.Item2; - LastAccessedTime = DateTime.UtcNow; - return c.Item1.Clone(); - } - - public void StoreCache(Ref surface, Rect bounds) - { - Invalidate(); - - _cache.Add((surface.Clone(), bounds)); - - if (_accessor != null) - { - const int Count = FixedArray.Count; - _cachedAt = _accessor.Get((_accessor.Index + (Count - 1)) % Count); - } - else - { - _cachedAt = 0; - } - - LastAccessedTime = DateTime.UtcNow; - } - - public (Ref Surface, Rect Bounds)[] UseCache() - { - LastAccessedTime = DateTime.UtcNow; - return _cache.Select(i => (i.Item1.Clone(), i.Item2)).ToArray(); - } - - public void StoreCache(ReadOnlySpan<(Ref Surface, Rect Bounds)> items) - { - Invalidate(); - - foreach ((Ref surface, Rect bounds) in items) - { - _cache.Add((surface.Clone(), bounds)); - } - - if (_accessor != null) - { - const int Count = FixedArray.Count; - _cachedAt = _accessor.Get((_accessor.Index + (Count - 1)) % Count); - } - else - { - _cachedAt = 0; - } - - LastAccessedTime = DateTime.UtcNow; - } - - private unsafe class FixedArrayAccessor - { - public FixedArray Array; - public int Index; - - public void IncrementIndex() - { - Index++; - // 折り返す - Index %= FixedArray.Count; - } - - public ref int Get(int index) - { - if (index is < 0 or >= FixedArray.Count) - throw new Exception("0 <= index <= 2"); - - return ref Array.Array[index]; - } - - public void Set(int value) - { - Array.Array[Index] = value; - } - - public int Minimum() - { - int value = int.MaxValue; - for (int i = 0; i < FixedArray.Count; i++) - { - value = Math.Min(Array.Array[i], value); - } - - return value; - } - } - - private unsafe struct FixedArray - { - public const int Count = 3; - public fixed int Array[Count]; - - public Span Span => new(Unsafe.AsPointer(ref Array[0]), Count); - } -} diff --git a/src/Beutl.Engine/Graphics/Rendering/Cache/RenderCacheContext.cs b/src/Beutl.Engine/Graphics/Rendering/Cache/RenderCacheContext.cs deleted file mode 100644 index bf2d31dea..000000000 --- a/src/Beutl.Engine/Graphics/Rendering/Cache/RenderCacheContext.cs +++ /dev/null @@ -1,226 +0,0 @@ -using System.Diagnostics; -using System.Runtime.CompilerServices; -using System.Text.Json.Serialization; -using Beutl.Configuration; -using Beutl.Media; -using Beutl.Media.Source; -using Microsoft.Extensions.Logging; -using SkiaSharp; - -namespace Beutl.Graphics.Rendering.Cache; - -public sealed class RenderCacheContext : IDisposable -{ - private readonly ILogger _logger = BeutlApplication.Current.LoggerFactory.CreateLogger(); - private readonly ConditionalWeakTable _table = []; - private RenderCacheOptions _cacheOptions = RenderCacheOptions.CreateFromGlobalConfiguration(); - - public RenderCacheOptions CacheOptions - { - get => _cacheOptions; - set - { - ArgumentNullException.ThrowIfNull(value); - if (_cacheOptions != value) - { - Dispose(); - } - - _cacheOptions = value; - } - } - - public RenderCache GetCache(IGraphicNode node) - { - return _table.GetValue(node, key => new RenderCache(key)); - } - - public bool CanCacheRecursive(IGraphicNode node) - { - RenderCache cache = GetCache(node); - if (!cache.CanCache()) - return false; - - if (node is ContainerNode container) - { - if (cache.Children?.Count != container.Children.Count) - return false; - - for (int i = 0; i < cache.Children.Count; i++) - { - WeakReference capturedRef = cache.Children[i]; - IGraphicNode current = container.Children[i]; - if (!capturedRef.TryGetTarget(out IGraphicNode? captured) - || !ReferenceEquals(captured, current) - || !CanCacheRecursive(current)) - { - return false; - } - } - } - - return true; - } - - // nodeの子要素だけ調べる。node自体は調べない - // MakeCacheで使う - public bool CanCacheRecursiveChildrenOnly(IGraphicNode node) - { - if (node is ContainerNode containerNode) - { - foreach (IGraphicNode item in containerNode.Children) - { - if (!CanCacheRecursive(item)) - { - return false; - } - } - } - - return true; - } - - public void ClearCache(IGraphicNode node, RenderCache cache) - { - cache.Invalidate(); - - if (node is ContainerNode containerNode) - { - foreach (IGraphicNode item in containerNode.Children) - { - ClearCache(item); - } - } - } - - public void ClearCache(IGraphicNode node) - { - if (_table.TryGetValue(node, out RenderCache? cache)) - { - cache.Invalidate(); - } - - if (node is ContainerNode containerNode) - { - foreach (IGraphicNode item in containerNode.Children) - { - ClearCache(item); - } - } - } - - // 再帰呼び出し - public void MakeCache(IGraphicNode node, IImmediateCanvasFactory factory) - { - if (!_cacheOptions.IsEnabled) - return; - - RenderCache cache = GetCache(node); - // ここでのnodeは途中まで、キャッシュしても良い - // CanCacheRecursive内で再帰呼び出ししているのはすべてキャッシュできる必要がある - if (cache.CanCacheBoundary() && CanCacheRecursiveChildrenOnly(node)) - { - if (!cache.IsCached) - { - MakeCacheCore(node, cache, factory); - } - } - else if (node is ContainerNode containerNode) - { - cache.Invalidate(); - foreach (IGraphicNode item in containerNode.Children) - { - MakeCache(item, factory); - } - } - } - - public void CreateDefaultCache(IGraphicNode node, RenderCache cache, IImmediateCanvasFactory factory) - { - Rect bounds = node.Bounds; - //bounds = bounds.Inflate(5); - PixelRect boundsInPixels = PixelRect.FromRect(bounds); - PixelSize size = boundsInPixels.Size; - if (!_cacheOptions.Rules.Match(size)) - return; - - // nodeの子要素のキャッシュをすべて削除 - ClearCache(node, cache); - - // nodeをキャッシュ - SKSurface? surface = factory.CreateRenderTarget(size.Width, size.Height); - if (surface == null) - { - _logger.LogWarning("CreateRenderTarget returns null. ({Width}x{Height})", size.Width, size.Height); - return; - } - - using (ImmediateCanvas canvas = factory.CreateCanvas(surface, true)) - { - using (canvas.PushTransform(Matrix.CreateTranslation(-boundsInPixels.X, -boundsInPixels.Y))) - { - node.Render(canvas); - } - } - - using (var surfaceRef = Ref.Create(surface)) - { - cache.StoreCache(surfaceRef, boundsInPixels.ToRect(1)); - } - - Debug.WriteLine($"[RenderCache:Created] '{node}'"); - } - - private void MakeCacheCore(IGraphicNode node, RenderCache cache, IImmediateCanvasFactory factory) - { - if (node is ISupportRenderCache supportRenderCache) - { - supportRenderCache.CreateCache(factory, cache, this); - } - else - { - CreateDefaultCache(node, cache, factory); - } - } - - public void Dispose() - { - Clear(); - } - - public void Clear() - { - foreach (KeyValuePair item in _table) - { - item.Value.Dispose(); - } - - _table.Clear(); - } -} - -[JsonSerializable(typeof(RenderCacheOptions))] -public record RenderCacheOptions(bool IsEnabled, RenderCacheRules Rules) -{ - public static readonly RenderCacheOptions Default = new(true, RenderCacheRules.Default); - public static readonly RenderCacheOptions Disabled = new(false, RenderCacheRules.Default); - - public static RenderCacheOptions CreateFromGlobalConfiguration() - { - EditorConfig config = GlobalConfiguration.Instance.EditorConfig; - return new RenderCacheOptions( - config.IsNodeCacheEnabled, - new RenderCacheRules(config.NodeCacheMaxPixels, config.NodeCacheMinPixels)); - } -} - -public readonly record struct RenderCacheRules(int MaxPixels, int MinPixels) -{ - public static readonly RenderCacheRules Default = new(1000 * 1000, 1); - - public bool Match(PixelSize size) - { - int count = size.Width * size.Height; - return MinPixels <= count && count <= MaxPixels; - } -} diff --git a/src/Beutl.Engine/Graphics/Rendering/Cache/RenderNodeCache.cs b/src/Beutl.Engine/Graphics/Rendering/Cache/RenderNodeCache.cs new file mode 100644 index 000000000..725bbf049 --- /dev/null +++ b/src/Beutl.Engine/Graphics/Rendering/Cache/RenderNodeCache.cs @@ -0,0 +1,208 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using Beutl.Media.Source; +using Beutl.Threading; +using SkiaSharp; + +namespace Beutl.Graphics.Rendering.Cache; + +public sealed class RenderNodeCache(RenderNode node) : IDisposable +{ + private readonly WeakReference _node = new(node); + private readonly List<(Ref, Rect)> _cache = new(1); + + public const int Count = 3; + + private int _count; + + ~RenderNodeCache() + { + if (!IsDisposed) + Dispose(); + } + + public bool IsCached => _cache.Count != 0; + + public int CacheCount => _cache.Count; + + public DateTime LastAccessedTime { get; private set; } + + public bool IsDisposed { get; private set; } + + public List>? Children { get; private set; } + + public void ReportRenderCount(int count) + { + _count = count; + } + + public void IncrementRenderCount() + { + _count++; + } + + public void CaptureChildren() + { + if (_node.TryGetTarget(out RenderNode? node) + && node is ContainerRenderNode container) + { + if (Children == null) + { + Children = new List>(container.Children.Count); + } + else + { + Children.EnsureCapacity(container.Children.Count); + } + + CollectionsMarshal.SetCount(Children, container.Children.Count); + Span> span = CollectionsMarshal.AsSpan(Children); + + for (int i = 0; i < container.Children.Count; i++) + { + RenderNode item = container.Children[i]; + ref WeakReference refrence = ref span[i]; + + if (refrence == null) + { + refrence = new WeakReference(item); + } + else + { + refrence.SetTarget(item); + } + } + } + } + + public bool SameChildren() + { + if (Children != null + && _node.TryGetTarget(out RenderNode? node) + && node is ContainerRenderNode container) + { + if (Children.Count != container.Children.Count) + return false; + + for (int i = 0; i < Children.Count; i++) + { + WeakReference capturedRef = Children[i]; + RenderNode current = container.Children[i]; + if (!capturedRef.TryGetTarget(out RenderNode? captured) + || !ReferenceEquals(captured, current)) + { + return false; + } + } + + return true; + } + else + { + return true; + } + } + + public bool CanCache() + { + return _count >= Count; + } + + public void Invalidate() + { + RenderThread.Dispatcher.CheckAccess(); +#if DEBUG + if (_cache.Count != 0) + { + Debug.WriteLine($"[RenderCache:Invalildated] '{(_node.TryGetTarget(out RenderNode? node) ? node : null)}'"); + } +#endif + + foreach ((Ref, Rect) item in _cache) + { + item.Item1.Dispose(); + } + _cache.Clear(); + } + + public void Dispose() + { + void DisposeOnRenderThread() + { + if (_cache.Count != 0) + { + (Ref, Rect)[] tmp = [.. _cache]; + _cache.Clear(); + + RenderThread.Dispatcher.Dispatch(() => + { + foreach ((Ref, Rect) item in tmp) + { + item.Item1.Dispose(); + } + }, DispatchPriority.Low); + } + + IsDisposed = true; + } + + if (!IsDisposed) + { + if (RenderThread.Dispatcher.CheckAccess()) + { + foreach ((Ref, Rect) item in _cache) + { + item.Item1.Dispose(); + } + _cache.Clear(); + IsDisposed = true; + } + else + { + DisposeOnRenderThread(); + } + + GC.SuppressFinalize(this); + } + } + + public Ref UseCache(out Rect bounds) + { + if (_cache.Count == 0) + { + throw new Exception("キャッシュはありません"); + } + + (Ref, Rect) c = _cache[0]; + bounds = c.Item2; + LastAccessedTime = DateTime.UtcNow; + return c.Item1.Clone(); + } + + public void StoreCache(Ref surface, Rect bounds) + { + Invalidate(); + + _cache.Add((surface.Clone(), bounds)); + + LastAccessedTime = DateTime.UtcNow; + } + + public IEnumerable<(Ref Surface, Rect Bounds)> UseCache() + { + LastAccessedTime = DateTime.UtcNow; + return _cache.Select(i => (i.Item1.Clone(), i.Item2)); + } + + public void StoreCache(ReadOnlySpan<(Ref Surface, Rect Bounds)> items) + { + Invalidate(); + + foreach ((Ref surface, Rect bounds) in items) + { + _cache.Add((surface.Clone(), bounds)); + } + + LastAccessedTime = DateTime.UtcNow; + } +} diff --git a/src/Beutl.Engine/Graphics/Rendering/Cache/RenderNodeCacheContext.cs b/src/Beutl.Engine/Graphics/Rendering/Cache/RenderNodeCacheContext.cs new file mode 100644 index 000000000..4f4cc7de9 --- /dev/null +++ b/src/Beutl.Engine/Graphics/Rendering/Cache/RenderNodeCacheContext.cs @@ -0,0 +1,181 @@ +using System.Diagnostics; +using System.Text.Json.Serialization; +using Beutl.Configuration; +using Beutl.Media; +using Beutl.Media.Source; +using Microsoft.Extensions.Logging; +using SkiaSharp; + +namespace Beutl.Graphics.Rendering.Cache; + +// TODO: インスタンスのあるクラスである必要はないので、近々削除する +public sealed class RenderNodeCacheContext(RenderScene scene) +{ + private readonly ILogger _logger = + BeutlApplication.Current.LoggerFactory.CreateLogger(); + + private RenderCacheOptions _cacheOptions = RenderCacheOptions.CreateFromGlobalConfiguration(); + + public RenderCacheOptions CacheOptions + { + get => _cacheOptions; + set + { + ArgumentNullException.ThrowIfNull(value); + scene.ClearCache(); + _cacheOptions = value; + } + } + + public static bool CanCacheRecursive(RenderNode node) + { + RenderNodeCache cache = node.Cache; + if (!cache.CanCache()) + return false; + + if (node is ContainerRenderNode container) + { + if (cache.Children?.Count != container.Children.Count) + return false; + + for (int i = 0; i < cache.Children.Count; i++) + { + WeakReference capturedRef = cache.Children[i]; + RenderNode current = container.Children[i]; + if (!capturedRef.TryGetTarget(out RenderNode? captured) + || !ReferenceEquals(captured, current) + || !CanCacheRecursive(current)) + { + return false; + } + } + } + + return true; + } + + // nodeの子要素だけ調べる。node自体は調べない + // MakeCacheで使う + public static bool CanCacheRecursiveChildrenOnly(RenderNode node) + { + if (node is ContainerRenderNode containerNode) + { + foreach (RenderNode item in containerNode.Children) + { + if (!CanCacheRecursive(item)) + { + return false; + } + } + } + + return true; + } + + public static void ClearCache(RenderNode node, RenderNodeCache cache) + { + cache.Invalidate(); + + if (node is ContainerRenderNode containerNode) + { + foreach (RenderNode item in containerNode.Children) + { + ClearCache(item); + } + } + } + + public static void ClearCache(RenderNode node) + { + node.Cache.Invalidate(); + + if (node is not ContainerRenderNode containerNode) return; + + foreach (RenderNode item in containerNode.Children) + { + ClearCache(item); + } + } + + // 再帰呼び出し + public void MakeCache(RenderNode node, IImmediateCanvasFactory factory) + { + if (!_cacheOptions.IsEnabled) + return; + + RenderNodeCache cache = node.Cache; + // ここでのnodeは途中まで、キャッシュしても良い + // CanCacheRecursive内で再帰呼び出ししているのはすべてキャッシュできる必要がある + if (cache.CanCache() && CanCacheRecursiveChildrenOnly(node)) + { + if (!cache.IsCached) + { + CreateDefaultCache(node, cache, factory); + } + } + else if (node is ContainerRenderNode containerNode) + { + cache.Invalidate(); + foreach (RenderNode item in containerNode.Children) + { + MakeCache(item, factory); + } + } + } + + public void CreateDefaultCache(RenderNode node, RenderNodeCache cache, IImmediateCanvasFactory factory) + { + var processor = new RenderNodeProcessor(node, factory, false); + var list = processor.RasterizeToSurface(); + int pixels = list.Sum(i => + { + var pr = PixelRect.FromRect(i.Bounds); + return pr.Width * pr.Height; + }); + if (!_cacheOptions.Rules.Match(pixels)) + return; + + // nodeの子要素のキャッシュをすべて削除 + ClearCache(node, cache); + + var arr = list.Select(i => (Ref.Create(i.Surface), i.Bounds)).ToArray(); + cache.StoreCache(arr); + foreach ((Ref s, Rect _) in arr) + { + s.Dispose(); + } + + Debug.WriteLine($"[RenderCache:Created] '{node}'"); + } +} + +[JsonSerializable(typeof(RenderCacheOptions))] +public record RenderCacheOptions(bool IsEnabled, RenderCacheRules Rules) +{ + public static readonly RenderCacheOptions Default = new(true, RenderCacheRules.Default); + public static readonly RenderCacheOptions Disabled = new(false, RenderCacheRules.Default); + + public static RenderCacheOptions CreateFromGlobalConfiguration() + { + EditorConfig config = GlobalConfiguration.Instance.EditorConfig; + return new RenderCacheOptions( + config.IsNodeCacheEnabled, + new RenderCacheRules(config.NodeCacheMaxPixels, config.NodeCacheMinPixels)); + } +} + +public readonly record struct RenderCacheRules(int MaxPixels, int MinPixels) +{ + public static readonly RenderCacheRules Default = new(1000 * 1000, 1); + + public bool Match(PixelSize size) + { + int count = size.Width * size.Height; + return MinPixels <= count && count <= MaxPixels; + } + + public bool Match(int pixels) + { + return MinPixels <= pixels && pixels <= MaxPixels; + } +} diff --git a/src/Beutl.Engine/Graphics/Rendering/ClearNode.cs b/src/Beutl.Engine/Graphics/Rendering/ClearNode.cs deleted file mode 100644 index 0f258f257..000000000 --- a/src/Beutl.Engine/Graphics/Rendering/ClearNode.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Beutl.Media; - -namespace Beutl.Graphics.Rendering; - -public sealed class ClearNode(Color color) : DrawNode(Rect.Empty) -{ - public Color Color { get; } = color; - - public bool Equals(Color color) - { - return Color == color; - } - - public override void Render(ImmediateCanvas canvas) - { - canvas.Clear(Color); - } - - public override bool HitTest(Point point) - { - return false; - } -} diff --git a/src/Beutl.Engine/Graphics/Rendering/ClearRenderNode.cs b/src/Beutl.Engine/Graphics/Rendering/ClearRenderNode.cs new file mode 100644 index 000000000..6328a35a9 --- /dev/null +++ b/src/Beutl.Engine/Graphics/Rendering/ClearRenderNode.cs @@ -0,0 +1,18 @@ +using Beutl.Media; + +namespace Beutl.Graphics.Rendering; + +public sealed class ClearRenderNode(Color color) : RenderNode +{ + public Color Color { get; } = color; + + public bool Equals(Color color) + { + return Color == color; + } + + public override RenderNodeOperation[] Process(RenderNodeContext context) + { + return [RenderNodeOperation.CreateLambda(Rect.Empty, canvas => canvas.Clear(Color))]; + } +} diff --git a/src/Beutl.Engine/Graphics/Rendering/ContainerNode.cs b/src/Beutl.Engine/Graphics/Rendering/ContainerNode.cs deleted file mode 100644 index 1d90963d2..000000000 --- a/src/Beutl.Engine/Graphics/Rendering/ContainerNode.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Beutl.Graphics.Rendering; - -public class ContainerNode : IGraphicNode -{ - private readonly List _children = []; - private bool _isBoundsDirty = true; - private Rect _originalBounds; - - ~ContainerNode() - { - if (!IsDisposed) - { - OnDispose(false); - IsDisposed = true; - } - } - - public Rect OriginalBounds - { - get - { - //if (_isBoundsDirty) - { - _originalBounds = default; - foreach (IGraphicNode child in _children) - { - _originalBounds = _originalBounds.Union(child.Bounds); - } - - _originalBounds = _originalBounds.Normalize(); - _isBoundsDirty = false; - } - - return _originalBounds; - } - } - - public Rect Bounds => TransformBounds(OriginalBounds); - - public IReadOnlyList Children => _children; - - public bool IsDisposed { get; private set; } - - public virtual bool HitTest(Point point) - { - foreach (IGraphicNode child in Children) - { - if (child.HitTest(point)) - return true; - } - - return false; - } - - public virtual void Render(ImmediateCanvas canvas) - { - foreach (IGraphicNode item in _children) - { - canvas.DrawNode(item); - } - } - - protected virtual Rect TransformBounds(Rect bounds) - { - return bounds; - } - - public void AddChild(IGraphicNode item) - { - ArgumentNullException.ThrowIfNull(item); - _children.Add(item); - _isBoundsDirty = true; - } - - public void RemoveChild(IGraphicNode item) - { - ArgumentNullException.ThrowIfNull(item); - _children.Remove(item); - _isBoundsDirty = true; - } - - public void RemoveRange(int index, int count) - { - _children.RemoveRange(index, count); - _isBoundsDirty = _isBoundsDirty || count > 0; - } - - public void SetChild(int index, IGraphicNode item) - { - _children[index]?.Dispose(); - _children[index] = item; - _isBoundsDirty = true; - } - - public void BringFrom(ContainerNode containerNode) - { - _children.Clear(); - _children.AddRange(containerNode._children); - - containerNode._children.Clear(); - _isBoundsDirty = true; - } - - public void Dispose() - { - if (!IsDisposed) - { - OnDispose(true); - IsDisposed = true; - GC.SuppressFinalize(this); - } - } - - protected virtual void OnDispose(bool disposing) - { - foreach (IGraphicNode? item in CollectionsMarshal.AsSpan(_children)) - { - item.Dispose(); - } - - _children.Clear(); - } -} diff --git a/src/Beutl.Engine/Graphics/Rendering/ContainerRenderNode.cs b/src/Beutl.Engine/Graphics/Rendering/ContainerRenderNode.cs new file mode 100644 index 000000000..3cde1d546 --- /dev/null +++ b/src/Beutl.Engine/Graphics/Rendering/ContainerRenderNode.cs @@ -0,0 +1,56 @@ +using System.Runtime.InteropServices; + +namespace Beutl.Graphics.Rendering; + +public class ContainerRenderNode : RenderNode +{ + private readonly List _children = []; + + public IReadOnlyList Children => _children; + + public void AddChild(RenderNode item) + { + ArgumentNullException.ThrowIfNull(item); + _children.Add(item); + } + + public void RemoveChild(RenderNode item) + { + ArgumentNullException.ThrowIfNull(item); + _children.Remove(item); + } + + public void RemoveRange(int index, int count) + { + _children.RemoveRange(index, count); + } + + public void SetChild(int index, RenderNode item) + { + _children[index]?.Dispose(); + _children[index] = item; + } + + public void BringFrom(ContainerRenderNode containerNode) + { + _children.Clear(); + _children.AddRange(containerNode._children); + + containerNode._children.Clear(); + } + + public override RenderNodeOperation[] Process(RenderNodeContext context) + { + return context.Input; + } + + protected override void OnDispose(bool disposing) + { + foreach (RenderNode? item in CollectionsMarshal.AsSpan(_children)) + { + item.Dispose(); + } + + _children.Clear(); + } +} diff --git a/src/Beutl.Engine/Graphics/Rendering/DeferradCanvas.cs b/src/Beutl.Engine/Graphics/Rendering/DeferradCanvas.cs deleted file mode 100644 index b60d0f84b..000000000 --- a/src/Beutl.Engine/Graphics/Rendering/DeferradCanvas.cs +++ /dev/null @@ -1,434 +0,0 @@ -using Beutl.Graphics.Effects; -using Beutl.Media; -using Beutl.Media.Pixel; -using Beutl.Media.Source; -using Beutl.Media.TextFormatting; - -namespace Beutl.Graphics.Rendering; - -public sealed class DeferradCanvas(ContainerNode container, PixelSize canvasSize = default) : ICanvas -{ - private readonly Stack<(ContainerNode, int)> _nodes = []; - private int _drawOperationindex; - - public PixelSize Size => canvasSize; - - public bool IsDisposed => false; - - private void Add(IGraphicNode node) - { - if (_drawOperationindex < container.Children.Count) - { - container.SetChild(_drawOperationindex, node); - } - else - { - container.AddChild(node); - } - } - - private void AddAndPush(ContainerNode node, ContainerNode? old) - { - if (old != null) - { - node.BringFrom(old); - } - - Add(node); - Push(node); - } - - private void Push(ContainerNode node) - { - _nodes.Push((container, _drawOperationindex + 1)); - - _drawOperationindex = 0; - container = node; - } - - private T? Next() where T : class, IGraphicNode - { - return _drawOperationindex < container.Children.Count ? container.Children[_drawOperationindex] as T : null; - } - - private IGraphicNode? Next() - { - return _drawOperationindex < container.Children.Count ? container.Children[_drawOperationindex] : null; - } - - public void Dispose() - { - container.RemoveRange(_drawOperationindex, container.Children.Count - _drawOperationindex); - } - - public void Reset() - { - _drawOperationindex = 0; - _nodes.Clear(); - } - - public void Clear() - { - ClearNode? next = Next(); - - if (next == null || !next.Equals(default)) - { - Add(new ClearNode(default)); - } - - ++_drawOperationindex; - } - - public void Clear(Color color) - { - ClearNode? next = Next(); - - if (next == null || !next.Equals(color)) - { - Add(new ClearNode(color)); - } - - ++_drawOperationindex; - } - - public void DrawImageSource(IImageSource source, IBrush? fill, IPen? pen) - { - ImageSourceNode? next = Next(); - - if (next == null || !next.Equals(source, fill, pen)) - { - Add(new ImageSourceNode(source, ConvertBrush(fill), pen)); - } - - ++_drawOperationindex; - } - - public void DrawVideoSource(IVideoSource source, TimeSpan frame, IBrush? fill, IPen? pen) - { - Rational rate = source.FrameRate; - double frameNum = frame.TotalSeconds * (rate.Numerator / (double)rate.Denominator); - DrawVideoSource(source, (int)frameNum, fill, pen); - } - - public void DrawVideoSource(IVideoSource source, int frame, IBrush? fill, IPen? pen) - { - VideoSourceNode? next = Next(); - - if (next == null || !next.Equals(source, frame, fill, pen)) - { - Add(new VideoSourceNode(source, frame, ConvertBrush(fill), pen)); - } - - ++_drawOperationindex; - } - - public void DrawEllipse(Rect rect, IBrush? fill, IPen? pen) - { - EllipseNode? next = Next(); - - if (next == null || !next.Equals(rect, fill, pen)) - { - Add(new EllipseNode(rect, ConvertBrush(fill), pen)); - } - - ++_drawOperationindex; - } - - public void DrawGeometry(Geometry geometry, IBrush? fill, IPen? pen) - { - GeometryNode? next = Next(); - - if (next == null || !next.Equals(geometry, fill, pen)) - { - Add(new GeometryNode(geometry, ConvertBrush(fill), pen)); - } - - ++_drawOperationindex; - } - - public void DrawRectangle(Rect rect, IBrush? fill, IPen? pen) - { - RectangleNode? next = Next(); - - if (next == null || !next.Equals(rect, fill, pen)) - { - Add(new RectangleNode(rect, ConvertBrush(fill), pen)); - } - - ++_drawOperationindex; - } - - public void DrawText(FormattedText text, IBrush? fill, IPen? pen) - { - TextNode? next = Next(); - - if (next == null || !next.Equals(text, fill, pen)) - { - Add(new TextNode(text, ConvertBrush(fill), pen)); - } - - ++_drawOperationindex; - } - - public void DrawDrawable(Drawable drawable) - { - DrawableNode? next = Next(); - - if (next == null || !ReferenceEquals(next.Drawable, drawable)) - { - AddAndPush(new DrawableNode(drawable), next); - } - else - { - Push(next); - } - - int count = _nodes.Count; - try - { - drawable.Render(this); - } - finally - { - Pop(count); - } - } - - public void DrawNode(IGraphicNode node) - { - IGraphicNode? next = Next(); - - if (next == null || !node.Equals(next)) - { - Add(node); - } - - ++_drawOperationindex; - } - - public void DrawBackdrop(IBackdrop backdrop) - { - DrawBackdropNode? next = Next(); - - var b = new Rect(canvasSize.ToSize(1)); - if (next == null || !next.Equals(backdrop, b)) - { - Add(new DrawBackdropNode(backdrop, b)); - } - - ++_drawOperationindex; - } - - public IBackdrop Snapshot() - { - SnapshotBackdropNode? next = Next(); - - if (next == null) - { - Add(next = new SnapshotBackdropNode()); - } - - ++_drawOperationindex; - return next; - } - - public Bitmap GetBitmap() - { - throw new NotImplementedException(); - } - - public void Pop(int count = -1) - { - if (count < 0) - { - while (count < 0 - && _nodes.TryPop(out (ContainerNode, int) state)) - { - container.RemoveRange(_drawOperationindex, container.Children.Count - _drawOperationindex); - - container = state.Item1; - _drawOperationindex = state.Item2; - - count++; - } - } - else - { - while (_nodes.Count >= count - && _nodes.TryPop(out (ContainerNode, int) state)) - { - container.RemoveRange(_drawOperationindex, container.Children.Count - _drawOperationindex); - - container = state.Item1; - _drawOperationindex = state.Item2; - } - } - } - - public PushedState Push() - { - PushNode? next = Next(); - - if (next == null) - { - AddAndPush(new PushNode(), next); - } - else - { - Push(next); - } - - return new(this, _nodes.Count); - } - - public PushedState PushLayer(Rect limit = default) - { - LayerNode? next = Next(); - - if (next == null || next.Limit != limit) - { - AddAndPush(new LayerNode(limit), next); - } - else - { - Push(next); - } - - return new(this, _nodes.Count); - } - - public PushedState PushBlendMode(BlendMode blendMode) - { - BlendModeNode? next = Next(); - - if (next == null || !next.Equals(blendMode)) - { - AddAndPush(new BlendModeNode(blendMode), next); - } - else - { - Push(next); - } - - return new(this, _nodes.Count); - } - - public PushedState PushClip(Rect clip, ClipOperation operation = ClipOperation.Intersect) - { - RectClipNode? next = Next(); - - if (next == null || !next.Equals(clip, operation)) - { - AddAndPush(new RectClipNode(clip, operation), next); - } - else - { - Push(next); - } - - return new(this, _nodes.Count); - } - - public PushedState PushClip(Geometry geometry, ClipOperation operation = ClipOperation.Intersect) - { - GeometryClipNode? next = Next(); - - if (next == null || !next.Equals(geometry, operation)) - { - AddAndPush(new GeometryClipNode(geometry, operation), next); - } - else - { - Push(next); - } - - return new(this, _nodes.Count); - } - - public PushedState PushOpacity(float opacity) - { - OpacityNode? next = Next(); - - if (next == null || !next.Equals(opacity)) - { - AddAndPush(new OpacityNode(opacity), next); - } - else - { - Push(next); - } - - return new(this, _nodes.Count); - } - - public PushedState PushFilterEffect(FilterEffect effect) - { - FilterEffectNode? next = Next(); - - if (next == null || !next.Equals(effect)) - { - AddAndPush(new FilterEffectNode(effect), next); - } - else - { - Push(next); - } - - return new(this, _nodes.Count); - } - - public PushedState PushOpacityMask(IBrush mask, Rect bounds, bool invert = false) - { - OpacityMaskNode? next = Next(); - - if (next == null || !next.Equals(mask, bounds, invert)) - { - AddAndPush(new OpacityMaskNode(mask, bounds, invert), next); - } - else - { - Push(next); - } - - return new(this, _nodes.Count); - } - - public PushedState PushTransform(Matrix matrix, TransformOperator transformOperator = TransformOperator.Prepend) - { - TransformNode? next = Next(); - - if (next == null || !next.Equals(matrix, transformOperator)) - { - AddAndPush(new TransformNode(matrix, transformOperator), next); - } - else - { - Push(next); - } - - return new(this, _nodes.Count); - } - - private static IBrush? ConvertBrush(IBrush? brush) - { - if (brush is IDrawableBrush drawableBrush) - { - RenderScene? scene = null; - Rect bounds = default; - if (drawableBrush is { Drawable: { IsVisible: true } drawable }) - { - drawable.Measure(Graphics.Size.Infinity); - - bounds = drawable.Bounds; - scene = new RenderScene(bounds.Size.Ceiling()); - scene[0].UpdateAll(new[] { drawable }); - } - - return new RenderSceneBrush(drawableBrush, scene, bounds); - } - else - { - return brush; - } - } -} diff --git a/src/Beutl.Engine/Graphics/Rendering/DrawBackdropNode.cs b/src/Beutl.Engine/Graphics/Rendering/DrawBackdropNode.cs deleted file mode 100644 index e184d5a61..000000000 --- a/src/Beutl.Engine/Graphics/Rendering/DrawBackdropNode.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Beutl.Graphics.Rendering.Cache; - -namespace Beutl.Graphics.Rendering; - -public class DrawBackdropNode(IBackdrop backdrop, Rect bounds) : DrawNode(bounds), ISupportRenderCache -{ - public IBackdrop Backdrop { get; } = backdrop; - - public bool Equals(IBackdrop backdrop, Rect bounds) - { - return Backdrop == backdrop - && Bounds == bounds; - } - - public override bool HitTest(Point point) - { - return Bounds.Contains(point); - } - - public override void Render(ImmediateCanvas canvas) - { - Backdrop.Draw(canvas); - } - - void ISupportRenderCache.Accepts(RenderCache cache) - { - cache.ReportRenderCount(0); - } - - void ISupportRenderCache.CreateCache(IImmediateCanvasFactory factory, RenderCache cache, RenderCacheContext context) - { - } - - void ISupportRenderCache.RenderWithCache(ImmediateCanvas canvas, RenderCache cache) - { - } -} diff --git a/src/Beutl.Engine/Graphics/Rendering/DrawBackdropRenderNode.cs b/src/Beutl.Engine/Graphics/Rendering/DrawBackdropRenderNode.cs new file mode 100644 index 000000000..5f02e2036 --- /dev/null +++ b/src/Beutl.Engine/Graphics/Rendering/DrawBackdropRenderNode.cs @@ -0,0 +1,22 @@ +namespace Beutl.Graphics.Rendering; + +public class DrawBackdropRenderNode(IBackdrop backdrop, Rect bounds) : RenderNode() +{ + public IBackdrop Backdrop { get; } = backdrop; + + public Rect Bounds { get; } = bounds; + + public bool Equals(IBackdrop backdrop, Rect bounds) + { + return Backdrop == backdrop && Bounds == bounds; + } + + public override RenderNodeOperation[] Process(RenderNodeContext context) + { + context.IsRenderCacheEnabled = false; + return + [ + RenderNodeOperation.CreateLambda(Bounds, canvas => Backdrop.Draw(canvas), Bounds.Contains) + ]; + } +} diff --git a/src/Beutl.Engine/Graphics/Rendering/DrawableNode.cs b/src/Beutl.Engine/Graphics/Rendering/DrawableRenderNode.cs similarity index 76% rename from src/Beutl.Engine/Graphics/Rendering/DrawableNode.cs rename to src/Beutl.Engine/Graphics/Rendering/DrawableRenderNode.cs index eaf02a7d8..279966149 100644 --- a/src/Beutl.Engine/Graphics/Rendering/DrawableNode.cs +++ b/src/Beutl.Engine/Graphics/Rendering/DrawableRenderNode.cs @@ -1,6 +1,6 @@ namespace Beutl.Graphics.Rendering; -public class DrawableNode(Drawable drawable) : ContainerNode +public class DrawableRenderNode(Drawable drawable) : ContainerRenderNode { public Drawable Drawable { get; private set; } = drawable; diff --git a/src/Beutl.Engine/Graphics/Rendering/EllipseNode.cs b/src/Beutl.Engine/Graphics/Rendering/EllipseRenderNode.cs similarity index 72% rename from src/Beutl.Engine/Graphics/Rendering/EllipseNode.cs rename to src/Beutl.Engine/Graphics/Rendering/EllipseRenderNode.cs index dab935db3..85526e4c4 100644 --- a/src/Beutl.Engine/Graphics/Rendering/EllipseNode.cs +++ b/src/Beutl.Engine/Graphics/Rendering/EllipseRenderNode.cs @@ -2,25 +2,32 @@ namespace Beutl.Graphics.Rendering; -public sealed class EllipseNode(Rect rect, IBrush? fill, IPen? pen) - : BrushDrawNode(fill, pen, PenHelper.GetBounds(rect, pen)) +public sealed class EllipseRenderNode(Rect rect, IBrush? fill, IPen? pen) + : BrushRenderNode(fill, pen) { public Rect Rect { get; } = rect; public bool Equals(Rect rect, IBrush? fill, IPen? pen) { return Rect == rect - && EqualityComparer.Default.Equals(Fill, fill) - && EqualityComparer.Default.Equals(Pen, pen); + && EqualityComparer.Default.Equals(Fill, fill) + && EqualityComparer.Default.Equals(Pen, pen); } - public override void Render(ImmediateCanvas canvas) + public override RenderNodeOperation[] Process(RenderNodeContext context) { - canvas.DrawEllipse(Rect, Fill, Pen); + return + [ + RenderNodeOperation.CreateLambda( + PenHelper.GetBounds(Rect, Pen), + canvas => canvas.DrawEllipse(Rect, Fill, Pen), + HitTest + ) + ]; } //https://github.com/AvaloniaUI/Avalonia/blob/release/0.10.21/src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs - public override bool HitTest(Point point) + private bool HitTest(Point point) { Point center = Rect.Center; diff --git a/src/Beutl.Engine/Graphics/Rendering/FilterEffectNode.cs b/src/Beutl.Engine/Graphics/Rendering/FilterEffectNode.cs deleted file mode 100644 index b15525f4d..000000000 --- a/src/Beutl.Engine/Graphics/Rendering/FilterEffectNode.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System.Collections.Immutable; -using System.Diagnostics; -using Beutl.Graphics.Effects; -using Beutl.Graphics.Rendering.Cache; -using Beutl.Media; -using Beutl.Media.Source; -using SkiaSharp; - -namespace Beutl.Graphics.Rendering; - -public sealed class FilterEffectNode : ContainerNode, ISupportRenderCache -{ - private FilterEffectNodeComparer _comparer; - private FilterEffectContext? _prevContext; - - private int? _prevVersion; - private Rect _rect = Rect.Invalid; - - public FilterEffectNode(FilterEffect filterEffect) - { - FilterEffect = filterEffect; - _comparer = new(this); - } - - public FilterEffect FilterEffect { get; } - - protected override void OnDispose(bool disposing) - { - base.OnDispose(disposing); - _prevContext?.Dispose(); - _prevContext = null; - _prevVersion = null; - } - - public override bool HitTest(Point point) - { - if (_prevContext?.CountItems() > 0) - { - return Bounds.Contains(point); - } - else - { - return base.HitTest(point); - } - } - - public bool Equals(FilterEffect filterEffect) - { - return FilterEffect == filterEffect; - } - - protected override Rect TransformBounds(Rect bounds) - { - Rect r = _rect; - if (!bounds.IsInvalid) - { - r = FilterEffect.TransformBounds(bounds); - } - - if (r.IsInvalid) - { - r = bounds; - } - - return r; - } - - private FilterEffectContext GetOrCreateContext() - { - FilterEffectContext? context = _prevContext; - if (context == null - || _prevVersion != FilterEffect.Version) - { - context = new FilterEffectContext(Children.Count == 1 ? OriginalBounds : Rect.Invalid); - context.Apply(FilterEffect); - _prevContext?.Dispose(); - _prevContext = context; - _prevVersion = FilterEffect.Version; - } - - return context; - } - - private void RenderCore( - ImmediateCanvas canvas, - int offset, int? count, - EffectTargets effectTargets) - { - FilterEffectContext context = GetOrCreateContext(); - - using (var builder = new SKImageFilterBuilder()) - using (var activator = new FilterEffectActivator(effectTargets, builder, canvas)) - { - activator.Apply(context, offset, count); - - if (builder.HasFilter()) - { - using (var paint = new SKPaint()) - { - paint.ImageFilter = builder.GetFilter(); - - foreach (EffectTarget t in activator.CurrentTargets) - { - using (canvas.PushBlendMode(BlendMode.SrcOver)) - using (canvas.PushTransform(Matrix.CreateTranslation(t.Bounds.X - t.OriginalBounds.X, - t.Bounds.Y - t.OriginalBounds.Y))) - using (canvas.PushPaint(paint)) - { - t.Draw(canvas); - } - } - } - } - else - { - foreach (var t in activator.CurrentTargets) - { - if (t.Surface != null) - { - canvas.DrawSurface(t.Surface.Value, t.Bounds.Position); - } - else if (t.Node == this - || !t.IsEmpty) - { - base.Render(canvas); - } - } - } - - _rect = activator.CurrentTargets.CalculateBounds(); - - _comparer.OnRender(activator, offset, count); - } - } - - public override void Render(ImmediateCanvas canvas) - { - using (EffectTargets targets = [.. Children.Select(v => new EffectTarget(v))]) - { - RenderCore(canvas, 0, null, targets); - } - } - - void ISupportRenderCache.Accepts(RenderCache cache) - { - _comparer.Accepts(cache); - } - - void ISupportRenderCache.CreateCache(IImmediateCanvasFactory factory, RenderCache cache, RenderCacheContext context) - { - int minNumber = cache.GetMinNumber(); - - FilterEffectContext fecontext = GetOrCreateContext(); - - using (EffectTargets targets = [.. Children.Select(v => new EffectTarget(v))]) - using (var builder = new SKImageFilterBuilder()) - using (var activator = new FilterEffectActivator(targets, builder, factory)) - { - activator.Apply(fecontext, 0, minNumber); - activator.Flush(false); - - if (targets.Any(t => !context.CacheOptions.Rules.Match(PixelRect.FromRect(t.Bounds).Size))) - return; - - // nodeの子要素のキャッシュをすべて削除 - context.ClearCache(this, cache); - - cache.StoreCache([ - .. activator.CurrentTargets - .Select(i => (i.Surface, i.Bounds)) - .Where(i => i.Item1 != null) - ]); - } - - Debug.WriteLine($"[RenderCache:Created] '{this}[0..{minNumber}]'"); - } - - void ISupportRenderCache.RenderWithCache(ImmediateCanvas canvas, RenderCache cache) - { - int minNumber = cache.GetMinNumber(); - FilterEffectContext context = GetOrCreateContext(); - - (Ref Surface, Rect Bounds)[] cacheItems = cache.UseCache(); - try - { - using (var targets = new EffectTargets()) - { - targets.AddRange(cacheItems.Select(i => - { - SKSurface srcSurface = i.Surface.Value; - SKRectI rect = srcSurface.Canvas.DeviceClipBounds; - SKSurface newSurface = canvas.CreateRenderTarget(rect.Width, rect.Height)!; - newSurface.Canvas.DrawSurface(srcSurface, default); - - using var surfaceRef = Ref.Create(newSurface); - return new EffectTarget(surfaceRef, i.Bounds) { OriginalBounds = i.Bounds.WithX(0).WithY(0) }; - })); - - RenderCore(canvas, minNumber, null, targets); - } - } - finally - { - foreach ((Ref s, _) in cacheItems) - { - s.Dispose(); - } - } - } -} diff --git a/src/Beutl.Engine/Graphics/Rendering/FilterEffectRenderNode.cs b/src/Beutl.Engine/Graphics/Rendering/FilterEffectRenderNode.cs new file mode 100644 index 000000000..5baac86ae --- /dev/null +++ b/src/Beutl.Engine/Graphics/Rendering/FilterEffectRenderNode.cs @@ -0,0 +1,67 @@ +using Beutl.Graphics.Effects; +using SkiaSharp; + +namespace Beutl.Graphics.Rendering; + +public sealed class FilterEffectRenderNode(FilterEffect filterEffect) : ContainerRenderNode +{ + private readonly int _version = filterEffect.Version; + + public override RenderNodeOperation[] Process(RenderNodeContext context) + { + using var feContext = new FilterEffectContext(context.CalculateBounds()); + feContext.Apply(FilterEffect); + var effectTargets = new EffectTargets(); + effectTargets.AddRange(context.Input.Select(i => new EffectTarget(i))); + + using (var builder = new SKImageFilterBuilder()) + using (var activator = new FilterEffectActivator(effectTargets, builder, context.CanvasFactory)) + { + activator.Apply(feContext); + + if (builder.HasFilter()) + { + var imageFilter = builder.GetFilter(); + return activator.CurrentTargets.Select(t => + { + var paint = new SKPaint(); + paint.ImageFilter = imageFilter; + return RenderNodeOperation.CreateLambda( + bounds: t.Bounds, + render: canvas => + { + using (canvas.PushBlendMode(BlendMode.SrcOver)) + using (canvas.PushTransform(Matrix.CreateTranslation( + t.Bounds.X - t.OriginalBounds.X, + t.Bounds.Y - t.OriginalBounds.Y))) + using (canvas.PushPaint(paint)) + { + t.Draw(canvas); + } + }, + hitTest: t.Bounds.Contains, + onDispose: () => + { + t.Dispose(); + paint.Dispose(); + } + ); + }).ToArray(); + } + else + { + return activator.CurrentTargets.Select(i => + i.NodeOperation ?? + RenderNodeOperation.CreateFromSurface(i.Bounds, i.Bounds.Position, i.Surface!)) + .ToArray(); + } + } + } + + public FilterEffect FilterEffect { get; } = filterEffect; + + public bool Equals(FilterEffect filterEffect) + { + return FilterEffect == filterEffect && _version == filterEffect.Version; + } +} diff --git a/src/Beutl.Engine/Graphics/Rendering/FpsText.cs b/src/Beutl.Engine/Graphics/Rendering/FpsText.cs index ac33ee25f..e5878b9fa 100644 --- a/src/Beutl.Engine/Graphics/Rendering/FpsText.cs +++ b/src/Beutl.Engine/Graphics/Rendering/FpsText.cs @@ -63,7 +63,7 @@ public void Dispose() using (canvas.PushTransform(Matrix.CreateTranslation(width / 2, height / 2))) { canvas.DrawRectangle(fpsText._textBlock.Bounds, s_background, null); - fpsText._textBlock.Render(canvas); + canvas.DrawDrawable(fpsText._textBlock); } } } diff --git a/src/Beutl.Engine/Graphics/Rendering/GeometryClipNode.cs b/src/Beutl.Engine/Graphics/Rendering/GeometryClipRenderNode.cs similarity index 54% rename from src/Beutl.Engine/Graphics/Rendering/GeometryClipNode.cs rename to src/Beutl.Engine/Graphics/Rendering/GeometryClipRenderNode.cs index 384ccfeaf..6f6075651 100644 --- a/src/Beutl.Engine/Graphics/Rendering/GeometryClipNode.cs +++ b/src/Beutl.Engine/Graphics/Rendering/GeometryClipRenderNode.cs @@ -2,7 +2,7 @@ namespace Beutl.Graphics.Rendering; -public sealed class GeometryClipNode(Geometry clip, ClipOperation operation) : ContainerNode +public sealed class GeometryClipRenderNode(Geometry clip, ClipOperation operation) : ContainerRenderNode { private readonly int _version = clip.Version; @@ -17,12 +17,18 @@ public bool Equals(Geometry clip, ClipOperation operation) && Operation == operation; } - public override void Render(ImmediateCanvas canvas) + public override RenderNodeOperation[] Process(RenderNodeContext context) { - using (canvas.PushClip(Clip, Operation)) + return context.Input.Select(r => { - base.Render(canvas); - } + return RenderNodeOperation.CreateDecorator(r, canvas => + { + using (canvas.PushClip(Clip, Operation)) + { + r.Render(canvas); + } + }); + }).ToArray(); } protected override void OnDispose(bool disposing) diff --git a/src/Beutl.Engine/Graphics/Rendering/GeometryNode.cs b/src/Beutl.Engine/Graphics/Rendering/GeometryNode.cs deleted file mode 100644 index 2075ff878..000000000 --- a/src/Beutl.Engine/Graphics/Rendering/GeometryNode.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Beutl.Media; - -namespace Beutl.Graphics.Rendering; - -public sealed class GeometryNode(Geometry geometry, IBrush? fill, IPen? pen) - : BrushDrawNode(fill, pen, PenHelper.CalculateBoundsWithStrokeCap(geometry.GetRenderBounds(pen), pen)) -{ - private readonly int _version = geometry.Version; - - public Geometry Geometry { get; private set; } = geometry; - - public bool Equals(Geometry geometry, IBrush? fill, IPen? pen) - { - return Geometry == geometry - && _version == geometry.Version - && EqualityComparer.Default.Equals(Fill, fill) - && EqualityComparer.Default.Equals(Pen, pen); - } - - public override void Render(ImmediateCanvas canvas) - { - canvas.DrawGeometry(Geometry, Fill, Pen); - } - - protected override void OnDispose(bool disposing) - { - base.OnDispose(disposing); - Geometry = null!; - } - - public override bool HitTest(Point point) - { - return (Fill != null && Geometry.FillContains(point)) - || (Pen != null && Geometry.StrokeContains(Pen, point)); - } -} diff --git a/src/Beutl.Engine/Graphics/Rendering/GeometryRenderNode.cs b/src/Beutl.Engine/Graphics/Rendering/GeometryRenderNode.cs new file mode 100644 index 000000000..408af2998 --- /dev/null +++ b/src/Beutl.Engine/Graphics/Rendering/GeometryRenderNode.cs @@ -0,0 +1,43 @@ +using Beutl.Media; + +namespace Beutl.Graphics.Rendering; + +public sealed class GeometryRenderNode(Geometry geometry, IBrush? fill, IPen? pen) + : BrushRenderNode(fill, pen) +{ + private readonly int _version = geometry.Version; + + public Geometry Geometry { get; private set; } = geometry; + + public bool Equals(Geometry geometry, IBrush? fill, IPen? pen) + { + return Geometry == geometry + && _version == geometry.Version + && EqualityComparer.Default.Equals(Fill, fill) + && EqualityComparer.Default.Equals(Pen, pen); + } + + public override RenderNodeOperation[] Process(RenderNodeContext context) + { + return + [ + RenderNodeOperation.CreateLambda( + bounds: PenHelper.CalculateBoundsWithStrokeCap(Geometry.GetRenderBounds(Pen), Pen), + render: canvas => canvas.DrawGeometry(Geometry, Fill, Pen), + hitTest: HitTest + ) + ]; + } + + protected override void OnDispose(bool disposing) + { + base.OnDispose(disposing); + Geometry = null!; + } + + private bool HitTest(Point point) + { + return (Fill != null && Geometry.FillContains(point)) + || (Pen != null && Geometry.StrokeContains(Pen, point)); + } +} diff --git a/src/Beutl.Engine/Graphics/Rendering/GraphicsContext2D.cs b/src/Beutl.Engine/Graphics/Rendering/GraphicsContext2D.cs new file mode 100644 index 000000000..202fc4809 --- /dev/null +++ b/src/Beutl.Engine/Graphics/Rendering/GraphicsContext2D.cs @@ -0,0 +1,527 @@ +using Beutl.Graphics.Effects; +using Beutl.Graphics.Transformation; +using Beutl.Media; +using Beutl.Media.Source; +using Beutl.Media.TextFormatting; + +namespace Beutl.Graphics.Rendering; + +public sealed class GraphicsContext2D(ContainerRenderNode container, PixelSize canvasSize = default) + : IDisposable, IPopable +{ + private readonly Stack<(ContainerRenderNode, int)> _nodes = []; + private int _drawOperationindex; + private ContainerRenderNode _container = container; + + public PixelSize Size => canvasSize; + + internal Action? OnUntracked { get; set; } + + private void Untracked(RenderNode? node) + { + if (node != null) OnUntracked?.Invoke(node); + } + + private void Add(RenderNode node) + { + if (_drawOperationindex < _container.Children.Count) + { + Untracked(_container.Children[_drawOperationindex]); + _container.SetChild(_drawOperationindex, node); + } + else + { + _container.AddChild(node); + } + } + + private void AddAndPush(ContainerRenderNode node, ContainerRenderNode? old) + { + if (old != null) + { + node.BringFrom(old); + } + + Add(node); + Push(node); + } + + private void Push(ContainerRenderNode node) + { + _nodes.Push((_container, _drawOperationindex + 1)); + + _drawOperationindex = 0; + _container = node; + } + + private T? Next() where T : RenderNode + { + return _drawOperationindex < _container.Children.Count ? _container.Children[_drawOperationindex] as T : null; + } + + private RenderNode? Next() + { + return _drawOperationindex < _container.Children.Count ? _container.Children[_drawOperationindex] : null; + } + + public void Dispose() + { + _container.RemoveRange(_drawOperationindex, _container.Children.Count - _drawOperationindex); + } + + public void Reset() + { + _drawOperationindex = 0; + _nodes.Clear(); + } + + public void Clear() + { + ClearRenderNode? next = Next(); + + if (next == null || !next.Equals(default)) + { + Add(new ClearRenderNode(default)); + } + + ++_drawOperationindex; + } + + public void Clear(Color color) + { + ClearRenderNode? next = Next(); + + if (next == null || !next.Equals(color)) + { + Add(new ClearRenderNode(color)); + } + + ++_drawOperationindex; + } + + public void DrawImageSource(IImageSource source, IBrush? fill, IPen? pen) + { + ImageSourceRenderNode? next = Next(); + + if (next == null || !next.Equals(source, fill, pen)) + { + Add(new ImageSourceRenderNode(source, ConvertBrush(fill), pen)); + } + + ++_drawOperationindex; + } + + public void DrawVideoSource(IVideoSource source, TimeSpan frame, IBrush? fill, IPen? pen) + { + Rational rate = source.FrameRate; + double frameNum = frame.TotalSeconds * (rate.Numerator / (double)rate.Denominator); + DrawVideoSource(source, (int)frameNum, fill, pen); + } + + public void DrawVideoSource(IVideoSource source, int frame, IBrush? fill, IPen? pen) + { + VideoSourceRenderNode? next = Next(); + + if (next == null || !next.Equals(source, frame, fill, pen)) + { + Add(new VideoSourceRenderNode(source, frame, ConvertBrush(fill), pen)); + } + + ++_drawOperationindex; + } + + public void DrawEllipse(Rect rect, IBrush? fill, IPen? pen) + { + EllipseRenderNode? next = Next(); + + if (next == null || !next.Equals(rect, fill, pen)) + { + Add(new EllipseRenderNode(rect, ConvertBrush(fill), pen)); + } + + ++_drawOperationindex; + } + + public void DrawGeometry(Geometry geometry, IBrush? fill, IPen? pen) + { + GeometryRenderNode? next = Next(); + + if (next == null || !next.Equals(geometry, fill, pen)) + { + Add(new GeometryRenderNode(geometry, ConvertBrush(fill), pen)); + } + + ++_drawOperationindex; + } + + public void DrawRectangle(Rect rect, IBrush? fill, IPen? pen) + { + RectangleRenderNode? next = Next(); + + if (next == null || !next.Equals(rect, fill, pen)) + { + Add(new RectangleRenderNode(rect, ConvertBrush(fill), pen)); + } + + ++_drawOperationindex; + } + + public void DrawText(FormattedText text, IBrush? fill, IPen? pen) + { + TextRenderNode? next = Next(); + + if (next == null || !next.Equals(text, fill, pen)) + { + Add(new TextRenderNode(text, ConvertBrush(fill), pen)); + } + + ++_drawOperationindex; + } + + public void DrawDrawable(Drawable drawable) + { + DrawableRenderNode? next = Next(); + + if (next == null || !ReferenceEquals(next.Drawable, drawable)) + { + AddAndPush(new DrawableRenderNode(drawable), next); + } + else + { + Push(next); + } + + int count = _nodes.Count; + try + { + drawable.Render(this); + } + finally + { + Pop(count); + } + } + + public void DrawNode(RenderNode node) + { + RenderNode? next = Next(); + + if (next == null || !node.Equals(next)) + { + Add(node); + } + + ++_drawOperationindex; + } + + public void DrawBackdrop(IBackdrop backdrop) + { + DrawBackdropRenderNode? next = Next(); + + var b = new Rect(canvasSize.ToSize(1)); + if (next == null || !next.Equals(backdrop, b)) + { + Add(new DrawBackdropRenderNode(backdrop, b)); + } + + ++_drawOperationindex; + } + + public IBackdrop Snapshot() + { + SnapshotBackdropRenderNode? next = Next(); + + if (next == null) + { + Add(next = new SnapshotBackdropRenderNode()); + } + + ++_drawOperationindex; + return next; + } + + public void Pop(int count = -1) + { + if (count < 0) + { + while (count < 0 + && _nodes.TryPop(out (ContainerRenderNode, int) state)) + { + foreach (RenderNode node in _container.Children.Take(_drawOperationindex..)) + { + node.Dispose(); + Untracked(node); + } + + _container.RemoveRange(_drawOperationindex, _container.Children.Count - _drawOperationindex); + + _container = state.Item1; + _drawOperationindex = state.Item2; + + count++; + } + } + else + { + while (_nodes.Count >= count + && _nodes.TryPop(out (ContainerRenderNode, int) state)) + { + foreach (RenderNode node in _container.Children.Take(_drawOperationindex..)) + { + node.Dispose(); + Untracked(node); + } + + _container.RemoveRange(_drawOperationindex, _container.Children.Count - _drawOperationindex); + + _container = state.Item1; + _drawOperationindex = state.Item2; + } + } + } + + public PushedState Push() + { + PushRenderNode? next = Next(); + + if (next == null) + { + AddAndPush(new PushRenderNode(), next); + } + else + { + Push(next); + } + + return new(this, _nodes.Count); + } + + public PushedState PushLayer(Rect limit = default) + { + LayerRenderNode? next = Next(); + + if (next == null || next.Limit != limit) + { + AddAndPush(new LayerRenderNode(limit), next); + } + else + { + Push(next); + } + + return new(this, _nodes.Count); + } + + public PushedState PushBlendMode(BlendMode blendMode) + { + BlendModeRenderNode? next = Next(); + + if (next == null || !next.Equals(blendMode)) + { + AddAndPush(new BlendModeRenderNode(blendMode), next); + } + else + { + Push(next); + } + + return new(this, _nodes.Count); + } + + public PushedState PushClip(Rect clip, ClipOperation operation = ClipOperation.Intersect) + { + RectClipRenderNode? next = Next(); + + if (next == null || !next.Equals(clip, operation)) + { + AddAndPush(new RectClipRenderNode(clip, operation), next); + } + else + { + Push(next); + } + + return new(this, _nodes.Count); + } + + public PushedState PushClip(Geometry geometry, ClipOperation operation = ClipOperation.Intersect) + { + GeometryClipRenderNode? next = Next(); + + if (next == null || !next.Equals(geometry, operation)) + { + AddAndPush(new GeometryClipRenderNode(geometry, operation), next); + } + else + { + Push(next); + } + + return new(this, _nodes.Count); + } + + public PushedState PushOpacity(float opacity) + { + OpacityRenderNode? next = Next(); + + if (next == null || !next.Equals(opacity)) + { + AddAndPush(new OpacityRenderNode(opacity), next); + } + else + { + Push(next); + } + + return new(this, _nodes.Count); + } + + public PushedState PushFilterEffect(FilterEffect effect) + { + switch (effect) + { + case FilterEffectGroup group: + { + for (int i = group.Children.Count - 1; i >= 0; i--) + { + FilterEffect item = group.Children[i]; + PushFilterEffect(item); + } + + break; + } +#pragma warning disable CS0618 + case CombinedFilterEffect combined: +#pragma warning restore CS0618 + { + if (combined.Second != null) + PushFilterEffect(combined.Second); + + if (combined.First != null) + PushFilterEffect(combined.First); + + break; + } + default: + { + FilterEffectRenderNode? next = Next(); + + if (next == null || !next.Equals(effect)) + { + AddAndPush(new FilterEffectRenderNode(effect), next); + } + else + { + Push(next); + } + + break; + } + } + + return new(this, _nodes.Count); + } + + public PushedState PushOpacityMask(IBrush mask, Rect bounds, bool invert = false) + { + OpacityMaskRenderNode? next = Next(); + + if (next == null || !next.Equals(mask, bounds, invert)) + { + AddAndPush(new OpacityMaskRenderNode(mask, bounds, invert), next); + } + else + { + Push(next); + } + + return new(this, _nodes.Count); + } + + public PushedState PushTransform(Matrix matrix, TransformOperator transformOperator = TransformOperator.Prepend) + { + TransformRenderNode? next = Next(); + + if (next == null || !next.Equals(matrix, transformOperator)) + { + AddAndPush(new TransformRenderNode(matrix, transformOperator), next); + } + else + { + Push(next); + } + + return new(this, _nodes.Count); + } + + public PushedState PushTransform(ITransform transform, + TransformOperator transformOperator = TransformOperator.Prepend) + { + switch (transform) + { + case TransformGroup group: + { + for (int i = group.Children.Count - 1; i >= 0; i--) + { + ITransform item = group.Children[i]; + PushTransform(item, transformOperator); + } + + break; + } +#pragma warning disable CS0618 + case MultiTransform multi: +#pragma warning restore CS0618 + { + if (multi.Left != null) + PushTransform(multi.Left, transformOperator); + + if (multi.Right != null) + PushTransform(multi.Right, transformOperator); + + break; + } + + default: + { + TransformRenderNode? next = Next(); + var matrix = transform.Value; + if (next == null || !next.Equals(matrix, transformOperator)) + { + AddAndPush(new TransformRenderNode(matrix, transformOperator), next); + } + else + { + Push(next); + } + + break; + } + } + + return new(this, _nodes.Count); + } + + private static IBrush? ConvertBrush(IBrush? brush) + { + if (brush is IDrawableBrush drawableBrush) + { + RenderScene? scene = null; + Rect bounds = default; + if (drawableBrush is { Drawable: { IsVisible: true } drawable }) + { + drawable.Measure(Graphics.Size.Infinity); + + bounds = drawable.Bounds; + scene = new RenderScene(bounds.Size.Ceiling()); + scene[0].UpdateAll([drawable]); + } + + return new RenderSceneBrush(drawableBrush, scene, bounds); + } + else + { + return brush; + } + } +} diff --git a/src/Beutl.Engine/Graphics/Rendering/IGraphicNode.cs b/src/Beutl.Engine/Graphics/Rendering/IGraphicNode.cs deleted file mode 100644 index 073ab69b8..000000000 --- a/src/Beutl.Engine/Graphics/Rendering/IGraphicNode.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Beutl.Graphics.Rendering; - -public interface IGraphicNode : INode -{ - Rect Bounds { get; } - - bool HitTest(Point point); - - void Render(ImmediateCanvas canvas); -} diff --git a/src/Beutl.Engine/Graphics/Rendering/ImageSourceNode.cs b/src/Beutl.Engine/Graphics/Rendering/ImageSourceRenderNode.cs similarity index 59% rename from src/Beutl.Engine/Graphics/Rendering/ImageSourceNode.cs rename to src/Beutl.Engine/Graphics/Rendering/ImageSourceRenderNode.cs index 3005e8315..e70300432 100644 --- a/src/Beutl.Engine/Graphics/Rendering/ImageSourceNode.cs +++ b/src/Beutl.Engine/Graphics/Rendering/ImageSourceRenderNode.cs @@ -3,11 +3,13 @@ namespace Beutl.Graphics.Rendering; -public sealed class ImageSourceNode(IImageSource source, IBrush? fill, IPen? pen) - : BrushDrawNode(fill, pen, PenHelper.GetBounds(new Rect(default, source.FrameSize.ToSize(1)), pen)) +public sealed class ImageSourceRenderNode(IImageSource source, IBrush? fill, IPen? pen) + : BrushRenderNode(fill, pen) { public IImageSource Source { get; } = source.Clone(); + public Rect Bounds { get; } = PenHelper.GetBounds(new Rect(default, source.FrameSize.ToSize(1)), pen); + public bool Equals(IImageSource source, IBrush? fill, IPen? pen) { return EqualityComparer.Default.Equals(Source, source) @@ -15,15 +17,24 @@ public bool Equals(IImageSource source, IBrush? fill, IPen? pen) && EqualityComparer.Default.Equals(Pen, pen); } - public override void Render(ImmediateCanvas canvas) + public override RenderNodeOperation[] Process(RenderNodeContext context) { - if (Source.TryGetRef(out Ref? bitmap)) - { - using (bitmap) - { - canvas.DrawBitmap(bitmap.Value, Fill, Pen); - } - } + return + [ + RenderNodeOperation.CreateLambda( + bounds: Bounds, + render: canvas => + { + if (!Source.TryGetRef(out Ref? bitmap)) return; + + using (bitmap) + { + canvas.DrawBitmap(bitmap.Value, Fill, Pen); + } + }, + hitTest: HitTest + ) + ]; } protected override void OnDispose(bool disposing) @@ -32,7 +43,7 @@ protected override void OnDispose(bool disposing) Source.Dispose(); } - public override bool HitTest(Point point) + private bool HitTest(Point point) { StrokeAlignment alignment = Pen?.StrokeAlignment ?? StrokeAlignment.Inside; float thickness = Pen?.Thickness ?? 0; diff --git a/src/Beutl.Engine/Graphics/Rendering/LayerNode.cs b/src/Beutl.Engine/Graphics/Rendering/LayerNode.cs deleted file mode 100644 index 2c9cdd07a..000000000 --- a/src/Beutl.Engine/Graphics/Rendering/LayerNode.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Beutl.Graphics.Rendering; - -public class LayerNode(Rect limit) : ContainerNode -{ - public Rect Limit { get; } = limit; - - public override void Render(ImmediateCanvas canvas) - { - using (canvas.PushLayer(Limit)) - { - base.Render(canvas); - } - } -} diff --git a/src/Beutl.Engine/Graphics/Rendering/LayerRenderNode.cs b/src/Beutl.Engine/Graphics/Rendering/LayerRenderNode.cs new file mode 100644 index 000000000..3b0d784af --- /dev/null +++ b/src/Beutl.Engine/Graphics/Rendering/LayerRenderNode.cs @@ -0,0 +1,37 @@ +namespace Beutl.Graphics.Rendering; + +public class LayerRenderNode(Rect limit) : ContainerRenderNode +{ + public Rect Limit { get; } = limit; + + public bool Equals(Rect limit) + { + return Limit == limit; + } + + public override RenderNodeOperation[] Process(RenderNodeContext context) + { + return + [ + RenderNodeOperation.CreateLambda( + bounds: context.CalculateBounds(), + render: canvas => + { + using (canvas.PushLayer(Limit)) + { + foreach (RenderNodeOperation op in context.Input) + { + op.Render(canvas); + } + } + }, + hitTest: p => context.Input.Any(n => n.HitTest(p)), onDispose: () => + { + foreach (RenderNodeOperation op in context.Input) + { + op.Dispose(); + } + }) + ]; + } +} diff --git a/src/Beutl.Engine/Graphics/Rendering/OpacityMaskNode.cs b/src/Beutl.Engine/Graphics/Rendering/OpacityMaskRenderNode.cs similarity index 55% rename from src/Beutl.Engine/Graphics/Rendering/OpacityMaskNode.cs rename to src/Beutl.Engine/Graphics/Rendering/OpacityMaskRenderNode.cs index f6e9cf970..06480684c 100644 --- a/src/Beutl.Engine/Graphics/Rendering/OpacityMaskNode.cs +++ b/src/Beutl.Engine/Graphics/Rendering/OpacityMaskRenderNode.cs @@ -2,7 +2,7 @@ namespace Beutl.Graphics.Rendering; -public sealed class OpacityMaskNode(IBrush mask, Rect maskBounds, bool invert) : ContainerNode +public sealed class OpacityMaskRenderNode(IBrush mask, Rect maskBounds, bool invert) : ContainerRenderNode { public IBrush Mask { get; private set; } = (mask as IMutableBrush)?.ToImmutable() ?? mask; @@ -17,12 +17,18 @@ public bool Equals(IBrush? mask, Rect maskBounds, bool invert) && Invert == invert; } - public override void Render(ImmediateCanvas canvas) + public override RenderNodeOperation[] Process(RenderNodeContext context) { - using (canvas.PushOpacityMask(Mask, MaskBounds, Invert)) + return context.Input.Select(r => { - base.Render(canvas); - } + return RenderNodeOperation.CreateDecorator(r, canvas => + { + using (canvas.PushOpacityMask(Mask, MaskBounds, Invert)) + { + r.Render(canvas); + } + }); + }).ToArray(); } protected override void OnDispose(bool disposing) diff --git a/src/Beutl.Engine/Graphics/Rendering/OpacityNode.cs b/src/Beutl.Engine/Graphics/Rendering/OpacityNode.cs deleted file mode 100644 index 049b799a2..000000000 --- a/src/Beutl.Engine/Graphics/Rendering/OpacityNode.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Beutl.Graphics.Rendering; - -public sealed class OpacityNode(float opacity) : ContainerNode -{ - public float Opacity { get; } = opacity; - - public bool Equals(float opacity) - { - return Opacity == opacity; - } - - public override void Render(ImmediateCanvas canvas) - { - using (canvas.PushOpacity(Opacity)) - { - base.Render(canvas); - } - } -} diff --git a/src/Beutl.Engine/Graphics/Rendering/OpacityRenderNode.cs b/src/Beutl.Engine/Graphics/Rendering/OpacityRenderNode.cs new file mode 100644 index 000000000..4539283ff --- /dev/null +++ b/src/Beutl.Engine/Graphics/Rendering/OpacityRenderNode.cs @@ -0,0 +1,25 @@ +namespace Beutl.Graphics.Rendering; + +public sealed class OpacityRenderNode(float opacity) : ContainerRenderNode +{ + public float Opacity { get; } = opacity; + + public bool Equals(float opacity) + { + return Opacity == opacity; + } + + public override RenderNodeOperation[] Process(RenderNodeContext context) + { + return context.Input.Select(r => + { + return RenderNodeOperation.CreateDecorator(r, canvas => + { + using (canvas.PushOpacity(Opacity)) + { + r.Render(canvas); + } + }); + }).ToArray(); + } +} diff --git a/src/Beutl.Engine/Graphics/Rendering/PushNode.cs b/src/Beutl.Engine/Graphics/Rendering/PushNode.cs deleted file mode 100644 index 1ec94e71f..000000000 --- a/src/Beutl.Engine/Graphics/Rendering/PushNode.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Beutl.Graphics.Rendering; - -public sealed class PushNode : ContainerNode -{ - public PushNode() - { - } - - public override void Render(ImmediateCanvas canvas) - { - using (canvas.Push()) - { - base.Render(canvas); - } - } -} diff --git a/src/Beutl.Engine/Graphics/Rendering/PushRenderNode.cs b/src/Beutl.Engine/Graphics/Rendering/PushRenderNode.cs new file mode 100644 index 000000000..0c16662b8 --- /dev/null +++ b/src/Beutl.Engine/Graphics/Rendering/PushRenderNode.cs @@ -0,0 +1,17 @@ +namespace Beutl.Graphics.Rendering; + +public sealed class PushRenderNode : ContainerRenderNode +{ + public override RenderNodeOperation[] Process(RenderNodeContext context) + { + return context.Input.Select(r => + RenderNodeOperation.CreateDecorator(r, canvas => + { + using (canvas.Push()) + { + r.Render(canvas); + } + })) + .ToArray(); + } +} diff --git a/src/Beutl.Engine/Graphics/Rendering/RectClipNode.cs b/src/Beutl.Engine/Graphics/Rendering/RectClipNode.cs deleted file mode 100644 index 9a4c32183..000000000 --- a/src/Beutl.Engine/Graphics/Rendering/RectClipNode.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Beutl.Graphics.Rendering; - -public sealed class RectClipNode(Rect clip, ClipOperation operation) : ContainerNode -{ - public Rect Clip { get; } = clip; - - public ClipOperation Operation { get; } = operation; - - public bool Equals(Rect clip, ClipOperation operation) - { - return Clip == clip - && Operation == operation; - } - - public override void Render(ImmediateCanvas canvas) - { - using (canvas.PushClip(Clip, Operation)) - { - base.Render(canvas); - } - } -} diff --git a/src/Beutl.Engine/Graphics/Rendering/RectClipRenderNode.cs b/src/Beutl.Engine/Graphics/Rendering/RectClipRenderNode.cs new file mode 100644 index 000000000..279c699ee --- /dev/null +++ b/src/Beutl.Engine/Graphics/Rendering/RectClipRenderNode.cs @@ -0,0 +1,28 @@ +namespace Beutl.Graphics.Rendering; + +public sealed class RectClipRenderNode(Rect clip, ClipOperation operation) : ContainerRenderNode +{ + public Rect Clip { get; } = clip; + + public ClipOperation Operation { get; } = operation; + + public bool Equals(Rect clip, ClipOperation operation) + { + return Clip == clip + && Operation == operation; + } + + public override RenderNodeOperation[] Process(RenderNodeContext context) + { + return context.Input.Select(r => + { + return RenderNodeOperation.CreateDecorator(r, canvas => + { + using (canvas.PushClip(Clip, Operation)) + { + r.Render(canvas); + } + }); + }).ToArray(); + } +} diff --git a/src/Beutl.Engine/Graphics/Rendering/RectangleNode.cs b/src/Beutl.Engine/Graphics/Rendering/RectangleRenderNode.cs similarity index 59% rename from src/Beutl.Engine/Graphics/Rendering/RectangleNode.cs rename to src/Beutl.Engine/Graphics/Rendering/RectangleRenderNode.cs index 8b74ee863..93a43f7ab 100644 --- a/src/Beutl.Engine/Graphics/Rendering/RectangleNode.cs +++ b/src/Beutl.Engine/Graphics/Rendering/RectangleRenderNode.cs @@ -2,24 +2,28 @@ namespace Beutl.Graphics.Rendering; -public sealed class RectangleNode(Rect rect, IBrush? fill, IPen? pen) - : BrushDrawNode(fill, pen, PenHelper.GetBounds(rect, pen)) +public sealed class RectangleRenderNode(Rect rect, IBrush? fill, IPen? pen) + : BrushRenderNode(fill, pen) { public Rect Rect { get; } = rect; public bool Equals(Rect rect, IBrush? fill, IPen? pen) { return Rect == rect - && EqualityComparer.Default.Equals(Fill, fill) - && EqualityComparer.Default.Equals(Pen, pen); + && EqualityComparer.Default.Equals(Fill, fill) + && EqualityComparer.Default.Equals(Pen, pen); } - public override void Render(ImmediateCanvas canvas) + public override RenderNodeOperation[] Process(RenderNodeContext context) { - canvas.DrawRectangle(Rect, Fill, Pen); + return + [ + RenderNodeOperation.CreateLambda(PenHelper.GetBounds(Rect, Pen), + canvas => canvas.DrawRectangle(Rect, Fill, Pen), HitTest) + ]; } - public override bool HitTest(Point point) + private bool HitTest(Point point) { StrokeAlignment alignment = Pen?.StrokeAlignment ?? StrokeAlignment.Inside; float thickness = Pen?.Thickness ?? 0; diff --git a/src/Beutl.Engine/Graphics/Rendering/RenderLayer.cs b/src/Beutl.Engine/Graphics/Rendering/RenderLayer.cs index 588348f1f..f07b3b166 100644 --- a/src/Beutl.Engine/Graphics/Rendering/RenderLayer.cs +++ b/src/Beutl.Engine/Graphics/Rendering/RenderLayer.cs @@ -7,14 +7,16 @@ namespace Beutl.Graphics.Rendering; public sealed class RenderLayer(RenderScene renderScene) : IDisposable { - private class Entry(DrawableNode node) : IDisposable + private class Entry(DrawableRenderNode node) : IDisposable { ~Entry() { Dispose(); } - public DrawableNode Node { get; } = node; + public DrawableRenderNode Node { get; } = node; + + public Rect Bounds { get; set; } public bool IsDirty { get; set; } = true; @@ -46,7 +48,7 @@ public void Add(Drawable drawable) { if (!_cache.TryGetValue(drawable, out Entry? entry)) { - entry = new Entry(new DrawableNode(drawable)); + entry = new Entry(new DrawableRenderNode(drawable)); _cache.Add(drawable, entry); var weakRef = new WeakReference(entry); @@ -68,7 +70,7 @@ public void Add(Drawable drawable) if (entry.IsDirty) { // DeferredCanvasを作成し、記録 - using var canvas = new DeferradCanvas(entry.Node, renderScene.Size); + using var canvas = new GraphicsContext2D(entry.Node, renderScene.Size); drawable.Render(canvas); entry.IsDirty = false; } @@ -92,11 +94,11 @@ public void UpdateAll(IReadOnlyList elements) } } - public void ClearAllNodeCache(RenderCacheContext? context) + public void ClearAllNodeCache(RenderNodeCacheContext? context) { foreach (KeyValuePair item in _cache) { - context?.ClearCache(item.Value.Node); + RenderNodeCacheContext.ClearCache(item.Value.Node); item.Value.Dispose(); } @@ -108,56 +110,53 @@ public void Render(ImmediateCanvas canvas) { foreach (Entry? entry in CollectionsMarshal.AsSpan(_currentFrame)) { - DrawableNode node = entry.Node; + DrawableRenderNode node = entry.Node; Drawable drawable = node.Drawable; if (entry.IsDirty) { - using var dcanvas = new DeferradCanvas(node, renderScene.Size); - drawable.Render(dcanvas); + using var context = new GraphicsContext2D(node, renderScene.Size); + drawable.Render(context); entry.IsDirty = false; } - RenderCacheContext? cacheContext = canvas.GetCacheContext(); + RenderNodeCacheContext? cacheContext = canvas.GetCacheContext(); if (cacheContext != null) { - void AcceptsAll(IGraphicNode node) + void RevalidateAll(RenderNode current) { - RenderCache cache = cacheContext.GetCache(node); + RenderNodeCache cache = current.Cache; - if (node is ContainerNode c) + if (current is ContainerRenderNode c) { - foreach (IGraphicNode item in c.Children) + foreach (RenderNode item in c.Children) { - AcceptsAll(item); + RevalidateAll(item); } cache.CaptureChildren(); } - if (node is ISupportRenderCache supportCache) + cache.IncrementRenderCount(); + if (cache.IsCached && !RenderNodeCacheContext.CanCacheRecursive(current)) { - supportCache.Accepts(cache); - if (cache.IsCached - && !(cache.CanCacheBoundary() - && cacheContext.CanCacheRecursiveChildrenOnly(node))) - { - cache.Invalidate(); - } - } - else - { - cache.IncrementRenderCount(); - if (cache.IsCached && !cacheContext.CanCacheRecursive(node)) - { - cache.Invalidate(); - } + cache.Invalidate(); } } - AcceptsAll(node); + RevalidateAll(node); } - canvas.DrawNode(node); + var processor = new RenderNodeProcessor(node, canvas, true); + Rect bounds = default; + var ops = processor.PullToRoot(); + foreach (var op in ops) + { + op.Render(canvas); + op.Dispose(); + bounds = bounds.Union(op.Bounds); + } + + entry.Bounds = bounds; cacheContext?.MakeCache(node, canvas); } @@ -182,7 +181,7 @@ public void Dispose() } } - public Drawable? HitTest(Point point) + public Drawable? HitTest(Point point, IImmediateCanvasFactory canvasFactory) { if (_currentFrame == null || _currentFrame.Count == 0) return null; @@ -190,9 +189,21 @@ public void Dispose() for (int i = _currentFrame.Count - 1; i >= 0; i--) { Entry entry = _currentFrame[i]; - if (entry.Node.HitTest(point)) + var processor = new RenderNodeProcessor(entry.Node, canvasFactory, false); + var arr = processor.PullToRoot(); + try + { + if (arr.Any(op => op.HitTest(point))) + { + return entry.Node.Drawable; + } + } + finally { - return entry.Node.Drawable; + foreach (var op in arr) + { + op.Dispose(); + } } } @@ -208,12 +219,26 @@ public Rect[] GetBoundaries() int index = 0; foreach (Entry? entry in CollectionsMarshal.AsSpan(_currentFrame)) { - DrawableNode node = entry.Node; - - list[index++] = node.Bounds; + list[index++] = entry.Bounds; //list[index++] = node.Drawable.Bounds; } return list; } + + internal void ClearCache() + { + foreach (KeyValuePair item in _cache) + { + RenderNodeCacheContext.ClearCache(item.Value.Node); + } + + if (_currentFrame == null) + return; + + foreach (Entry item in _currentFrame) + { + RenderNodeCacheContext.ClearCache(item.Node); + } + } } diff --git a/src/Beutl.Engine/Graphics/Rendering/DrawNode.cs b/src/Beutl.Engine/Graphics/Rendering/RenderNode.cs similarity index 56% rename from src/Beutl.Engine/Graphics/Rendering/DrawNode.cs rename to src/Beutl.Engine/Graphics/Rendering/RenderNode.cs index b74190597..2cd95e558 100644 --- a/src/Beutl.Engine/Graphics/Rendering/DrawNode.cs +++ b/src/Beutl.Engine/Graphics/Rendering/RenderNode.cs @@ -1,19 +1,16 @@ -using System.Diagnostics; +using Beutl.Graphics.Rendering.Cache; namespace Beutl.Graphics.Rendering; -public abstract class DrawNode : IGraphicNode +public abstract class RenderNode : INode { - public DrawNode(Rect bounds) + protected RenderNode() { - bounds = bounds.Normalize(); - - Bounds = bounds; + Cache = new RenderNodeCache(this); } - ~DrawNode() + ~RenderNode() { - Debug.WriteLine("GC発生"); if (!IsDisposed) { OnDispose(false); @@ -21,17 +18,18 @@ public DrawNode(Rect bounds) } } - public Rect Bounds { get; } - public bool IsDisposed { get; private set; } - public abstract void Render(ImmediateCanvas canvas); + public RenderNodeCache Cache { get; } + + public abstract RenderNodeOperation[] Process(RenderNodeContext context); public void Dispose() { if (!IsDisposed) { OnDispose(true); + Cache.Dispose(); IsDisposed = true; GC.SuppressFinalize(this); } @@ -40,6 +38,4 @@ public void Dispose() protected virtual void OnDispose(bool disposing) { } - - public abstract bool HitTest(Point point); } diff --git a/src/Beutl.Engine/Graphics/Rendering/RenderNodeContext.cs b/src/Beutl.Engine/Graphics/Rendering/RenderNodeContext.cs new file mode 100644 index 000000000..517755260 --- /dev/null +++ b/src/Beutl.Engine/Graphics/Rendering/RenderNodeContext.cs @@ -0,0 +1,15 @@ +namespace Beutl.Graphics.Rendering; + +public class RenderNodeContext(IImmediateCanvasFactory canvasFactory, RenderNodeOperation[] input) +{ + public RenderNodeOperation[] Input { get; } = input; + + public IImmediateCanvasFactory CanvasFactory { get; } = canvasFactory; + + public bool IsRenderCacheEnabled { get; set; } = true; + + public Rect CalculateBounds() + { + return Input.Aggregate(default, (current, operation) => current.Union(operation.Bounds)); + } +} diff --git a/src/Beutl.Engine/Graphics/Rendering/RenderNodeOperation.cs b/src/Beutl.Engine/Graphics/Rendering/RenderNodeOperation.cs new file mode 100644 index 000000000..a100802f7 --- /dev/null +++ b/src/Beutl.Engine/Graphics/Rendering/RenderNodeOperation.cs @@ -0,0 +1,76 @@ +using Beutl.Media.Source; +using SkiaSharp; + +namespace Beutl.Graphics.Rendering; + +public abstract class RenderNodeOperation : IDisposable +{ + public bool IsDisposed { get; private set; } + + // Invalidになることはない + public abstract Rect Bounds { get; } + + public abstract void Render(ImmediateCanvas canvas); + + public abstract bool HitTest(Point point); + + public void Dispose() + { + if (!IsDisposed) + { + OnDispose(true); + IsDisposed = true; + GC.SuppressFinalize(this); + } + } + + protected virtual void OnDispose(bool disposing) + { + } + + public static RenderNodeOperation CreateDecorator( + RenderNodeOperation child, Action render, + Func? hitTest = null, + Action? onDispose = null) + { + return CreateLambda(child.Bounds, render, hitTest: hitTest ?? child.HitTest, onDispose: () => + { + child.Dispose(); + onDispose?.Invoke(); + }); + } + + public static RenderNodeOperation CreateLambda( + Rect bounds, Action render, + Func? hitTest = null, + Action? onDispose = null) + { + return new LambdaRenderNodeOperation(bounds, render, hitTest, onDispose); + } + + public static RenderNodeOperation CreateFromSurface(Rect bounds, Point position, SKSurface surface) + { + return CreateLambda(bounds, canvas => canvas.DrawSurface(surface, position), bounds.Contains, surface.Dispose); + } + + public static RenderNodeOperation CreateFromSurface(Rect bounds, Point position, Ref surface) + { + return CreateLambda(bounds, canvas => canvas.DrawSurface(surface.Value, position), bounds.Contains, surface.Dispose); + } + + private class LambdaRenderNodeOperation( + Rect bounds, + Action render, + Func? hitTest, + Action? onDispose) + : RenderNodeOperation + { + public override Rect Bounds => bounds; + + public override void Render(ImmediateCanvas canvas) => render(canvas); + + public override bool HitTest(Point point) => hitTest?.Invoke(point) ?? false; + + protected override void OnDispose(bool disposing) => onDispose?.Invoke(); + } +} diff --git a/src/Beutl.Engine/Graphics/Rendering/RenderNodeProcessor.cs b/src/Beutl.Engine/Graphics/Rendering/RenderNodeProcessor.cs new file mode 100644 index 000000000..bcb0d33b8 --- /dev/null +++ b/src/Beutl.Engine/Graphics/Rendering/RenderNodeProcessor.cs @@ -0,0 +1,141 @@ +using Beutl.Collections.Pooled; +using Beutl.Media; +using Beutl.Media.Pixel; +using SkiaSharp; + +namespace Beutl.Graphics.Rendering; + +public class RenderNodeProcessor +{ + private readonly IImmediateCanvasFactory _canvasFactory; + private readonly bool _useRenderCache; + + public RenderNodeProcessor( + RenderNode root, IImmediateCanvasFactory canvasFactory, bool useRenderCache) + { + _canvasFactory = canvasFactory; + _useRenderCache = useRenderCache; + Root = root; + } + + public RenderNode Root { get; set; } + + public void Render(ImmediateCanvas canvas) + { + var ops = PullToRoot(); + foreach (var op in ops) + { + op.Render(canvas); + op.Dispose(); + } + } + + internal List<(SKSurface Surface, Rect Bounds)> RasterizeToSurface() + { + var list = new List<(SKSurface, Rect)>(); + var ops = PullToRoot(); + foreach (var op in ops) + { + var rect = PixelRect.FromRect(op.Bounds); + if (rect.Width <= 0 || rect.Height <= 0) continue; + SKSurface surface = _canvasFactory.CreateRenderTarget(rect.Width, rect.Height) + ?? throw new Exception("surface is null"); + + using ImmediateCanvas canvas = _canvasFactory.CreateCanvas(surface, true); + + using (canvas.PushTransform(Matrix.CreateTranslation(-op.Bounds.X, -op.Bounds.Y))) + { + op.Render(canvas); + op.Dispose(); + } + + list.Add((surface, op.Bounds)); + } + + return list; + } + + public List> Rasterize() + { + var list = new List>(); + var ops = PullToRoot(); + foreach (var op in ops) + { + var rect = PixelRect.FromRect(op.Bounds); + using SKSurface? surface = _canvasFactory.CreateRenderTarget(rect.Width, rect.Height) + ?? throw new Exception("surface is null"); + + using ImmediateCanvas canvas = _canvasFactory.CreateCanvas(surface, true); + + using (canvas.PushTransform(Matrix.CreateTranslation(-op.Bounds.X, -op.Bounds.Y))) + { + op.Render(canvas); + op.Dispose(); + } + + list.Add(canvas.GetBitmap()); + } + + return list; + } + + public Bitmap RasterizeAndConcat() + { + var ops = PullToRoot(); + var bounds = ops.Aggregate(Rect.Empty, (a, n) => a.Union(n.Bounds)); + var rect = PixelRect.FromRect(bounds); + using SKSurface surface = _canvasFactory.CreateRenderTarget(rect.Width, rect.Height) + ?? throw new Exception("surface is null"); + + using ImmediateCanvas canvas = _canvasFactory.CreateCanvas(surface, true); + using (canvas.PushTransform(Matrix.CreateTranslation(-bounds.X, -bounds.Y))) + { + foreach (var op in ops) + { + op.Render(canvas); + op.Dispose(); + } + } + + return canvas.GetBitmap(); + } + + public RenderNodeOperation[] PullToRoot() + { + return Pull(Root); + } + + public RenderNodeOperation[] Pull(RenderNode node) + { + if (_useRenderCache && node.Cache is { IsCached: true } cache) + { + return cache.UseCache() + .Select(i => RenderNodeOperation.CreateFromSurface( + bounds: i.Bounds, + position: i.Bounds.Position, + surface: i.Surface)) + .ToArray(); + } + + RenderNodeOperation[] input = []; + if (node is ContainerRenderNode container) + { + using var operations = new PooledList(); + foreach (RenderNode innerNode in container.Children) + { + operations.AddRange(Pull(innerNode)); + } + + input = operations.ToArray(); + } + + var context = new RenderNodeContext(_canvasFactory, input); + var result = node.Process(context); + if (_useRenderCache && !context.IsRenderCacheEnabled) + { + node.Cache.ReportRenderCount(0); + } + + return result; + } +} diff --git a/src/Beutl.Engine/Graphics/Rendering/RenderScene.cs b/src/Beutl.Engine/Graphics/Rendering/RenderScene.cs index 74922b3a8..85de052d4 100644 --- a/src/Beutl.Engine/Graphics/Rendering/RenderScene.cs +++ b/src/Beutl.Engine/Graphics/Rendering/RenderScene.cs @@ -52,11 +52,11 @@ public void Render(ImmediateCanvas canvas) } } - public Drawable? HitTest(Point point) + public Drawable? HitTest(Point point, IImmediateCanvasFactory canvasFactory) { foreach (int key in _layer.Keys.Reverse()) { - if (_layer[key].HitTest(point) is { } drawable) + if (_layer[key].HitTest(point, canvasFactory) is { } drawable) { return drawable; } @@ -64,4 +64,12 @@ public void Render(ImmediateCanvas canvas) return null; } + + internal void ClearCache() + { + foreach (RenderLayer layer in _layer.Values) + { + layer.ClearCache(); + } + } } diff --git a/src/Beutl.Engine/Graphics/Rendering/Renderer.cs b/src/Beutl.Engine/Graphics/Rendering/Renderer.cs index a4cb31021..845ebc5da 100644 --- a/src/Beutl.Engine/Graphics/Rendering/Renderer.cs +++ b/src/Beutl.Engine/Graphics/Rendering/Renderer.cs @@ -12,12 +12,13 @@ public class Renderer : IRenderer private readonly SKSurface _surface; private readonly FpsText _fpsText = new(); private readonly InstanceClock _instanceClock = new(); - private readonly RenderCacheContext _cacheContext = new(); + private readonly RenderNodeCacheContext _cacheContext; public Renderer(int width, int height) { FrameSize = new PixelSize(width, height); RenderScene = new RenderScene(FrameSize); + _cacheContext = new RenderNodeCacheContext(RenderScene); (_immediateCanvas, _surface) = RenderThread.Dispatcher.Invoke(() => { var factory = (IImmediateCanvasFactory)this; @@ -35,7 +36,8 @@ public Renderer(int width, int height) { OnDispose(false); _immediateCanvas.Dispose(); - _cacheContext.Dispose(); + RenderScene.ClearCache(); + RenderScene.Dispose(); IsDisposed = true; } @@ -65,7 +67,7 @@ public void Dispose() { OnDispose(true); _immediateCanvas.Dispose(); - _cacheContext.Dispose(); + RenderScene.ClearCache(); RenderScene.Dispose(); GC.SuppressFinalize(this); @@ -127,14 +129,15 @@ ImmediateCanvas IImmediateCanvasFactory.CreateCanvas(SKSurface surface, bool lea return surface; } - public RenderCacheContext? GetCacheContext() + public RenderNodeCacheContext? GetCacheContext() { return _cacheContext; } public Drawable? HitTest(Point point) { - return RenderScene.HitTest(point); + RenderThread.Dispatcher.VerifyAccess(); + return RenderScene.HitTest(point, this); } public bool Render(TimeSpan timeSpan) diff --git a/src/Beutl.Engine/Graphics/Rendering/SnapshotBackdropNode.cs b/src/Beutl.Engine/Graphics/Rendering/SnapshotBackdropNode.cs deleted file mode 100644 index 2dd9dc89d..000000000 --- a/src/Beutl.Engine/Graphics/Rendering/SnapshotBackdropNode.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Beutl.Graphics.Rendering.Cache; -using Beutl.Media; -using Beutl.Media.Pixel; - -namespace Beutl.Graphics.Rendering; - -public class SnapshotBackdropNode() : DrawNode(default), IBackdrop, ISupportRenderCache -{ - private Bitmap? _bitmap; - - public override bool HitTest(Point point) => false; - - public override void Render(ImmediateCanvas canvas) - { - _bitmap?.Dispose(); - _bitmap = canvas.GetRoot().GetBitmap(); - } - - public void Draw(ImmediateCanvas canvas) - { - if (_bitmap != null) - { - canvas.DrawBitmap(_bitmap, Brushes.White, null); - } - } - - protected override void OnDispose(bool disposing) - { - base.OnDispose(disposing); - _bitmap?.Dispose(); - _bitmap = null; - } - - void ISupportRenderCache.Accepts(RenderCache cache) - { - cache.ReportRenderCount(0); - } - - void ISupportRenderCache.CreateCache(IImmediateCanvasFactory factory, RenderCache cache, RenderCacheContext context) - { - } - - void ISupportRenderCache.RenderWithCache(ImmediateCanvas canvas, RenderCache cache) - { - } -} diff --git a/src/Beutl.Engine/Graphics/Rendering/SnapshotBackdropRenderNode.cs b/src/Beutl.Engine/Graphics/Rendering/SnapshotBackdropRenderNode.cs new file mode 100644 index 000000000..3cba96afd --- /dev/null +++ b/src/Beutl.Engine/Graphics/Rendering/SnapshotBackdropRenderNode.cs @@ -0,0 +1,37 @@ +using Beutl.Media; +using Beutl.Media.Pixel; + +namespace Beutl.Graphics.Rendering; + +public class SnapshotBackdropRenderNode : RenderNode, IBackdrop +{ + private Bitmap? _bitmap; + + public override RenderNodeOperation[] Process(RenderNodeContext context) + { + context.IsRenderCacheEnabled = false; + return + [ + RenderNodeOperation.CreateLambda(default, canvas => + { + _bitmap?.Dispose(); + _bitmap = canvas.GetRoot().GetBitmap(); + }) + ]; + } + + public void Draw(ImmediateCanvas canvas) + { + if (_bitmap != null) + { + canvas.DrawBitmap(_bitmap, Brushes.White, null); + } + } + + protected override void OnDispose(bool disposing) + { + base.OnDispose(disposing); + _bitmap?.Dispose(); + _bitmap = null; + } +} diff --git a/src/Beutl.Engine/Graphics/Rendering/TextNode.cs b/src/Beutl.Engine/Graphics/Rendering/TextRenderNode.cs similarity index 52% rename from src/Beutl.Engine/Graphics/Rendering/TextNode.cs rename to src/Beutl.Engine/Graphics/Rendering/TextRenderNode.cs index 3e627ce06..eac6d4a75 100644 --- a/src/Beutl.Engine/Graphics/Rendering/TextNode.cs +++ b/src/Beutl.Engine/Graphics/Rendering/TextRenderNode.cs @@ -1,28 +1,30 @@ using Beutl.Media; using Beutl.Media.TextFormatting; - using SkiaSharp; namespace Beutl.Graphics.Rendering; -public sealed class TextNode(FormattedText text, IBrush? fill, IPen? pen) - : BrushDrawNode(fill, pen, text.ActualBounds) +public sealed class TextRenderNode(FormattedText text, IBrush? fill, IPen? pen) + : BrushRenderNode(fill, pen) { public FormattedText Text { get; private set; } = text; public bool Equals(FormattedText text, IBrush? fill, IPen? pen) { return Text == text - && EqualityComparer.Default.Equals(Fill, fill) - && EqualityComparer.Default.Equals(Pen, pen); + && EqualityComparer.Default.Equals(Fill, fill) + && EqualityComparer.Default.Equals(Pen, pen); } - public override void Render(ImmediateCanvas canvas) + public override RenderNodeOperation[] Process(RenderNodeContext context) { - canvas.DrawText(Text, Fill, Pen); + return + [ + RenderNodeOperation.CreateLambda(Text.ActualBounds, canvas => canvas.DrawText(Text, Fill, Pen), HitTest) + ]; } - public override bool HitTest(Point point) + private bool HitTest(Point point) { SKPath fill = Text.GetFillPath(); if (Fill != null && fill.Contains(point.X, point.Y)) diff --git a/src/Beutl.Engine/Graphics/Rendering/TransformNode.cs b/src/Beutl.Engine/Graphics/Rendering/TransformNode.cs deleted file mode 100644 index 8fdf15270..000000000 --- a/src/Beutl.Engine/Graphics/Rendering/TransformNode.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace Beutl.Graphics.Rendering; - -public sealed class TransformNode(Matrix transform, TransformOperator transformOperator) : ContainerNode -{ - public Matrix Transform { get; } = transform; - - public TransformOperator TransformOperator { get; } = transformOperator; - - protected override Rect TransformBounds(Rect bounds) - { - return bounds.TransformToAABB(Transform); - } - - public bool Equals(Matrix transform, TransformOperator transformOperator) - { - return Transform == transform - && TransformOperator == transformOperator; - } - - public override void Render(ImmediateCanvas canvas) - { - using (canvas.PushTransform(Transform, TransformOperator)) - { - base.Render(canvas); - } - } - - // Todo: Append, Setの時の動作 - public override bool HitTest(Point point) - { - if (Transform.HasInverse) - point *= Transform.Invert(); - return base.HitTest(point); - } -} diff --git a/src/Beutl.Engine/Graphics/Rendering/TransformRenderNode.cs b/src/Beutl.Engine/Graphics/Rendering/TransformRenderNode.cs new file mode 100644 index 000000000..6db9e2363 --- /dev/null +++ b/src/Beutl.Engine/Graphics/Rendering/TransformRenderNode.cs @@ -0,0 +1,36 @@ +namespace Beutl.Graphics.Rendering; + +public sealed class TransformRenderNode(Matrix transform, TransformOperator transformOperator) : ContainerRenderNode +{ + public Matrix Transform { get; } = transform; + + public TransformOperator TransformOperator { get; } = transformOperator; + + public override RenderNodeOperation[] Process(RenderNodeContext context) + { + return context.Input.Select(r => + RenderNodeOperation.CreateLambda( + r.Bounds.TransformToAABB(Transform), + canvas => + { + using (canvas.PushTransform(Transform, TransformOperator)) + { + r.Render(canvas); + } + }, + hitTest: point => + { + if (Transform.HasInverse) + point *= Transform.Invert(); + return r.HitTest(point); + }, + onDispose: r.Dispose)) + .ToArray(); + } + + public bool Equals(Matrix transform, TransformOperator transformOperator) + { + return Transform == transform + && TransformOperator == transformOperator; + } +} diff --git a/src/Beutl.Engine/Graphics/Rendering/VideoSourceNode.cs b/src/Beutl.Engine/Graphics/Rendering/VideoSourceNode.cs deleted file mode 100644 index 28d4efb0b..000000000 --- a/src/Beutl.Engine/Graphics/Rendering/VideoSourceNode.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Beutl.Media; -using Beutl.Media.Source; - -namespace Beutl.Graphics.Rendering; - -public sealed class VideoSourceNode( - IVideoSource source, int frame, IBrush? fill, IPen? pen) - : BrushDrawNode(fill, pen, PenHelper.GetBounds(new Rect(default, source.FrameSize.ToSize(1)), pen)) -{ - public IVideoSource Source { get; } = source.Clone(); - - public int Frame { get; } = frame; - - public bool Equals(IVideoSource source, int frame, IBrush? fill, IPen? pen) - { - return Frame == frame - && EqualityComparer.Default.Equals(Source, source) - && EqualityComparer.Default.Equals(Fill, fill) - && EqualityComparer.Default.Equals(Pen, pen); - } - - public override void Render(ImmediateCanvas canvas) - { - if (Source.Read(Frame, out IBitmap? bitmap)) - { - using (bitmap) - { - canvas.DrawBitmap(bitmap, Fill, Pen); - } - } - } - - protected override void OnDispose(bool disposing) - { - base.OnDispose(disposing); - Source.Dispose(); - } - - public override bool HitTest(Point point) - { - StrokeAlignment alignment = Pen?.StrokeAlignment ?? StrokeAlignment.Inside; - float thickness = Pen?.Thickness ?? 0; - thickness = PenHelper.GetRealThickness(alignment, thickness); - - if (Fill != null) - { - Rect rect = Bounds.Inflate(thickness); - return rect.ContainsExclusive(point); - } - else - { - Rect borderRect = Bounds.Inflate(thickness); - Rect emptyRect = Bounds.Deflate(thickness); - return borderRect.ContainsExclusive(point) && !emptyRect.ContainsExclusive(point); - } - } -} diff --git a/src/Beutl.Engine/Graphics/Rendering/VideoSourceRenderNode.cs b/src/Beutl.Engine/Graphics/Rendering/VideoSourceRenderNode.cs new file mode 100644 index 000000000..69fe03925 --- /dev/null +++ b/src/Beutl.Engine/Graphics/Rendering/VideoSourceRenderNode.cs @@ -0,0 +1,72 @@ +using Beutl.Media; +using Beutl.Media.Source; + +namespace Beutl.Graphics.Rendering; + +public sealed class VideoSourceRenderNode( + IVideoSource source, + int frame, + IBrush? fill, + IPen? pen) + : BrushRenderNode(fill, pen) +{ + public IVideoSource Source { get; } = source.Clone(); + + public int Frame { get; } = frame; + + public Rect Bounds { get; } = PenHelper.GetBounds(new Rect(default, source.FrameSize.ToSize(1)), pen); + + public bool Equals(IVideoSource source, int frame, IBrush? fill, IPen? pen) + { + return Frame == frame + && EqualityComparer.Default.Equals(Source, source) + && EqualityComparer.Default.Equals(Fill, fill) + && EqualityComparer.Default.Equals(Pen, pen); + } + + public override RenderNodeOperation[] Process(RenderNodeContext context) + { + return + [ + RenderNodeOperation.CreateLambda( + bounds: Bounds, + render: canvas => + { + if (Source.Read(Frame, out IBitmap? bitmap)) + { + using (bitmap) + { + canvas.DrawBitmap(bitmap, Fill, Pen); + } + } + }, + hitTest: HitTest + ) + ]; + } + + protected override void OnDispose(bool disposing) + { + base.OnDispose(disposing); + Source.Dispose(); + } + + private bool HitTest(Point point) + { + StrokeAlignment alignment = Pen?.StrokeAlignment ?? StrokeAlignment.Inside; + float thickness = Pen?.Thickness ?? 0; + thickness = PenHelper.GetRealThickness(alignment, thickness); + + if (Fill != null) + { + Rect rect = Bounds.Inflate(thickness); + return rect.ContainsExclusive(point); + } + else + { + Rect borderRect = Bounds.Inflate(thickness); + Rect emptyRect = Bounds.Deflate(thickness); + return borderRect.ContainsExclusive(point) && !emptyRect.ContainsExclusive(point); + } + } +} diff --git a/src/Beutl.Engine/Graphics/Shapes/Shape.cs b/src/Beutl.Engine/Graphics/Shapes/Shape.cs index ae2d056f0..7f610e1de 100644 --- a/src/Beutl.Engine/Graphics/Shapes/Shape.cs +++ b/src/Beutl.Engine/Graphics/Shapes/Shape.cs @@ -234,7 +234,7 @@ private static float ActualThickness(IPen pen) protected abstract Geometry? CreateGeometry(); - protected override void OnDraw(ICanvas canvas) + protected override void OnDraw(GraphicsContext2D context) { Geometry? geometry = GetOrCreateGeometry(); if (geometry == null) @@ -255,17 +255,17 @@ protected override void OnDraw(ICanvas canvas) matrix *= Matrix.CreateScale(scale); - using (canvas.PushTransform(matrix)) + using (context.PushTransform(matrix)) { - canvas.DrawGeometry(geometry, Fill, Pen); + context.DrawGeometry(geometry, Fill, Pen); } } - public override void Render(ICanvas canvas) + public override void Render(GraphicsContext2D context) { if (IsVisible) { - Size availableSize = canvas.Size.ToSize(1); + Size availableSize = context.Size.ToSize(1); Size size = MeasureCore(availableSize); var rect = new Rect(size).Translate(CreatedGeometry?.Bounds.Position ?? default); if (FilterEffect != null) @@ -275,13 +275,13 @@ public override void Render(ICanvas canvas) Matrix transform = GetTransformMatrix(availableSize, size); Rect transformedBounds = rect.TransformToAABB(transform); - using (canvas.PushBlendMode(BlendMode)) - using (canvas.PushTransform(transform)) - using (canvas.PushOpacity(Opacity / 100f)) - using (FilterEffect == null ? new() : canvas.PushFilterEffect(FilterEffect)) - using (OpacityMask == null ? new() : canvas.PushOpacityMask(OpacityMask, new Rect(size))) + using (context.PushBlendMode(BlendMode)) + using (context.PushTransform(transform)) + using (context.PushOpacity(Opacity / 100f)) + using (FilterEffect == null ? new() : context.PushFilterEffect(FilterEffect)) + using (OpacityMask == null ? new() : context.PushOpacityMask(OpacityMask, new Rect(size))) { - OnDraw(canvas); + OnDraw(context); } Bounds = transformedBounds; diff --git a/src/Beutl.Engine/Graphics/Shapes/TextBlock.cs b/src/Beutl.Engine/Graphics/Shapes/TextBlock.cs index 45c0f9202..9de8bf740 100644 --- a/src/Beutl.Engine/Graphics/Shapes/TextBlock.cs +++ b/src/Beutl.Engine/Graphics/Shapes/TextBlock.cs @@ -3,6 +3,7 @@ using System.Runtime.InteropServices; using System.Text.Json.Nodes; using Beutl.Animation; +using Beutl.Graphics.Rendering; using Beutl.Language; using Beutl.Media; using Beutl.Media.TextFormatting; @@ -210,25 +211,25 @@ internal static SKPath ToSKPath(TextElements elements) return skpath; } - protected override void OnDraw(ICanvas canvas) + protected override void OnDraw(GraphicsContext2D context) { OnUpdateText(); if (_elements != null) { if (SplitByCharacters) { - DrawSplitted(canvas, _elements); + DrawSplitted(context, _elements); } else { - DrawGrouped(canvas, _elements); + DrawGrouped(context, _elements); } } } - private void DrawGrouped(ICanvas canvas, TextElements elements) + private void DrawGrouped(GraphicsContext2D context, TextElements elements) { - using (canvas.Push()) + using (context.Push()) { float prevBottom = 0; foreach (Span line in elements.Lines) @@ -236,7 +237,7 @@ private void DrawGrouped(ICanvas canvas, TextElements elements) Size lineBounds = MeasureLine(line); float ascent = MinAscent(line); - using (canvas.PushTransform(Matrix.CreateTranslation(0, prevBottom - ascent))) + using (context.PushTransform(Matrix.CreateTranslation(0, prevBottom - ascent))) { float prevRight = 0; foreach (FormattedText item in line) @@ -245,9 +246,9 @@ private void DrawGrouped(ICanvas canvas, TextElements elements) { Rect elementBounds = item.Bounds; - using (canvas.PushTransform(Matrix.CreateTranslation(prevRight + item.Spacing / 2, 0))) + using (context.PushTransform(Matrix.CreateTranslation(prevRight + item.Spacing / 2, 0))) { - canvas.DrawText(item, item.Brush ?? Fill, item.Pen ?? Pen); + context.DrawText(item, item.Brush ?? Fill, item.Pen ?? Pen); prevRight += elementBounds.Width + item.Spacing; } @@ -260,7 +261,7 @@ private void DrawGrouped(ICanvas canvas, TextElements elements) } } - private void DrawSplitted(ICanvas canvas, TextElements elements) + private void DrawSplitted(GraphicsContext2D context, TextElements elements) { float prevBottom = 0; foreach (Span line in elements.Lines) @@ -278,9 +279,9 @@ private void DrawSplitted(ICanvas canvas, TextElements elements) foreach (Geometry geometry in item.ToGeometies()) { - using (canvas.PushTransform(Matrix.CreateTranslation(prevRight + item.Spacing / 2, yPosition))) + using (context.PushTransform(Matrix.CreateTranslation(prevRight + item.Spacing / 2, yPosition))) { - canvas.DrawGeometry(geometry, item.Brush ?? Fill, item.Pen ?? Pen); + context.DrawGeometry(geometry, item.Brush ?? Fill, item.Pen ?? Pen); } } diff --git a/src/Beutl.Engine/Graphics/SourceBackdrop.cs b/src/Beutl.Engine/Graphics/SourceBackdrop.cs index 9caf2d843..d494804db 100644 --- a/src/Beutl.Engine/Graphics/SourceBackdrop.cs +++ b/src/Beutl.Engine/Graphics/SourceBackdrop.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Beutl.Graphics.Rendering; using Beutl.Language; namespace Beutl.Graphics; @@ -30,23 +31,23 @@ protected override Size MeasureCore(Size availableSize) return availableSize; } - protected override void OnDraw(ICanvas canvas) + protected override void OnDraw(GraphicsContext2D context) { } - public override void Render(ICanvas canvas) + public override void Render(GraphicsContext2D context) { - base.Render(canvas); + base.Render(context); if (IsVisible) { - var backdrop = canvas.Snapshot(); + var backdrop = context.Snapshot(); if (Clear) { - canvas.Clear(); + context.Clear(); } - Size availableSize = canvas.Size.ToSize(1); + Size availableSize = context.Size.ToSize(1); Size size = MeasureCore(availableSize); var rect = new Rect(size); if (FilterEffect != null) @@ -56,12 +57,12 @@ public override void Render(ICanvas canvas) Matrix transform = GetTransformMatrix(availableSize, size); Rect transformedBounds = rect.TransformToAABB(transform); - using (canvas.PushBlendMode(BlendMode)) - using (canvas.PushTransform(transform)) - using (FilterEffect == null ? new() : canvas.PushFilterEffect(FilterEffect)) - using (OpacityMask == null ? new() : canvas.PushOpacityMask(OpacityMask, new Rect(size))) + using (context.PushBlendMode(BlendMode)) + using (context.PushTransform(transform)) + using (FilterEffect == null ? new() : context.PushFilterEffect(FilterEffect)) + using (OpacityMask == null ? new() : context.PushOpacityMask(OpacityMask, new Rect(size))) { - canvas.DrawBackdrop(backdrop); + context.DrawBackdrop(backdrop); } Bounds = transformedBounds; diff --git a/src/Beutl.Engine/Graphics/SourceImage.cs b/src/Beutl.Engine/Graphics/SourceImage.cs index 437731f01..88195ed74 100644 --- a/src/Beutl.Engine/Graphics/SourceImage.cs +++ b/src/Beutl.Engine/Graphics/SourceImage.cs @@ -1,4 +1,5 @@ -using Beutl.Media; +using Beutl.Graphics.Rendering; +using Beutl.Media; using Beutl.Media.Source; namespace Beutl.Graphics; @@ -36,11 +37,11 @@ protected override Size MeasureCore(Size availableSize) } } - protected override void OnDraw(ICanvas canvas) + protected override void OnDraw(GraphicsContext2D context) { if (_source != null) { - canvas.DrawImageSource(_source, Brushes.White, null); + context.DrawImageSource(_source, Brushes.White, null); } } } diff --git a/src/Beutl.Engine/Graphics/SourceVideo.cs b/src/Beutl.Engine/Graphics/SourceVideo.cs index 7d90d6e20..d7a171b42 100644 --- a/src/Beutl.Engine/Graphics/SourceVideo.cs +++ b/src/Beutl.Engine/Graphics/SourceVideo.cs @@ -1,4 +1,5 @@ using Beutl.Animation; +using Beutl.Graphics.Rendering; using Beutl.Media; using Beutl.Media.Source; @@ -99,7 +100,7 @@ protected override Size MeasureCore(Size availableSize) } } - protected override void OnDraw(ICanvas canvas) + protected override void OnDraw(GraphicsContext2D context) { if (_source?.IsDisposed == false) { @@ -112,7 +113,7 @@ protected override void OnDraw(ICanvas canvas) Rational rate = _source.FrameRate; double frameNum = pos.TotalSeconds * (rate.Numerator / (double)rate.Denominator); - canvas.DrawVideoSource(_source, (int)frameNum, Brushes.White, null); + context.DrawVideoSource(_source, (int)frameNum, Brushes.White, null); } } } diff --git a/src/Beutl.Engine/Graphics/Transformation/MultiTransform.cs b/src/Beutl.Engine/Graphics/Transformation/MultiTransform.cs index a017b3cbe..6b7fc46d7 100644 --- a/src/Beutl.Engine/Graphics/Transformation/MultiTransform.cs +++ b/src/Beutl.Engine/Graphics/Transformation/MultiTransform.cs @@ -3,6 +3,7 @@ namespace Beutl.Graphics.Transformation; +[Obsolete("Use TransformGroup instead.")] public sealed class MultiTransform : Transform { public static readonly CoreProperty LeftProperty; diff --git a/src/Beutl/ViewModels/EditViewModel.cs b/src/Beutl/ViewModels/EditViewModel.cs index 15684fada..233a4a8a3 100644 --- a/src/Beutl/ViewModels/EditViewModel.cs +++ b/src/Beutl/ViewModels/EditViewModel.cs @@ -127,7 +127,7 @@ private void OnEditorConfigPropertyChanged(object? sender, PropertyChangedEventA or nameof(EditorConfig.NodeCacheMaxPixels) or nameof(EditorConfig.NodeCacheMinPixels)) { - RenderCacheContext? cacheContext = Renderer.Value.GetCacheContext(); + RenderNodeCacheContext? cacheContext = Renderer.Value.GetCacheContext(); if (cacheContext != null) { cacheContext.CacheOptions = RenderCacheOptions.CreateFromGlobalConfiguration(); diff --git a/src/Beutl/ViewModels/OutputViewModel.cs b/src/Beutl/ViewModels/OutputViewModel.cs index ed3d56d01..1c27f399e 100644 --- a/src/Beutl/ViewModels/OutputViewModel.cs +++ b/src/Beutl/ViewModels/OutputViewModel.cs @@ -193,7 +193,7 @@ await RenderThread.Dispatcher.InvokeAsync(async () => using (frameProgress.CombineLatest(sampleProgress).Subscribe(t => ProgressValue.Value = t.Item1.TotalSeconds + t.Item2.TotalSeconds)) { - RenderCacheContext? cacheContext = renderer.GetCacheContext(); + RenderNodeCacheContext? cacheContext = renderer.GetCacheContext(); if (cacheContext != null) { diff --git a/src/Beutl/ViewModels/PlayerViewModel.cs b/src/Beutl/ViewModels/PlayerViewModel.cs index 69791719a..248ddf247 100644 --- a/src/Beutl/ViewModels/PlayerViewModel.cs +++ b/src/Beutl/ViewModels/PlayerViewModel.cs @@ -702,44 +702,16 @@ public Task> DrawSelectedDrawable(Drawable drawable) return RenderThread.Dispatcher.InvokeAsync(() => { if (Scene == null) throw new Exception("Scene is null."); - IRenderer renderer = EditViewModel.Renderer.Value; + SceneRenderer renderer = EditViewModel.Renderer.Value; PixelSize frameSize = renderer.FrameSize; - using var root = new DrawableNode(drawable); - using var dcanvas = new DeferradCanvas(root, frameSize); - drawable.Render(dcanvas); - - Rect bounds = root.Bounds; - var rect = PixelRect.FromRect(bounds); - using SKSurface? surface = renderer.CreateRenderTarget(rect.Width, rect.Height) - ?? throw new Exception("surface is null"); - - using ImmediateCanvas icanvas = renderer.CreateCanvas(surface, true); - - RenderCacheContext? cacheContext = renderer.GetCacheContext(); - RenderCacheOptions? restoreCacheOptions = null; - - if (cacheContext != null) + using var root = new DrawableRenderNode(drawable); + using (var context = new GraphicsContext2D(root, frameSize)) { - restoreCacheOptions = cacheContext.CacheOptions; - cacheContext.CacheOptions = RenderCacheOptions.Disabled; + drawable.Render(context); } - try - { - using (icanvas.PushTransform(Matrix.CreateTranslation(-bounds.X, -bounds.Y))) - { - icanvas.DrawNode(root); - } - - return icanvas.GetBitmap(); - } - finally - { - if (cacheContext != null && restoreCacheOptions != null) - { - cacheContext.CacheOptions = restoreCacheOptions; - } - } + var processor = new RenderNodeProcessor(root, renderer, false); + return processor.RasterizeAndConcat(); }); } @@ -752,7 +724,7 @@ public Task> DrawFrame() if (Scene == null) throw new Exception("Scene is null."); IRenderer renderer = EditViewModel.Renderer.Value; - RenderCacheContext? cacheContext = renderer.GetCacheContext(); + RenderNodeCacheContext? cacheContext = renderer.GetCacheContext(); RenderCacheOptions? restoreCacheOptions = null; if (cacheContext != null) diff --git a/src/Beutl/Views/PlayerView.axaml.DragDrop.cs b/src/Beutl/Views/PlayerView.axaml.DragDrop.cs index d83317b16..7ead013ac 100644 --- a/src/Beutl/Views/PlayerView.axaml.DragDrop.cs +++ b/src/Beutl/Views/PlayerView.axaml.DragDrop.cs @@ -2,6 +2,7 @@ using Avalonia.Platform.Storage; using Beutl.Graphics; using Beutl.Graphics.Effects; +using Beutl.Graphics.Rendering; using Beutl.Graphics.Transformation; using Beutl.Helpers; using Beutl.Models; @@ -31,7 +32,9 @@ private async void OnFrameDrop(object? sender, DragEventArgs e) bool containsTra = e.Data.Contains(KnownLibraryItemFormats.Transform); if (containsFe || containsTra) { - Drawable? drawable = editViewModel.Renderer.Value.HitTest(new((float)scaledPosition.X, (float)scaledPosition.Y)); + Drawable? drawable = await RenderThread.Dispatcher.InvokeAsync(() => + editViewModel.Renderer.Value.HitTest( + new((float)scaledPosition.X, (float)scaledPosition.Y))); if (drawable != null) { diff --git a/src/Beutl/Views/PlayerView.axaml.MouseControl.cs b/src/Beutl/Views/PlayerView.axaml.MouseControl.cs index 2ffb3d876..e4fbac6d9 100644 --- a/src/Beutl/Views/PlayerView.axaml.MouseControl.cs +++ b/src/Beutl/Views/PlayerView.axaml.MouseControl.cs @@ -8,6 +8,7 @@ using Beutl.Commands; using Beutl.Controls; using Beutl.Graphics; +using Beutl.Graphics.Rendering; using Beutl.Graphics.Transformation; using Beutl.Helpers; using Beutl.Logging; @@ -358,7 +359,9 @@ public void OnPressed(PointerPressedEventArgs e) double scaleX = Image.Bounds.Size.Width / scene.FrameSize.Width; _scaledStartPosition = imagePosition / scaleX; - Drawable = EditViewModel.Renderer.Value.HitTest(new((float)_scaledStartPosition.X, (float)_scaledStartPosition.Y)); + Drawable = RenderThread.Dispatcher.Invoke(() => + EditViewModel.Renderer.Value.HitTest( + new((float)_scaledStartPosition.X, (float)_scaledStartPosition.Y))); if (Drawable != null) { diff --git a/tests/Beutl.UnitTests/Engine/DeferradCanvasTests.cs b/tests/Beutl.UnitTests/Engine/DeferradCanvasTests.cs deleted file mode 100644 index 1f3cc7a1a..000000000 --- a/tests/Beutl.UnitTests/Engine/DeferradCanvasTests.cs +++ /dev/null @@ -1,83 +0,0 @@ -using Beutl.Graphics; -using Beutl.Graphics.Effects; -using Beutl.Graphics.Rendering; -using Beutl.Logging; -using Beutl.Media; -using Beutl.Media.Pixel; -using Microsoft.Extensions.Logging; - -namespace Beutl.UnitTests.Engine; - -public class DeferradCanvasTests -{ - [SetUp] - public void Setup() - { - Log.LoggerFactory = LoggerFactory.Create(b => b.AddSimpleConsole()); - } - - private static void Draw(ICanvas canvas) - { - using (canvas.PushTransform(Matrix.CreateTranslation(100, 100))) - { - canvas.DrawRectangle(new Rect(0, 0, 500, 500), Brushes.White, null); - - using (canvas.PushTransform(Matrix.CreateTranslation(500, 500))) - { - canvas.DrawRectangle(new Rect(0, 0, 50, 50), Brushes.Blue, null); - } - - canvas.DrawRectangle(new Rect(700, 0, 10, 10), Brushes.Red, null); - } - } - - [Test] - public void Draw() - { - var container = new ContainerNode(); - var dcanvas = new DeferradCanvas(container); - Draw(dcanvas); - - using var canvas = new ImmediateCanvas(1000, 700); - using (canvas.Push()) - { - container.Render(canvas); - using Bitmap bmp = canvas.GetBitmap(); - Assert.That(bmp.Save(Path.Combine(ArtifactProvider.GetArtifactDirectory(), "canvas1.png"), EncodedImageFormat.Png)); - } - - using (canvas.Push()) - { - canvas.Clear(); - Draw(canvas); - using Bitmap bmp1 = canvas.GetBitmap(); - Assert.That(bmp1.Save(Path.Combine(ArtifactProvider.GetArtifactDirectory(), "canvas2.png"), EncodedImageFormat.Png)); - } - } - - [Test] - public void DrawEffect() - { - var container = new ContainerNode(); - var dcanvas = new DeferradCanvas(container); - using (dcanvas.PushTransform(Matrix.CreateTranslation(75, 0))) - using (dcanvas.PushTransform(Matrix.CreateRotation(MathF.PI / 4))) - { - using (dcanvas.PushFilterEffect(new Blur() { Sigma = new Size(10, 10) })) - { - dcanvas.DrawRectangle(new Rect(0, 0, 100, 100), Brushes.White, null); - } - - dcanvas.DrawRectangle(new Rect(100, 100, 100, 100), Brushes.White, null); - } - - using var canvas = new ImmediateCanvas(150, 150); - using (canvas.Push()) - { - container.Render(canvas); - using Bitmap bmp = canvas.GetBitmap(); - Assert.That(bmp.Save(Path.Combine(ArtifactProvider.GetArtifactDirectory(), "direct.png"), EncodedImageFormat.Png)); - } - - } -} diff --git a/tests/Beutl.UnitTests/Engine/Graphics/Rendering/Cache/RenderNodeCacheTests.cs b/tests/Beutl.UnitTests/Engine/Graphics/Rendering/Cache/RenderNodeCacheTests.cs new file mode 100644 index 000000000..4fa59c202 --- /dev/null +++ b/tests/Beutl.UnitTests/Engine/Graphics/Rendering/Cache/RenderNodeCacheTests.cs @@ -0,0 +1,237 @@ +using Beutl.Graphics; +using Beutl.Graphics.Rendering; +using Beutl.Graphics.Rendering.Cache; +using Beutl.Media.Source; +using SkiaSharp; + +namespace Beutl.UnitTests.Engine.Graphics.Rendering.Cache; + +public class RenderNodeCacheTests +{ + [Test] + [TestCase(3)] + [TestCase(4)] + public void ReportRenderCount_GreaterThanOrEqualToThree_ShouldSetCanCacheToTrue(int count) + { + // Arrange + using var node = new ContainerRenderNode(); + using var cache = new RenderNodeCache(node); + + // Act + cache.ReportRenderCount(count); + + // Assert + Assert.That(cache.CanCache(), Is.True); + } + + [Test] + public void IncrementRenderCount_CalledThreeOrMoreTimes_ShouldSetCanCacheToTrue() + { + // Arrange + using var node = new ContainerRenderNode(); + using var cache = new RenderNodeCache(node); + + // Act + cache.IncrementRenderCount(); + cache.IncrementRenderCount(); + cache.IncrementRenderCount(); + + // Assert + Assert.That(cache.CanCache(), Is.True); + } + + [Test] + public void CaptureChildren_ContainerNodeWithChildren_ShouldCaptureChildren() + { + // Arrange + using var node = new ContainerRenderNode(); + using var child1 = new ContainerRenderNode(); + using var child2 = new ContainerRenderNode(); + node.AddChild(child1); + node.AddChild(child2); + using var cache = new RenderNodeCache(node); + + // Act + cache.CaptureChildren(); + + // Assert + Assert.That(cache.Children, Is.Not.Null); + Assert.That(cache.Children!.Count, Is.EqualTo(2)); + } + + [Test] + public void CaptureChildren_ContainerNodeWithChildren_ShouldCaptureChildrenInOrder() + { + // Arrange + using var node = new ContainerRenderNode(); + using var child1 = new ContainerRenderNode(); + using var child2 = new ContainerRenderNode(); + node.AddChild(child1); + node.AddChild(child2); + using var cache = new RenderNodeCache(node); + + // Act + cache.CaptureChildren(); + + // Assert + Assert.That(cache.Children, Is.Not.Null); + Assert.That(cache.Children![0].TryGetTarget(out var target1), Is.True); + Assert.That(target1, Is.EqualTo(child1)); + Assert.That(cache.Children![1].TryGetTarget(out var target2), Is.True); + Assert.That(target2, Is.EqualTo(child2)); + } + + [Test] + public void SameChildren_ContainerNodeWithSameChildren_ShouldReturnTrue() + { + // Arrange + using var node = new ContainerRenderNode(); + using var child1 = new ContainerRenderNode(); + using var child2 = new ContainerRenderNode(); + node.AddChild(child1); + node.AddChild(child2); + using var cache = new RenderNodeCache(node); + + // Act + cache.CaptureChildren(); + + // Assert + Assert.That(cache.SameChildren(), Is.True); + } + + [Test] + public void SameChildren_RemovingChildFromContainerNode_ShouldReturnFalse() + { + // Arrange + using var node = new ContainerRenderNode(); + using var child1 = new ContainerRenderNode(); + using var child2 = new ContainerRenderNode(); + node.AddChild(child1); + node.AddChild(child2); + using var cache = new RenderNodeCache(node); + + // Act + cache.CaptureChildren(); + node.RemoveChild(child2); + + // Assert + Assert.That(cache.SameChildren(), Is.False); + } + + [Test] + public void SameChildren_AddingChildToContainerNode_ShouldReturnFalse() + { + // Arrange + using var node = new ContainerRenderNode(); + using var child1 = new ContainerRenderNode(); + using var child2 = new ContainerRenderNode(); + node.AddChild(child1); + using var cache = new RenderNodeCache(node); + + // Act + cache.CaptureChildren(); + node.AddChild(child2); + + // Assert + Assert.That(cache.SameChildren(), Is.False); + } + + [Test] + public void SameChildren_ReplacingChildInContainerNode_ShouldReturnFalse() + { + // Arrange + using var node = new ContainerRenderNode(); + using var child1 = new ContainerRenderNode(); + using var child2 = new ContainerRenderNode(); + node.AddChild(child1); + node.AddChild(child2); + using var cache = new RenderNodeCache(node); + + // Act + cache.CaptureChildren(); + node.RemoveChild(child1); + node.AddChild(child1); + + // Assert + Assert.That(cache.SameChildren(), Is.False); + } + + [Test] + public void UseCache_NotCached_ShouldThrowInvalidOperationException() + { + // Arrange + using var node = new ContainerRenderNode(); + using var cache = new RenderNodeCache(node); + + // Act & Assert + Assert.Catch(() => cache.UseCache(out _)); + } + + [Test] + public void UseCache_NotCached_ShouldReturnEmptyArray() + { + // Arrange + using var node = new ContainerRenderNode(); + using var cache = new RenderNodeCache(node); + + // Act + var result = cache.UseCache(); + + // Assert + Assert.That(result, Is.Empty); + } + + [Test] + public void StoreCache_Called_ShouldStoreCache() + { + // Arrange + using var node = new ContainerRenderNode(); + using var cache = new RenderNodeCache(node); + + // Act + using var surfaceRef = Ref.Create(SKSurface.CreateNull(1, 1)); + cache.StoreCache(surfaceRef, new Rect(0, 0, 1, 1)); + + // Assert + Assert.That(cache.IsCached, Is.True); + } + + [Test] + public void StoreCache_CalledMultipleTimes_ShouldStoreMultipleCaches() + { + // Arrange + using var node = new ContainerRenderNode(); + using var cache = new RenderNodeCache(node); + + // Act + using var surfaceRef1 = Ref.Create(SKSurface.CreateNull(1, 1)); + using var surfaceRef2 = Ref.Create(SKSurface.CreateNull(1, 1)); + cache.StoreCache([(surfaceRef1, new Rect(0, 0, 1, 1)), (surfaceRef2, new Rect(0, 0, 1, 1))]); + + // Assert + Assert.That(cache.IsCached, Is.True); + Assert.That(cache.CacheCount, Is.EqualTo(2)); + } + + [Test] + public void StoreCache_Called_ShouldInvalidateExistingCache() + { + // Arrange + using var node = new ContainerRenderNode(); + using var cache = new RenderNodeCache(node); + using (var surfaceRef = Ref.Create(SKSurface.CreateNull(1, 1))) + { + cache.StoreCache(surfaceRef, new Rect(0, 0, 1, 1)); + } + + // Act + using (var newSurfaceRef = Ref.Create(SKSurface.CreateNull(1, 1))) + { + cache.StoreCache(newSurfaceRef, new Rect(0, 0, 1, 1)); + } + + // Assert + Assert.That(cache.IsCached, Is.True); + Assert.That(cache.CacheCount, Is.EqualTo(1)); + } +} \ No newline at end of file diff --git a/tests/Beutl.UnitTests/Engine/Graphics/Rendering/ClearRenderNodeTests.cs b/tests/Beutl.UnitTests/Engine/Graphics/Rendering/ClearRenderNodeTests.cs new file mode 100644 index 000000000..987c0abd8 --- /dev/null +++ b/tests/Beutl.UnitTests/Engine/Graphics/Rendering/ClearRenderNodeTests.cs @@ -0,0 +1,75 @@ +using Beutl.Graphics; +using Beutl.Graphics.Rendering; +using Beutl.Media; +using Moq; +using SkiaSharp; + +namespace Beutl.UnitTests.Engine.Graphics.Rendering; + +public class ClearRenderNodeTest +{ + [Test] + public void Constructor_ShouldInitializeColor() + { + // Arrange + var color = new Color(255, 0, 0, 255); + + // Act + var node = new ClearRenderNode(color); + + // Assert + Assert.That(node.Color, Is.EqualTo(color)); + } + + [Test] + public void Equals_ShouldReturnTrueForSameColor() + { + // Arrange + var color = new Color(255, 0, 0, 255); + var node = new ClearRenderNode(color); + + // Act + var result = node.Equals(color); + + // Assert + Assert.That(result, Is.True); + } + + [Test] + public void Equals_ShouldReturnFalseForDifferentColor() + { + // Arrange + var color1 = new Color(255, 0, 0, 255); + var color2 = new Color(0, 255, 0, 255); + var node = new ClearRenderNode(color1); + + // Act + var result = node.Equals(color2); + + // Assert + Assert.That(result, Is.False); + } + + [Test] + public void Process_ShouldReturnRenderNodeOperation() + { + // Arrange + var color = new Color(255, 0, 0, 255); + var node = new ClearRenderNode(color); + var context = new RenderNodeContext(Mock.Of(), []); + using var surface = SKSurface.CreateNull(100, 100); + using var canvas = new ImmediateCanvas(surface, true); + + // Act + var operations = node.Process(context); + + // Assert + Assert.That(operations, Is.Not.Null); + Assert.That(operations.Length, Is.EqualTo(1)); + Assert.That(operations[0], Is.InstanceOf()); + Assert.That(operations[0].Bounds, Is.EqualTo(Rect.Empty)); + Assert.That(() => operations[0].Render(canvas), Throws.Nothing); + + operations[0].Dispose(); + } +} diff --git a/tests/Beutl.UnitTests/Engine/Graphics/Rendering/ContainerRenderNodeTest.cs b/tests/Beutl.UnitTests/Engine/Graphics/Rendering/ContainerRenderNodeTest.cs new file mode 100644 index 000000000..c53befab9 --- /dev/null +++ b/tests/Beutl.UnitTests/Engine/Graphics/Rendering/ContainerRenderNodeTest.cs @@ -0,0 +1,97 @@ +using Beutl.Graphics; +using Beutl.Graphics.Rendering; +using Moq; + +namespace Beutl.UnitTests.Engine.Graphics.Rendering; + +public class ContainerRenderNodeTest +{ + [Test] + public void AddChild_ShouldAddChild() + { + var node = new ContainerRenderNode(); + var child = new ContainerRenderNode(); + node.AddChild(child); + + Assert.That(node.Children, Contains.Item(child)); + } + + [Test] + public void RemoveChild_ShouldRemoveChild() + { + var node = new ContainerRenderNode(); + var child = new ContainerRenderNode(); + node.AddChild(child); + node.RemoveChild(child); + + Assert.That(node.Children, Does.Not.Contain(child)); + } + + [Test] + public void RemoveRange_ShouldRemoveRangeOfChildren() + { + var node = new ContainerRenderNode(); + var child1 = new ContainerRenderNode(); + var child2 = new ContainerRenderNode(); + node.AddChild(child1); + node.AddChild(child2); + + node.RemoveRange(0, 2); + + Assert.That(node.Children, Is.Empty); + } + + [Test] + public void SetChild_ShouldReplaceChildAtIndex() + { + var node = new ContainerRenderNode(); + var child1 = new ContainerRenderNode(); + var child2 = new ContainerRenderNode(); + node.AddChild(child1); + + node.SetChild(0, child2); + + Assert.That(node.Children[0], Is.EqualTo(child2)); + } + + [Test] + public void BringFrom_ShouldTransferChildrenFromAnotherContainer() + { + var node = new ContainerRenderNode(); + var sourceContainer = new ContainerRenderNode(); + var child = new ContainerRenderNode(); + sourceContainer.AddChild(child); + + node.BringFrom(sourceContainer); + + Assert.That(node.Children, Contains.Item(child)); + Assert.That(sourceContainer.Children, Is.Empty); + } + + [Test] + public void Process_ShouldReturnContextInput() + { + var node = new ContainerRenderNode(); + var context = new RenderNodeContext(Mock.Of(), []); + var result = node.Process(context); + + Assert.That(result, Is.EqualTo(context.Input)); + } + + [Test] + public void OnDispose_ShouldDisposeAllChildren() + { + var node = new ContainerRenderNode(); + var child1 = new ContainerRenderNode(); + var child2 = new ContainerRenderNode(); + node.AddChild(child1); + node.AddChild(child2); + + node.Dispose(); + + Assert.That(node.Children, Is.Empty); + Assert.That(node.IsDisposed, Is.True); + Assert.That(child1.IsDisposed, Is.True); + Assert.That(child2.IsDisposed, Is.True); + } +} diff --git a/tests/Beutl.UnitTests/Engine/Graphics/Rendering/EllipseRenderNodeTests.cs b/tests/Beutl.UnitTests/Engine/Graphics/Rendering/EllipseRenderNodeTests.cs new file mode 100644 index 000000000..147d19763 --- /dev/null +++ b/tests/Beutl.UnitTests/Engine/Graphics/Rendering/EllipseRenderNodeTests.cs @@ -0,0 +1,98 @@ +using Beutl.Graphics.Rendering; +using Beutl.Media; +using Beutl.Graphics; +using Moq; + +namespace Beutl.UnitTests.Engine.Graphics.Rendering; + +[TestFixture] +public class EllipseRenderNodeTest +{ + [Test] + public void Equals_ShouldReturnTrue_WhenAllPropertiesMatch() + { + var rect = new Rect(0, 0, 100, 100); + IBrush fill = new SolidColorBrush(Colors.Red); + IPen pen = new Pen { Brush = Brushes.Black, Thickness = 1 }; + + var node = new EllipseRenderNode(rect, fill, pen); + + Assert.That(node.Equals(rect, fill, pen), Is.True); + } + + [Test] + public void Equals_ShouldReturnFalse_WhenPropertiesDoNotMatch() + { + var rect1 = new Rect(0, 0, 100, 100); + var rect2 = new Rect(0, 0, 200, 200); + IBrush fill1 = new SolidColorBrush(Colors.Red); + IBrush fill2 = new SolidColorBrush(Colors.Blue); + IPen pen1 = new Pen { Brush = Brushes.Black, Thickness = 1 }; + IPen pen2 = new Pen { Brush = Brushes.Black, Thickness = 2 }; + + var node = new EllipseRenderNode(rect1, fill1, pen1); + + Assert.That(node.Equals(rect2, fill1, pen1), Is.False); + Assert.That(node.Equals(rect1, fill2, pen1), Is.False); + Assert.That(node.Equals(rect1, fill1, pen2), Is.False); + } + + [Test] + public void Process_ShouldReturnCorrectRenderNodeOperation() + { + var rect = new Rect(0, 0, 100, 100); + IBrush fill = new SolidColorBrush(Colors.Red); + IPen pen = new Pen { Brush = Brushes.Black, Thickness = 1 }; + var context = new RenderNodeContext(Mock.Of(), []); + + var node = new EllipseRenderNode(rect, fill, pen); + var operations = node.Process(context); + + Assert.That(operations, Is.Not.Null); + Assert.That(operations.Length, Is.EqualTo(1)); + } + + [Test] + public void HitTest_ShouldReturnTrue_WhenPointIsInsideEllipse() + { + var rect = new Rect(0, 0, 100, 100); + IBrush fill = new SolidColorBrush(Colors.Red); + IPen pen = new Pen { Brush = Brushes.Black, Thickness = 1 }; + var context = new RenderNodeContext(Mock.Of(), []); + + var node = new EllipseRenderNode(rect, fill, pen); + var operations = node.Process(context); + var point = new Point(50, 50); + + Assert.That(operations[0].HitTest(point), Is.True); + } + + [Test] + public void HitTest_ShouldReturnFalse_WhenPointIsOutsideEllipse() + { + var rect = new Rect(0, 0, 100, 100); + IBrush fill = new SolidColorBrush(Colors.Red); + IPen pen = new Pen { Brush = Brushes.Black, Thickness = 1 }; + var context = new RenderNodeContext(Mock.Of(), []); + + var node = new EllipseRenderNode(rect, fill, pen); + var operations = node.Process(context); + var point = new Point(150, 150); + + Assert.That(operations[0].HitTest(point), Is.False); + } + + [Test] + public void HitTest_ShouldReturnTrue_WhenPointIsInsideEllipseStroke() + { + var rect = new Rect(25, 25, 75, 75); + IPen pen = new Pen { Brush = Brushes.Black, Thickness = 50 }; + var context = new RenderNodeContext(Mock.Of(), []); + + var node = new EllipseRenderNode(rect, null, pen); + var operations = node.Process(context); + var point = new Point(30, 50); + + Assert.That(operations[0].HitTest(point), Is.True); + } +} diff --git a/tests/Beutl.UnitTests/Engine/Graphics/Rendering/FilterEffectRenderNodeTest.cs b/tests/Beutl.UnitTests/Engine/Graphics/Rendering/FilterEffectRenderNodeTest.cs new file mode 100644 index 000000000..b735cfb54 --- /dev/null +++ b/tests/Beutl.UnitTests/Engine/Graphics/Rendering/FilterEffectRenderNodeTest.cs @@ -0,0 +1,99 @@ +using Beutl.Graphics; +using Beutl.Graphics.Effects; +using Beutl.Graphics.Rendering; +using Beutl.Media; +using Moq; +using SkiaSharp; + +namespace Beutl.UnitTests.Engine.Graphics.Rendering; + +public class FilterEffectRenderNodeTest +{ + private static IImmediateCanvasFactory CreateCanvasFactory() + { + var mock = new Mock(); + mock.Setup(m => m.CreateCanvas(It.IsNotNull(), It.IsAny())) + .Returns(new Func((s, r) => new ImmediateCanvas(s, r))); + + mock.Setup(m => m.CreateRenderTarget(It.IsAny(), It.IsAny())) + .Returns(new Func((w, h) => SKSurface.CreateNull(w, h))); + + mock.Setup(m => m.GetCacheContext()) + .Returns(() => null); + + return mock.Object; + } + + private static RenderNodeContext CreateRenderNodeContext() + { + return new RenderNodeContext(CreateCanvasFactory(), [ + RenderNodeOperation.CreateLambda( + new Rect(0, 0, 100, 100), + canvas => canvas.DrawEllipse(new Rect(0, 0, 100, 100), Brushes.White, null), + point => false + ) + ]); + } + + [Test] + public void Process_ShouldReturnRenderNodeOperations() + { + var effect = new Blur(); + var node = new FilterEffectRenderNode(effect); + var context = CreateRenderNodeContext(); + var operations = node.Process(context); + + Assert.That(operations, Is.Not.Empty); + } + + [Test] + public void Process_ShouldApplyFilterEffect() + { + var effect = new Blur() { Sigma = new(10, 10) }; + var node = new FilterEffectRenderNode(effect); + var context = CreateRenderNodeContext(); + var operations = node.Process(context); + + Assert.That(operations, Is.Not.Empty); + Assert.That(operations[0].Bounds.X, Is.LessThan(0)); + Assert.That(operations[0].Bounds.Y, Is.LessThan(0)); + Assert.That(operations[0].Bounds.Width, Is.GreaterThan(100)); + Assert.That(operations[0].Bounds.Height, Is.GreaterThan(100)); + } + + [Test] + public void Equals_ShouldReturnTrueForSameFilterEffect() + { + var effect = new Blur(); + var node = new FilterEffectRenderNode(effect); + + var result = node.Equals(effect); + + Assert.That(result, Is.True); + } + + // Effectのプロパティを変更するとEqualsがfalseを返す + [Test] + public void Equals_ShouldReturnFalseForDifferentFilterEffectProperty() + { + var effect = new Blur(); + var node = new FilterEffectRenderNode(effect); + effect.Sigma = new(10, 10); + + var result = node.Equals(effect); + + Assert.That(result, Is.False); + } + + [Test] + public void Equals_ShouldReturnFalseForDifferentFilterEffect() + { + var effect1 = new Blur(); + var effect2 = new DropShadow(); + var node = new FilterEffectRenderNode(effect1); + + var result = node.Equals(effect2); + + Assert.That(result, Is.False); + } +} diff --git a/tests/Beutl.UnitTests/Engine/Graphics/Rendering/GeometryRenderNodeTest.cs b/tests/Beutl.UnitTests/Engine/Graphics/Rendering/GeometryRenderNodeTest.cs new file mode 100644 index 000000000..dbc201bfc --- /dev/null +++ b/tests/Beutl.UnitTests/Engine/Graphics/Rendering/GeometryRenderNodeTest.cs @@ -0,0 +1,98 @@ +using Beutl.Graphics.Rendering; +using Beutl.Media; +using Beutl.Graphics; +using Moq; + +namespace Beutl.UnitTests.Engine.Graphics.Rendering; + +[TestFixture] +public class GeometryRenderNodeTest +{ + [Test] + public void Equals_ShouldReturnTrue_WhenAllPropertiesMatch() + { + var geometry = new EllipseGeometry { Width = 100, Height = 100 }; + IBrush fill = new SolidColorBrush(Colors.Red); + IPen pen = new Pen { Brush = Brushes.Black, Thickness = 1 }; + + var node = new GeometryRenderNode(geometry, fill, pen); + + Assert.That(node.Equals(geometry, fill, pen), Is.True); + } + + [Test] + public void Equals_ShouldReturnFalse_WhenPropertiesDoNotMatch() + { + var geometry1 = new EllipseGeometry { Width = 100, Height = 100 }; + var geometry2 = new EllipseGeometry { Width = 100, Height = 100 }; + IBrush fill1 = new SolidColorBrush(Colors.Red); + IBrush fill2 = new SolidColorBrush(Colors.Blue); + IPen pen1 = new Pen { Brush = Brushes.Black, Thickness = 1 }; + IPen pen2 = new Pen { Brush = Brushes.Black, Thickness = 2 }; + + var node = new GeometryRenderNode(geometry1, fill1, pen1); + + Assert.That(node.Equals(geometry2, fill1, pen1), Is.False); + Assert.That(node.Equals(geometry1, fill2, pen1), Is.False); + Assert.That(node.Equals(geometry1, fill1, pen2), Is.False); + } + + [Test] + public void Process_ShouldReturnCorrectRenderNodeOperation() + { + var geometry = new EllipseGeometry { Width = 100, Height = 100 }; + IBrush fill = new SolidColorBrush(Colors.Red); + IPen pen = new Pen { Brush = Brushes.Black, Thickness = 1 }; + var context = new RenderNodeContext(Mock.Of(), []); + + var node = new GeometryRenderNode(geometry, fill, pen); + var operations = node.Process(context); + + Assert.That(operations, Is.Not.Null); + Assert.That(operations.Length, Is.EqualTo(1)); + } + + [Test] + public void HitTest_ShouldReturnTrue_WhenPointIsInsideGeometry() + { + var geometry = new EllipseGeometry { Width = 100, Height = 100 }; + IBrush fill = new SolidColorBrush(Colors.Red); + IPen pen = new Pen { Brush = Brushes.Black, Thickness = 1 }; + var context = new RenderNodeContext(Mock.Of(), []); + + var node = new GeometryRenderNode(geometry, fill, pen); + var operations = node.Process(context); + var point = new Point(50, 50); + + Assert.That(operations[0].HitTest(point), Is.True); + } + + [Test] + public void HitTest_ShouldReturnFalse_WhenPointIsOutsideGeometry() + { + var geometry = new EllipseGeometry { Width = 100, Height = 100 }; + IBrush fill = new SolidColorBrush(Colors.Red); + IPen pen = new Pen { Brush = Brushes.Black, Thickness = 1 }; + var context = new RenderNodeContext(Mock.Of(), []); + + var node = new GeometryRenderNode(geometry, fill, pen); + var operations = node.Process(context); + var point = new Point(150, 150); + + Assert.That(operations[0].HitTest(point), Is.False); + } + + [Test] + public void HitTest_ShouldReturnTrue_WhenPointIsInsideGeometryStroke() + { + var geometry = new EllipseGeometry { Width = 100, Height = 100 }; + IPen pen = new Pen { Brush = Brushes.Black, Thickness = 50 }; + var context = new RenderNodeContext(Mock.Of(), []); + + var node = new GeometryRenderNode(geometry, null, pen); + var operations = node.Process(context); + var point = new Point(0, 50); + + Assert.That(operations[0].HitTest(point), Is.True); + } +} diff --git a/tests/Beutl.UnitTests/Engine/Graphics/Rendering/GraphicsContext2DTests.cs b/tests/Beutl.UnitTests/Engine/Graphics/Rendering/GraphicsContext2DTests.cs new file mode 100644 index 000000000..05cb4887a --- /dev/null +++ b/tests/Beutl.UnitTests/Engine/Graphics/Rendering/GraphicsContext2DTests.cs @@ -0,0 +1,340 @@ +using Beutl.Graphics; +using Beutl.Graphics.Effects; +using Beutl.Graphics.Rendering; +using Beutl.Graphics.Shapes; +using Beutl.Graphics.Transformation; +using Beutl.Logging; +using Beutl.Media; +using Beutl.Media.Source; +using Microsoft.Extensions.Logging; +using Moq; + +namespace Beutl.UnitTests.Engine.Graphics.Rendering; + +public class GraphicsContext2DTests +{ + [SetUp] + public void Setup() + { + Log.LoggerFactory = LoggerFactory.Create(b => b.AddSimpleConsole()); + } + + [Test] + public void ShouldTriggerOnUntrackedEvent() + { + var drawable = new RectShape + { + AlignmentX = AlignmentX.Center, + AlignmentY = AlignmentY.Center, + TransformOrigin = RelativePoint.Center, + Width = 100, + Height = 100, + Fill = Brushes.White, + FilterEffect = new FilterEffectGroup { Children = { new SplitEffect(), new InnerShadow() } }, + Transform = new TransformGroup { Children = { new RotationTransform(), new ScaleTransform() } } + }; + + var node = new DrawableRenderNode(drawable); + var context = new GraphicsContext2D(node, new PixelSize(1920, 1080)); + drawable.Render(context); + + ((FilterEffectGroup)drawable.FilterEffect).Children.RemoveAt(0); + context = new GraphicsContext2D(node, new PixelSize(1920, 1080)); + bool triggered = false; + RenderNode? untrackedNode = null; + context.OnUntracked = n => + { + triggered = true; + untrackedNode = n; + }; + drawable.Render(context); + + Assert.That(triggered, Is.True); + Assert.That(untrackedNode, Is.Not.Null); + Assert.That(untrackedNode, Is.TypeOf()); + } + + [Test] + public void Clear_ShouldCreateClearRenderNode() + { + var node = new ContainerRenderNode(); + var context = new GraphicsContext2D(node, new PixelSize(1920, 1080)); + + context.Clear(); + + Assert.That(node.Children, Is.Not.Empty); + Assert.That(node.Children[0], Is.InstanceOf()); + } + + [Test] + public void ClearWithColor_ShouldCreateClearRenderNode() + { + var node = new ContainerRenderNode(); + var context = new GraphicsContext2D(node, new PixelSize(1920, 1080)); + + context.Clear(Colors.White); + + Assert.That(node.Children, Is.Not.Empty); + Assert.That(node.Children[0], Is.InstanceOf()); + Assert.That(((ClearRenderNode)node.Children[0]).Color, Is.EqualTo(Colors.White)); + } + + [Test] + public void DrawImageSource_ShouldCreateImageSourceRenderNode() + { + var node = new ContainerRenderNode(); + var context = new GraphicsContext2D(node, new PixelSize(1920, 1080)); + var image = new Mock(); + image.Setup(i => i.FrameSize).Returns(new PixelSize(100, 100)); + image.Setup(i => i.Clone()).Returns(() => image.Object); + + context.DrawImageSource(image.Object, Brushes.White, null); + + Assert.That(node.Children, Is.Not.Empty); + Assert.That(node.Children[0], Is.InstanceOf()); + } + + [Test] + public void DrawVideoSource_ShouldCreateVideoSourceRenderNode() + { + var node = new ContainerRenderNode(); + var context = new GraphicsContext2D(node, new PixelSize(1920, 1080)); + var video = new Mock(); + video.Setup(v => v.FrameSize).Returns(new PixelSize(100, 100)); + video.Setup(v => v.FrameRate).Returns(new Rational(30)); + video.Setup(v => v.Clone()).Returns(() => video.Object); + + context.DrawVideoSource(video.Object, TimeSpan.Zero, Brushes.White, null); + + Assert.That(node.Children, Is.Not.Empty); + Assert.That(node.Children[0], Is.InstanceOf()); + } + + [Test] + public void DrawEllipse_ShouldCreateEllipseRenderNode() + { + var node = new ContainerRenderNode(); + var context = new GraphicsContext2D(node, new PixelSize(1920, 1080)); + + context.DrawEllipse(new Rect(0, 0, 100, 100), Brushes.White, null); + + Assert.That(node.Children, Is.Not.Empty); + Assert.That(node.Children[0], Is.InstanceOf()); + } + + [Test] + public void DrawGeometry_ShouldCreateGeometryRenderNode() + { + var node = new ContainerRenderNode(); + var context = new GraphicsContext2D(node, new PixelSize(1920, 1080)); + var geometry = new EllipseGeometry { Width = 100, Height = 100 }; + + context.DrawGeometry(geometry, Brushes.White, null); + + Assert.That(node.Children, Is.Not.Empty); + Assert.That(node.Children[0], Is.InstanceOf()); + } + + [Test] + public void DrawRectangle_ShouldCreateRectangleRenderNode() + { + var node = new ContainerRenderNode(); + var context = new GraphicsContext2D(node, new PixelSize(1920, 1080)); + + context.DrawRectangle(new Rect(0, 0, 100, 100), Brushes.White, null); + + Assert.That(node.Children, Is.Not.Empty); + Assert.That(node.Children[0], Is.InstanceOf()); + } + + [Test] + public void DrawDrawable_ShouldCreateDrawableRenderNode() + { + var drawable = new RectShape(); + var node = new ContainerRenderNode(); + var context = new GraphicsContext2D(node, new PixelSize(1920, 1080)); + + context.DrawDrawable(drawable); + + Assert.That(node.Children, Is.Not.Empty); + Assert.That(node.Children[0], Is.InstanceOf()); + } + + [Test] + public void DrawNode_ShouldAddPassedNodeDirectly() + { + var node = new ContainerRenderNode(); + var context = new GraphicsContext2D(node, new PixelSize(1920, 1080)); + var child = new ContainerRenderNode(); + + context.DrawNode(child); + + Assert.That(node.Children, Is.Not.Empty); + Assert.That(node.Children[0], Is.EqualTo(child)); + } + + [Test] + public void DrawBackdrop_ShouldCreateDrawBackdropRenderNode() + { + var node = new ContainerRenderNode(); + var context = new GraphicsContext2D(node, new PixelSize(1920, 1080)); + var backdrop = new Mock(); + + context.DrawBackdrop(backdrop.Object); + + Assert.That(node.Children, Is.Not.Empty); + Assert.That(node.Children[0], Is.InstanceOf()); + } + + [Test] + public void Snapshot_ShouldCreateSnapshotBackdropRenderNode() + { + var node = new ContainerRenderNode(); + var context = new GraphicsContext2D(node, new PixelSize(1920, 1080)); + + _ = context.Snapshot(); + + Assert.That(node.Children, Is.Not.Empty); + Assert.That(node.Children[0], Is.InstanceOf()); + } + + [Test] + public void Push_ShouldCreatePushRenderNode() + { + var node = new ContainerRenderNode(); + var context = new GraphicsContext2D(node, new PixelSize(1920, 1080)); + + context.Push().Dispose(); + + Assert.That(node.Children, Is.Not.Empty); + Assert.That(node.Children[0], Is.InstanceOf()); + } + + [Test] + public void PushLayer_ShouldCreateLayerRenderNode() + { + var node = new ContainerRenderNode(); + var context = new GraphicsContext2D(node, new PixelSize(1920, 1080)); + + context.PushLayer().Dispose(); + + Assert.That(node.Children, Is.Not.Empty); + Assert.That(node.Children[0], Is.InstanceOf()); + } + + [Test] + public void PushBlendMode_ShouldCreateBlendModeRenderNode() + { + var node = new ContainerRenderNode(); + var context = new GraphicsContext2D(node, new PixelSize(1920, 1080)); + + context.PushBlendMode(BlendMode.Clear).Dispose(); + + Assert.That(node.Children, Is.Not.Empty); + Assert.That(node.Children[0], Is.InstanceOf()); + } + + [Test] + public void PushClip_ShouldCreateRectClipRenderNode() + { + var node = new ContainerRenderNode(); + var context = new GraphicsContext2D(node, new PixelSize(1920, 1080)); + + context.PushClip(new Rect(0, 0, 100, 100)).Dispose(); + + Assert.That(node.Children, Is.Not.Empty); + Assert.That(node.Children[0], Is.InstanceOf()); + } + + [Test] + public void PushClipGeometry_ShouldCreateGeometryClipRenderNode() + { + var node = new ContainerRenderNode(); + var context = new GraphicsContext2D(node, new PixelSize(1920, 1080)); + var geometry = new EllipseGeometry { Width = 100, Height = 100 }; + + context.PushClip(geometry).Dispose(); + + Assert.That(node.Children, Is.Not.Empty); + Assert.That(node.Children[0], Is.InstanceOf()); + } + + [Test] + public void PushOpacity_ShouldCreateOpacityRenderNode() + { + var node = new ContainerRenderNode(); + var context = new GraphicsContext2D(node, new PixelSize(1920, 1080)); + + context.PushOpacity(0.5f).Dispose(); + + Assert.That(node.Children, Is.Not.Empty); + Assert.That(node.Children[0], Is.InstanceOf()); + } + + [Test] + public void PushFilterEffect_ShouldCreateFilterEffectRenderNode() + { + var node = new ContainerRenderNode(); + var context = new GraphicsContext2D(node, new PixelSize(1920, 1080)); + var effect = new Blur(); + + context.PushFilterEffect(effect).Dispose(); + + Assert.That(node.Children, Is.Not.Empty); + Assert.That(node.Children[0], Is.InstanceOf()); + } + + [Test] + public void PushOpacityMask_ShouldCreateOpacityMaskRenderNode() + { + var node = new ContainerRenderNode(); + var context = new GraphicsContext2D(node, new PixelSize(1920, 1080)); + var mask = Brushes.White; + + context.PushOpacityMask(mask, new Rect(0, 0, 100, 100)).Dispose(); + + Assert.That(node.Children, Is.Not.Empty); + Assert.That(node.Children[0], Is.InstanceOf()); + } + + [Test] + public void PushTransform_ShouldCreateTransformRenderNode() + { + var node = new ContainerRenderNode(); + var context = new GraphicsContext2D(node, new PixelSize(1920, 1080)); + var transform = new RotationTransform(); + + context.PushTransform(transform).Dispose(); + + Assert.That(node.Children, Is.Not.Empty); + Assert.That(node.Children[0], Is.InstanceOf()); + } + + [Test] + public void PushTransformGroup_ShouldCreateTransformRenderNode() + { + var node = new ContainerRenderNode(); + var context = new GraphicsContext2D(node, new PixelSize(1920, 1080)); + var transform = new TransformGroup { Children = { new RotationTransform(), new ScaleTransform() } }; + + context.PushTransform(transform).Dispose(); + + Assert.That(node.Children, Is.Not.Empty); + Assert.That(node.Children[0], Is.InstanceOf()); + Assert.That(((TransformRenderNode)node.Children[0]).Children, Is.Not.Empty); + Assert.That(((TransformRenderNode)node.Children[0]).Children[0], Is.InstanceOf()); + } + + [Test] + public void PushMatrixTransform_ShouldCreateTransformRenderNode() + { + var node = new ContainerRenderNode(); + var context = new GraphicsContext2D(node, new PixelSize(1920, 1080)); + var matrix = Matrix.CreateRotation(45); + + context.PushTransform(matrix).Dispose(); + + Assert.That(node.Children, Is.Not.Empty); + Assert.That(node.Children[0], Is.InstanceOf()); + } +} diff --git a/tests/Beutl.UnitTests/Engine/Graphics/Rendering/RectClipRenderNodeTest.cs b/tests/Beutl.UnitTests/Engine/Graphics/Rendering/RectClipRenderNodeTest.cs new file mode 100644 index 000000000..5277c8d1e --- /dev/null +++ b/tests/Beutl.UnitTests/Engine/Graphics/Rendering/RectClipRenderNodeTest.cs @@ -0,0 +1,53 @@ +using Beutl.Graphics.Rendering; +using Beutl.Graphics; +using Moq; + +namespace Beutl.UnitTests.Engine.Graphics.Rendering; + +[TestFixture] +public class RectClipRenderNodeTest +{ + [Test] + public void Equals_ShouldReturnTrue_WhenAllPropertiesMatch() + { + var rect = new Rect(0, 0, 100, 100); + var operation = ClipOperation.Intersect; + var node = new RectClipRenderNode(rect, operation); + + Assert.That(node.Equals(rect, operation), Is.True); + } + + [Test] + public void Equals_ShouldReturnFalse_WhenPropertiesDoNotMatch() + { + var rect = new Rect(0, 0, 100, 100); + var operation = ClipOperation.Intersect; + var node = new RectClipRenderNode(rect, operation); + + Assert.That(node.Equals(default, operation), Is.False); + } + + [Test] + public void Process_WithoutInput_ShouldReturnEmptyRenderNodeOperation() + { + var context = new RenderNodeContext(Mock.Of(), []); + + var node = new RectClipRenderNode(new Rect(0, 0, 100, 100), ClipOperation.Intersect); + var operations = node.Process(context); + + Assert.That(operations, Is.Empty); + } + + [Test] + public void Process_WithInput_ShouldReturnExpectedRenderNodeOperation() + { + var context = new RenderNodeContext(Mock.Of(), [ + RenderNodeOperation.CreateLambda(default, _ => { }) + ]); + + var node = new RectClipRenderNode(new Rect(0, 0, 100, 100), ClipOperation.Intersect); + var operations = node.Process(context); + + Assert.That(operations, Is.Not.Empty); + } +} diff --git a/tests/Beutl.UnitTests/Engine/Graphics/Rendering/RectangleRenderNodeTest.cs b/tests/Beutl.UnitTests/Engine/Graphics/Rendering/RectangleRenderNodeTest.cs new file mode 100644 index 000000000..069c39589 --- /dev/null +++ b/tests/Beutl.UnitTests/Engine/Graphics/Rendering/RectangleRenderNodeTest.cs @@ -0,0 +1,98 @@ +using Beutl.Graphics.Rendering; +using Beutl.Media; +using Beutl.Graphics; +using Moq; + +namespace Beutl.UnitTests.Engine.Graphics.Rendering; + +[TestFixture] +public class RectangleRenderNodeTest +{ + [Test] + public void Equals_ShouldReturnTrue_WhenAllPropertiesMatch() + { + var rect = new Rect(0, 0, 100, 100); + IBrush fill = new SolidColorBrush(Colors.Red); + IPen pen = new Pen { Brush = Brushes.Black, Thickness = 1 }; + + var node = new RectangleRenderNode(rect, fill, pen); + + Assert.That(node.Equals(rect, fill, pen), Is.True); + } + + [Test] + public void Equals_ShouldReturnFalse_WhenPropertiesDoNotMatch() + { + var rect1 = new Rect(0, 0, 100, 100); + var rect2 = new Rect(0, 0, 200, 200); + IBrush fill1 = new SolidColorBrush(Colors.Red); + IBrush fill2 = new SolidColorBrush(Colors.Blue); + IPen pen1 = new Pen { Brush = Brushes.Black, Thickness = 1 }; + IPen pen2 = new Pen { Brush = Brushes.Black, Thickness = 2 }; + + var node = new RectangleRenderNode(rect1, fill1, pen1); + + Assert.That(node.Equals(rect2, fill1, pen1), Is.False); + Assert.That(node.Equals(rect1, fill2, pen1), Is.False); + Assert.That(node.Equals(rect1, fill1, pen2), Is.False); + } + + [Test] + public void Process_ShouldReturnCorrectRenderNodeOperation() + { + var rect = new Rect(0, 0, 100, 100); + IBrush fill = new SolidColorBrush(Colors.Red); + IPen pen = new Pen { Brush = Brushes.Black, Thickness = 1 }; + var context = new RenderNodeContext(Mock.Of(), []); + + var node = new RectangleRenderNode(rect, fill, pen); + var operations = node.Process(context); + + Assert.That(operations, Is.Not.Null); + Assert.That(operations.Length, Is.EqualTo(1)); + } + + [Test] + public void HitTest_ShouldReturnTrue_WhenPointIsInsideRectangle() + { + var rect = new Rect(0, 0, 100, 100); + IBrush fill = new SolidColorBrush(Colors.Red); + IPen pen = new Pen { Brush = Brushes.Black, Thickness = 1 }; + var context = new RenderNodeContext(Mock.Of(), []); + + var node = new RectangleRenderNode(rect, fill, pen); + var operations = node.Process(context); + var point = new Point(50, 50); + + Assert.That(operations[0].HitTest(point), Is.True); + } + + [Test] + public void HitTest_ShouldReturnFalse_WhenPointIsOutsideRectangle() + { + var rect = new Rect(0, 0, 100, 100); + IBrush fill = new SolidColorBrush(Colors.Red); + IPen pen = new Pen { Brush = Brushes.Black, Thickness = 1 }; + var context = new RenderNodeContext(Mock.Of(), []); + + var node = new RectangleRenderNode(rect, fill, pen); + var operations = node.Process(context); + var point = new Point(150, 150); + + Assert.That(operations[0].HitTest(point), Is.False); + } + + [Test] + public void HitTest_ShouldReturnTrue_WhenPointIsInsideRectangleStroke() + { + var rect = new Rect(25, 25, 75, 75); + IPen pen = new Pen { Brush = Brushes.Black, Thickness = 50 }; + var context = new RenderNodeContext(Mock.Of(), []); + + var node = new RectangleRenderNode(rect, null, pen); + var operations = node.Process(context); + var point = new Point(30, 50); + + Assert.That(operations[0].HitTest(point), Is.True); + } +} diff --git a/tests/Beutl.UnitTests/Engine/ShapeTests.cs b/tests/Beutl.UnitTests/Engine/ShapeTests.cs index 1a7dfb38b..b3b5db0b7 100644 --- a/tests/Beutl.UnitTests/Engine/ShapeTests.cs +++ b/tests/Beutl.UnitTests/Engine/ShapeTests.cs @@ -2,6 +2,7 @@ using Beutl.Graphics.Shapes; using Beutl.Logging; using Beutl.Media; +using Beutl.Media.Immutable; using Beutl.Media.Pixel; using Microsoft.Extensions.Logging; using NUnit.Framework.Legacy; @@ -33,7 +34,7 @@ public void DrawRectangle() using var canvas = new ImmediateCanvas(250, 250); canvas.Clear(Colors.Black); - shape.Render(canvas); + canvas.DrawDrawable(shape); using Bitmap bmp = canvas.GetBitmap(); @@ -66,7 +67,7 @@ public void DrawRectangleWithPen() canvas.Clear(Colors.Black); - shape.Render(canvas); + canvas.DrawDrawable(shape); using Bitmap bmp = canvas.GetBitmap(); @@ -90,7 +91,7 @@ public void DrawEllipse() using var canvas = new ImmediateCanvas(250, 250); canvas.Clear(Colors.Black); - shape.Render(canvas); + canvas.DrawDrawable(shape); using Bitmap bmp = canvas.GetBitmap(); @@ -116,7 +117,7 @@ public void DrawRoundedRect() using var canvas = new ImmediateCanvas(250, 250); canvas.Clear(Colors.Black); - shape.Render(canvas); + canvas.DrawDrawable(shape); using Bitmap bmp = canvas.GetBitmap(); @@ -152,7 +153,7 @@ public void DrawRoundedRectWithStroke(StrokeAlignment alignment) using var canvas = new ImmediateCanvas(250, 250); canvas.Clear(Colors.Black); - shape.Render(canvas); + canvas.DrawDrawable(shape); using Bitmap bmp = canvas.GetBitmap(); @@ -185,13 +186,14 @@ public void DrawGeometry() TransformOrigin = RelativePoint.Center, Data = geometry, - Fill = Brushes.White + Fill = Brushes.White, + Transform = new ImmutableTransform(Matrix.CreateTranslation(-geometry.Bounds.Position)) }; using var canvas = new ImmediateCanvas(250, 250); canvas.Clear(Colors.Black); - shape.Render(canvas); + canvas.DrawDrawable(shape); using Bitmap bmp = canvas.GetBitmap(); @@ -238,13 +240,14 @@ public void DrawGeometryWithPen(StrokeAlignment alignment, PathFillType fillType Thickness = 10, StrokeCap = StrokeCap.Round, StrokeAlignment = alignment, - } + }, + Transform = new ImmutableTransform(Matrix.CreateTranslation(-geometry.Bounds.Position)) }; using var canvas = new ImmediateCanvas(250, 250); canvas.Clear(Colors.Black); - shape.Render(canvas); + canvas.DrawDrawable(shape); using Bitmap bmp = canvas.GetBitmap(); diff --git a/tests/Beutl.UnitTests/Engine/TextBlockTests.cs b/tests/Beutl.UnitTests/Engine/TextBlockTests.cs index d4b733903..4f339928a 100644 --- a/tests/Beutl.UnitTests/Engine/TextBlockTests.cs +++ b/tests/Beutl.UnitTests/Engine/TextBlockTests.cs @@ -50,13 +50,13 @@ public void ParseAndDraw(string str, int id) tb.Measure(Size.Infinity); Rect bounds = tb.Bounds; - using var graphics = new ImmediateCanvas((int)bounds.Width, (int)bounds.Height); + using var canvas = new ImmediateCanvas((int)bounds.Width, (int)bounds.Height); - graphics.Clear(Colors.White); + canvas.Clear(Colors.White); - tb.Render(graphics); + canvas.DrawDrawable(tb); - using Bitmap bmp = graphics.GetBitmap(); + using Bitmap bmp = canvas.GetBitmap(); ClassicAssert.IsTrue(bmp.Save(Path.Combine(ArtifactProvider.GetArtifactDirectory(), $"{id}.png"), EncodedImageFormat.Png)); } diff --git a/tests/Beutl.UnitTests/ProjectSystem/Operation/PublishOperatorTests.cs b/tests/Beutl.UnitTests/ProjectSystem/Operation/PublishOperatorTests.cs index 95d8a49d3..945b7006c 100644 --- a/tests/Beutl.UnitTests/ProjectSystem/Operation/PublishOperatorTests.cs +++ b/tests/Beutl.UnitTests/ProjectSystem/Operation/PublishOperatorTests.cs @@ -1,5 +1,6 @@ using Beutl.Graphics; using Beutl.Graphics.Effects; +using Beutl.Graphics.Rendering; using Beutl.Graphics.Transformation; using Beutl.Media; using Beutl.Operation; @@ -12,7 +13,7 @@ private class TestDrawable : Drawable { protected override Size MeasureCore(Size availableSize) => throw new NotImplementedException(); - protected override void OnDraw(ICanvas canvas) => throw new NotImplementedException(); + protected override void OnDraw(GraphicsContext2D context) => throw new NotImplementedException(); } private class TestOperator() : PublishOperator( diff --git a/tests/TextFormattingPlayground/MainWindow.axaml.cs b/tests/TextFormattingPlayground/MainWindow.axaml.cs index aeab60fea..72466711e 100644 --- a/tests/TextFormattingPlayground/MainWindow.axaml.cs +++ b/tests/TextFormattingPlayground/MainWindow.axaml.cs @@ -50,7 +50,7 @@ private unsafe void Draw() using var canvas = new Canvas(width, height); canvas.Clear(Beutl.Media.Colors.Black); - _text.Render(canvas); + canvas.DrawDrawable(_text); using var bmp = canvas.GetBitmap();