// // AsyncEffectRenderer.cs // // Author: // Greg Lowe // // Copyright (c) 2010 Greg Lowe // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #if (!LIVE_PREVIEW_DEBUG && DEBUG) #undef DEBUG #endif using System; using System.Collections.Generic; using System.Threading; using Debug = System.Diagnostics.Debug; namespace Pinta.Core { // Only call methods on this class from a single thread (The UI thread). internal abstract class AsyncEffectRenderer { Settings settings; internal struct Settings { internal int ThreadCount { get; set; } internal int TileWidth { get; set; } internal int TileHeight { get; set; } internal int UpdateMillis { get; set; } internal ThreadPriority ThreadPriority { get; set; } } BaseEffect effect; Cairo.ImageSurface source_surface; Cairo.ImageSurface dest_surface; Gdk.Rectangle render_bounds; bool is_rendering; bool cancel_render_flag; bool restart_render_flag; int render_id; int current_tile; int total_tiles; List render_exceptions; uint timer_tick_id; object updated_lock; bool is_updated; int updated_x1; int updated_y1; int updated_x2; int updated_y2; internal AsyncEffectRenderer (Settings settings) { if (settings.ThreadCount < 1) settings.ThreadCount = 1; if (settings.TileWidth < 1) throw new ArgumentException ("EffectRenderSettings.TileWidth"); if (settings.TileHeight < 1) throw new ArgumentException ("EffectRenderSettings.TileHeight"); if (settings.UpdateMillis <= 0) settings.UpdateMillis = 100; effect = null; source_surface = null; dest_surface = null; this.settings = settings; is_rendering = false; render_id = 0; updated_lock = new object (); is_updated = false; render_exceptions = new List (); timer_tick_id = 0; } internal bool IsRendering { get { return is_rendering; } } internal double Progress { get { if (total_tiles == 0 || current_tile < 0) return 0; else if (current_tile < total_tiles) return (double)current_tile / (double)total_tiles; else return 1; } } internal void Start (BaseEffect effect, Cairo.ImageSurface source, Cairo.ImageSurface dest, Gdk.Rectangle renderBounds) { Debug.WriteLine ("AsyncEffectRenderer.Start ()"); if (effect == null) throw new ArgumentNullException ("effect"); if (source == null) throw new ArgumentNullException ("source"); if (dest == null) throw new ArgumentNullException ("dest"); if (renderBounds.IsEmpty) throw new ArgumentException ("renderBounds.IsEmpty"); // It is important the effect's properties don't change during rendering. // So a copy is made for the render. this.effect = effect.Clone(); this.source_surface = source; this.dest_surface = dest; this.render_bounds = renderBounds; // If a render is already in progress, then cancel it, // and start a new render. if (IsRendering) { cancel_render_flag = true; restart_render_flag = true; return; } StartRender (); } internal void Cancel () { Debug.WriteLine ("AsyncEffectRenderer.Cancel ()"); cancel_render_flag = true; restart_render_flag = false; if (!IsRendering) HandleRenderCompletion (); } protected abstract void OnUpdate (double progress, Gdk.Rectangle updatedBounds); protected abstract void OnCompletion (bool canceled, Exception[] exceptions); internal void Dispose () { if (timer_tick_id > 0) GLib.Source.Remove (timer_tick_id); } void StartRender () { is_rendering = true; cancel_render_flag = false; restart_render_flag = false; is_updated = false; render_id++; render_exceptions.Clear (); current_tile = -1; total_tiles = CalculateTotalTiles (); Debug.WriteLine ("AsyncEffectRenderer.Start () Render " + render_id + " starting."); // Copy the current render id. int renderId = render_id; // Start slave render threads. int threadCount = settings.ThreadCount; var slaves = new Thread[threadCount - 1]; for (int threadId = 1; threadId < threadCount; threadId++) slaves[threadId - 1] = StartSlaveThread (renderId, threadId); // Start the master render thread. var master = new Thread (() => { // Do part of the rendering on the master thread. Render (renderId, 0); // Wait for slave threads to complete. foreach (var slave in slaves) slave.Join (); // Change back to the UI thread to notify of completion. Gtk.Application.Invoke ((o,e) => HandleRenderCompletion ()); }); master.Priority = settings.ThreadPriority; master.Start (); // Start timer used to periodically fire update events on the UI thread. timer_tick_id = GLib.Timeout.Add((uint) settings.UpdateMillis, HandleTimerTick); } Thread StartSlaveThread (int renderId, int threadId) { var slave = new Thread(() => { Render (renderId, threadId); }); slave.Priority = settings.ThreadPriority; slave.Start (); return slave; } // Runs on a background thread. 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) return; RenderTile (renderId, threadId, tileIndex); } } // Runs on a background thread. void RenderTile (int renderId, int threadId, int tileIndex) { Exception exception = null; Gdk.Rectangle bounds = new Gdk.Rectangle (); try { bounds = GetTileBounds (tileIndex); if (!cancel_render_flag) { dest_surface.Flush (); effect.RenderEffect (source_surface, dest_surface, new [] { bounds }); dest_surface.MarkDirty (bounds.ToCairoRectangle ()); } } catch (Exception ex) { exception = ex; Debug.WriteLine ("AsyncEffectRenderer Error while rendering effect: " + effect.Text + " exception: " + ex.Message + "\n" + ex.StackTrace); } // Ignore completions of tiles after a cancel or from a previous render. if (!IsRendering || renderId != render_id) return; // Update bounds to be shown on next expose. lock (updated_lock) { if (is_updated) { updated_x1 = Math.Min (bounds.X, updated_x1); updated_y1 = Math.Min (bounds.Y, updated_y1); updated_x2 = Math.Max (bounds.X + bounds.Width, updated_x2); updated_y2 = Math.Max (bounds.Y + bounds.Height, updated_y2); } else { is_updated = true; updated_x1 = bounds.X; updated_y1 = bounds.Y; updated_x2 = bounds.X + bounds.Width; updated_y2 = bounds.Y + bounds.Height; } } if (exception != null) { lock (render_exceptions) { render_exceptions.Add (exception); } } } // Runs on a background thread. Gdk.Rectangle GetTileBounds (int tileIndex) { int horizTileCount = (int)Math.Ceiling((float)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 - x); int h = Math.Min(settings.TileHeight, render_bounds.Bottom - y); return new Gdk.Rectangle (x, y, w, h); } int CalculateTotalTiles () { return (int)(Math.Ceiling((float)render_bounds.Width / (float)settings.TileWidth) * Math.Ceiling((float)render_bounds.Height / (float)settings.TileHeight)); } // Called on the UI thread. bool HandleTimerTick () { Debug.WriteLine (DateTime.Now.ToString("HH:mm:ss:ffff") + " Timer tick."); Gdk.Rectangle bounds; lock (updated_lock) { if (!is_updated) return true; is_updated = false; bounds = new Gdk.Rectangle (updated_x1, updated_y1, updated_x2 - updated_x1, updated_y2 - updated_y1); } if (IsRendering && !cancel_render_flag) OnUpdate (Progress, bounds); return true; } void HandleRenderCompletion () { var exceptions = (render_exceptions == null || render_exceptions.Count == 0) ? null : render_exceptions.ToArray (); HandleTimerTick (); if (timer_tick_id > 0) GLib.Source.Remove (timer_tick_id); OnCompletion (cancel_render_flag, exceptions); if (restart_render_flag) StartRender (); else is_rendering = false; } } }