Skip to content

Commit

Permalink
Created method for slicing ROI into tiles, for use inside `AsyncEffec…
Browse files Browse the repository at this point in the history
…tRenderer` (#983)
  • Loading branch information
Lehonti committed Sep 21, 2024
1 parent e827f2a commit 2728dc4
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 73 deletions.
91 changes: 35 additions & 56 deletions Pinta.Core/Classes/AsyncEffectRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;
using Debug = System.Diagnostics.Debug;

Expand All @@ -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;
Expand All @@ -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<RectangleI> target_tiles;
readonly List<Exception> render_exceptions;

uint timer_tick_id;
Expand All @@ -101,6 +101,10 @@ internal AsyncEffectRenderer (Settings settings)
render_exceptions = new List<Exception> ();

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;
}
Expand All @@ -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;
}
Expand All @@ -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 ()");

Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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);
Expand All @@ -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
Expand All @@ -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);
}
Expand All @@ -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) {
Expand All @@ -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.
Expand Down Expand Up @@ -328,9 +306,10 @@ bool HandleTimerTick ()

void HandleRenderCompletion ()
{
var exceptions = (render_exceptions.Count == 0)
? Array.Empty<Exception> ()
: render_exceptions.ToArray ();
var exceptions =
render_exceptions.Count == 0
? Array.Empty<Exception> ()
: render_exceptions.ToArray ();

HandleTimerTick ();

Expand Down
25 changes: 19 additions & 6 deletions Pinta.Core/Extensions/OtherExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,25 @@ public static ReadOnlyCollection<T> ToReadOnlyCollection<T> (this ImmutableList<

private sealed class ImmutableBackedReadOnlyCollection<T> : ReadOnlyCollection<T>
{
internal ImmutableBackedReadOnlyCollection (ImmutableList<T> list) : base (list)
{
}
internal ImmutableBackedReadOnlyCollection (ImmutableArray<T> array) : base (array)
{
}
internal ImmutableBackedReadOnlyCollection (ImmutableList<T> list)
: base (list)
{ }

internal ImmutableBackedReadOnlyCollection (ImmutableArray<T> array)
: base (array)
{ }
}

public static IEnumerable<RectangleI> 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<T> (this T enumeration, params T[] values)
Expand Down
15 changes: 4 additions & 11 deletions Pinta.Core/Managers/LivePreviewManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<BaseEffect.ConfigDialogResponseEventArgs>? handler = null;
Expand Down Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions tests/Pinta.Core.Tests/RectangleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<RectangleI> 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<TestCaseData> vertical_slicing_cases = CreateVerticalSlicingCases ().ToArray ();
private static IEnumerable<TestCaseData> 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<TestCaseData> inflation_cases = CreateInflationCases ().ToArray ();
private static IEnumerable<TestCaseData> CreateInflationCases ()
{
Expand Down

0 comments on commit 2728dc4

Please sign in to comment.