From 2728dc46f7e66719c6be2d91b2a68f7399bbcdb3 Mon Sep 17 00:00:00 2001 From: Lehonti Ramos <17771375+Lehonti@users.noreply.github.com> Date: Sun, 22 Sep 2024 01:25:21 +0200 Subject: [PATCH] Created method for slicing ROI into tiles, for use inside `AsyncEffectRenderer` (#983) --- Pinta.Core/Classes/AsyncEffectRenderer.cs | 91 +++++++++-------------- Pinta.Core/Extensions/OtherExtensions.cs | 25 +++++-- Pinta.Core/Managers/LivePreviewManager.cs | 15 +--- tests/Pinta.Core.Tests/RectangleTests.cs | 23 ++++++ 4 files changed, 81 insertions(+), 73 deletions(-) diff --git a/Pinta.Core/Classes/AsyncEffectRenderer.cs b/Pinta.Core/Classes/AsyncEffectRenderer.cs index 49087e477..5a49ea52c 100644 --- a/Pinta.Core/Classes/AsyncEffectRenderer.cs +++ b/Pinta.Core/Classes/AsyncEffectRenderer.cs @@ -30,6 +30,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Threading; using Debug = System.Diagnostics.Debug; @@ -44,24 +45,24 @@ internal abstract class AsyncEffectRenderer internal sealed class Settings { internal int ThreadCount { get; } - internal int TileWidth { get; } - internal int TileHeight { get; } + internal RectangleI RenderBounds { get; } + internal bool EffectIsTileable { get; } internal int UpdateMillis { get; } internal ThreadPriority ThreadPriority { get; } internal Settings ( int threadCount, - int tileWidth, - int tileHeight, + RectangleI renderBounds, + bool effectIsTileable, int updateMilliseconds, ThreadPriority threadPriority) { - if (tileWidth < 0) throw new ArgumentOutOfRangeException (nameof (tileWidth), "Cannot be negative"); - if (tileHeight < 0) throw new ArgumentOutOfRangeException (nameof (tileHeight), "Cannot be negative"); + if (renderBounds.Width < 0) throw new ArgumentException ("Width cannot be negative", nameof (renderBounds)); + if (renderBounds.Height < 0) throw new ArgumentException ("Height cannot be negative", nameof (renderBounds)); if (updateMilliseconds <= 0) throw new ArgumentOutOfRangeException (nameof (updateMilliseconds), "Strictly positive value expected"); if (threadCount < 1) throw new ArgumentOutOfRangeException (nameof (threadCount), "Invalid number of threads"); - TileWidth = tileWidth; - TileHeight = tileHeight; + RenderBounds = renderBounds; + EffectIsTileable = effectIsTileable; ThreadCount = threadCount; UpdateMillis = updateMilliseconds; ThreadPriority = threadPriority; @@ -71,14 +72,13 @@ internal Settings ( BaseEffect? effect; Cairo.ImageSurface? source_surface; Cairo.ImageSurface? dest_surface; - RectangleI render_bounds; bool is_rendering; bool cancel_render_flag; bool restart_render_flag; int render_id; int current_tile; - int total_tiles; + readonly ImmutableArray target_tiles; readonly List render_exceptions; uint timer_tick_id; @@ -101,6 +101,10 @@ internal AsyncEffectRenderer (Settings settings) render_exceptions = new List (); timer_tick_id = 0; + target_tiles = + settings.EffectIsTileable + ? settings.RenderBounds.ToRows ().ToImmutableArray () // If effect is tileable, render each row in parallel. + : ImmutableArray.Create (settings.RenderBounds); // If the effect isn't tileable, there is a single tile for the entire render bounds this.settings = settings; } @@ -109,10 +113,10 @@ internal AsyncEffectRenderer (Settings settings) internal double Progress { get { - if (total_tiles == 0 || current_tile < 0) + if (target_tiles.Length == 0 || current_tile < 0) return 0; - else if (current_tile < total_tiles) - return current_tile / (double) total_tiles; + else if (current_tile < target_tiles.Length) + return current_tile / (double) target_tiles.Length; else return 1; } @@ -121,8 +125,7 @@ internal double Progress { internal void Start ( BaseEffect effect, Cairo.ImageSurface source, - Cairo.ImageSurface dest, - RectangleI renderBounds) + Cairo.ImageSurface dest) { Debug.WriteLine ("AsyncEffectRenderer.Start ()"); @@ -132,7 +135,6 @@ internal void Start ( source_surface = source; dest_surface = dest; - render_bounds = renderBounds; // If a render is already in progress, then cancel it, // and start a new render. @@ -179,8 +181,6 @@ void StartRender () current_tile = -1; - total_tiles = CalculateTotalTiles (); - Debug.WriteLine ("AsyncEffectRenderer.Start () Render " + render_id + " starting."); // Copy the current render id. @@ -193,7 +193,7 @@ void StartRender () slaves[threadId - 1] = StartSlaveThread (renderId, threadId); // Start the master render thread. - var master = new Thread (() => { + Thread master = new (() => { // Do part of the rendering on the master thread. Render (renderId, 0); @@ -218,7 +218,7 @@ void StartRender () Thread StartSlaveThread (int renderId, int threadId) { - var slave = new Thread (() => { + Thread slave = new (() => { Render (renderId, threadId); }) { Priority = settings.ThreadPriority @@ -234,7 +234,7 @@ void Render (int renderId, int threadId) // Fetch the next tile index and render it. for (; ; ) { int tileIndex = Interlocked.Increment (ref current_tile); - if (tileIndex >= total_tiles || cancel_render_flag) + if (tileIndex >= target_tiles.Length || cancel_render_flag) return; RenderTile (renderId, threadId, tileIndex); } @@ -244,17 +244,14 @@ void Render (int renderId, int threadId) void RenderTile (int renderId, int threadId, int tileIndex) { Exception? exception = null; - var bounds = new RectangleI (); + RectangleI tileBounds = target_tiles[tileIndex]; try { - - bounds = GetTileBounds (tileIndex); - // NRT - These are set in Start () before getting here if (!cancel_render_flag) { dest_surface!.Flush (); - effect!.Render (source_surface!, dest_surface, stackalloc[] { bounds }); - dest_surface.MarkDirty (bounds); + effect!.Render (source_surface!, dest_surface, stackalloc[] { tileBounds }); + dest_surface.MarkDirty (tileBounds); } } catch (Exception ex) { @@ -269,38 +266,19 @@ void RenderTile (int renderId, int threadId, int tileIndex) // Update bounds to be shown on next expose. lock (updated_lock) { if (is_updated) { - updated_area = RectangleI.Union (bounds, updated_area); + updated_area = RectangleI.Union (tileBounds, updated_area); } else { is_updated = true; - updated_area = bounds; - } - } - - if (exception != null) { - lock (render_exceptions) { - render_exceptions.Add (exception); + updated_area = tileBounds; } } - } - // Runs on a background thread. - RectangleI GetTileBounds (int tileIndex) - { - int horizTileCount = (int) Math.Ceiling (render_bounds.Width - / (float) settings.TileWidth); - - int x = ((tileIndex % horizTileCount) * settings.TileWidth) + render_bounds.X; - int y = ((tileIndex / horizTileCount) * settings.TileHeight) + render_bounds.Y; - int w = Math.Min (settings.TileWidth, render_bounds.Right + 1 - x); - int h = Math.Min (settings.TileHeight, render_bounds.Bottom + 1 - y); - - return new RectangleI (x, y, w, h); - } + if (exception == null) + return; - int CalculateTotalTiles () - { - return (int) (Math.Ceiling (render_bounds.Width / (float) settings.TileWidth) - * Math.Ceiling (render_bounds.Height / (float) settings.TileHeight)); + lock (render_exceptions) { + render_exceptions.Add (exception); + } } // Called on the UI thread. @@ -328,9 +306,10 @@ bool HandleTimerTick () void HandleRenderCompletion () { - var exceptions = (render_exceptions.Count == 0) - ? Array.Empty () - : render_exceptions.ToArray (); + var exceptions = + render_exceptions.Count == 0 + ? Array.Empty () + : render_exceptions.ToArray (); HandleTimerTick (); diff --git a/Pinta.Core/Extensions/OtherExtensions.cs b/Pinta.Core/Extensions/OtherExtensions.cs index dbf4f4733..371bd790b 100644 --- a/Pinta.Core/Extensions/OtherExtensions.cs +++ b/Pinta.Core/Extensions/OtherExtensions.cs @@ -68,12 +68,25 @@ public static ReadOnlyCollection ToReadOnlyCollection (this ImmutableList< private sealed class ImmutableBackedReadOnlyCollection : ReadOnlyCollection { - internal ImmutableBackedReadOnlyCollection (ImmutableList list) : base (list) - { - } - internal ImmutableBackedReadOnlyCollection (ImmutableArray array) : base (array) - { - } + internal ImmutableBackedReadOnlyCollection (ImmutableList list) + : base (list) + { } + + internal ImmutableBackedReadOnlyCollection (ImmutableArray array) + : base (array) + { } + } + + public static IEnumerable ToRows (this RectangleI original) + { + if (original.Height < 0) throw new ArgumentException ("Height cannot be negative", nameof (original)); + if (original.Height == 0) yield break; + for (int i = 0; i < original.Height; i++) + yield return new ( + original.X, + original.Y + i, + original.Width, + 1); } public static bool In (this T enumeration, params T[] values) diff --git a/Pinta.Core/Managers/LivePreviewManager.cs b/Pinta.Core/Managers/LivePreviewManager.cs index c794c4ba1..ceda3d2fa 100644 --- a/Pinta.Core/Managers/LivePreviewManager.cs +++ b/Pinta.Core/Managers/LivePreviewManager.cs @@ -123,24 +123,17 @@ public void Start (BaseEffect effect) Started?.Invoke (this, new LivePreviewStartedEventArgs ()); - // If the effect isn't tileable, there is a single tile for the entire render bounds. - // Otherwise, render each row in parallel. - int tileWidth = render_bounds.Width; - int tileHeight = 1; - if (!effect.IsTileable) - tileHeight = render_bounds.Height; - AsyncEffectRenderer.Settings settings = new ( threadCount: system_manager.RenderThreads, - tileWidth: tileWidth, - tileHeight: tileHeight, + renderBounds: render_bounds, + effectIsTileable: effect.IsTileable, updateMilliseconds: 100, threadPriority: ThreadPriority.BelowNormal); Debug.WriteLine (DateTime.Now.ToString ("HH:mm:ss:ffff") + "Start Live preview."); renderer = new Renderer (this, settings, chrome_manager); - renderer.Start (effect, layer.Surface, live_preview_surface, render_bounds); + renderer.Start (effect, layer.Surface, live_preview_surface); if (effect.IsConfigurable) { EventHandler? handler = null; @@ -277,7 +270,7 @@ void CleanUp () void EffectData_PropertyChanged (object? sender, PropertyChangedEventArgs e) { //TODO calculate bounds. - renderer.Start (effect, layer.Surface, live_preview_surface, render_bounds); + renderer.Start (effect, layer.Surface, live_preview_surface); } private sealed class Renderer : AsyncEffectRenderer diff --git a/tests/Pinta.Core.Tests/RectangleTests.cs b/tests/Pinta.Core.Tests/RectangleTests.cs index e9a0a7f50..06b65a0ee 100644 --- a/tests/Pinta.Core.Tests/RectangleTests.cs +++ b/tests/Pinta.Core.Tests/RectangleTests.cs @@ -48,6 +48,29 @@ public void CorrectIntersection (RectangleI a, RectangleI b, RectangleI expected public void CorrectInflation (RectangleI a, int widthInflation, int heightInflation, RectangleI expected) => Assert.That (a.Inflated (widthInflation, heightInflation), Is.EqualTo (expected)); + [TestCaseSource (nameof (vertical_slicing_cases))] + public void CorrectVerticalSlicing (RectangleI original, IReadOnlyList expectedSlices) + { + var actualSlices = original.ToRows ().ToArray (); + Assert.That (actualSlices.Length, Is.EqualTo (expectedSlices.Count)); + for (int i = 0; i < expectedSlices.Count; i++) + Assert.That (actualSlices[i], Is.EqualTo (expectedSlices[i])); + } + + private static readonly IReadOnlyList vertical_slicing_cases = CreateVerticalSlicingCases ().ToArray (); + private static IEnumerable CreateVerticalSlicingCases () + { + yield return new ( + new RectangleI (1, 1, 5, 5), + new[] { + new RectangleI(1,1,5,1), + new RectangleI(1,2,5,1), + new RectangleI(1,3,5,1), + new RectangleI(1,4,5,1), + new RectangleI(1,5,5,1), + }); + } + private static readonly IReadOnlyList inflation_cases = CreateInflationCases ().ToArray (); private static IEnumerable CreateInflationCases () {