1 // 2 // AsyncEffectRenderer.cs 3 // 4 // Author: 5 // Greg Lowe <greg@vis.net.nz> 6 // 7 // Copyright (c) 2010 Greg Lowe 8 // 9 // Permission is hereby granted, free of charge, to any person obtaining a copy 10 // of this software and associated documentation files (the "Software"), to deal 11 // in the Software without restriction, including without limitation the rights 12 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 // copies of the Software, and to permit persons to whom the Software is 14 // furnished to do so, subject to the following conditions: 15 // 16 // The above copyright notice and this permission notice shall be included in 17 // all copies or substantial portions of the Software. 18 // 19 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 // THE SOFTWARE. 26 27 #if (!LIVE_PREVIEW_DEBUG && DEBUG) 28 #undef DEBUG 29 #endif 30 31 using System; 32 using System.Collections.Generic; 33 using System.Threading; 34 using Debug = System.Diagnostics.Debug; 35 36 namespace Pinta.Core 37 { 38 39 // Only call methods on this class from a single thread (The UI thread). 40 internal abstract class AsyncEffectRenderer 41 { 42 Settings settings; 43 44 internal struct Settings { 45 internal int ThreadCount { get; set; } 46 internal int TileWidth { get; set; } 47 internal int TileHeight { get; set; } 48 internal int UpdateMillis { get; set; } 49 internal ThreadPriority ThreadPriority { get; set; } 50 } 51 52 BaseEffect effect; 53 Cairo.ImageSurface source_surface; 54 Cairo.ImageSurface dest_surface; 55 Gdk.Rectangle render_bounds; 56 57 bool is_rendering; 58 bool cancel_render_flag; 59 bool restart_render_flag; 60 int render_id; 61 int current_tile; 62 int total_tiles; 63 List<Exception> render_exceptions; 64 65 uint timer_tick_id; 66 67 object updated_lock; 68 bool is_updated; 69 int updated_x1; 70 int updated_y1; 71 int updated_x2; 72 int updated_y2; 73 AsyncEffectRenderer(Settings settings)74 internal AsyncEffectRenderer (Settings settings) 75 { 76 if (settings.ThreadCount < 1) 77 settings.ThreadCount = 1; 78 79 if (settings.TileWidth < 1) 80 throw new ArgumentException ("EffectRenderSettings.TileWidth"); 81 82 if (settings.TileHeight < 1) 83 throw new ArgumentException ("EffectRenderSettings.TileHeight"); 84 85 if (settings.UpdateMillis <= 0) 86 settings.UpdateMillis = 100; 87 88 effect = null; 89 source_surface = null; 90 dest_surface = null; 91 this.settings = settings; 92 93 is_rendering = false; 94 render_id = 0; 95 updated_lock = new object (); 96 is_updated = false; 97 render_exceptions = new List<Exception> (); 98 99 timer_tick_id = 0; 100 } 101 102 internal bool IsRendering { 103 get { return is_rendering; } 104 } 105 106 internal double Progress { 107 get { 108 if (total_tiles == 0 || current_tile < 0) 109 return 0; 110 else if (current_tile < total_tiles) 111 return (double)current_tile / (double)total_tiles; 112 else 113 return 1; 114 } 115 } 116 Start(BaseEffect effect, Cairo.ImageSurface source, Cairo.ImageSurface dest, Gdk.Rectangle renderBounds)117 internal void Start (BaseEffect effect, 118 Cairo.ImageSurface source, 119 Cairo.ImageSurface dest, 120 Gdk.Rectangle renderBounds) 121 { 122 Debug.WriteLine ("AsyncEffectRenderer.Start ()"); 123 124 if (effect == null) 125 throw new ArgumentNullException ("effect"); 126 127 if (source == null) 128 throw new ArgumentNullException ("source"); 129 130 if (dest == null) 131 throw new ArgumentNullException ("dest"); 132 133 if (renderBounds.IsEmpty) 134 throw new ArgumentException ("renderBounds.IsEmpty"); 135 136 // It is important the effect's properties don't change during rendering. 137 // So a copy is made for the render. 138 this.effect = effect.Clone(); 139 140 this.source_surface = source; 141 this.dest_surface = dest; 142 this.render_bounds = renderBounds; 143 144 // If a render is already in progress, then cancel it, 145 // and start a new render. 146 if (IsRendering) { 147 cancel_render_flag = true; 148 restart_render_flag = true; 149 return; 150 } 151 152 StartRender (); 153 } 154 Cancel()155 internal void Cancel () 156 { 157 Debug.WriteLine ("AsyncEffectRenderer.Cancel ()"); 158 cancel_render_flag = true; 159 restart_render_flag = false; 160 161 if (!IsRendering) 162 HandleRenderCompletion (); 163 } 164 OnUpdate(double progress, Gdk.Rectangle updatedBounds)165 protected abstract void OnUpdate (double progress, Gdk.Rectangle updatedBounds); 166 OnCompletion(bool canceled, Exception[] exceptions)167 protected abstract void OnCompletion (bool canceled, Exception[] exceptions); 168 Dispose()169 internal void Dispose () 170 { 171 if (timer_tick_id > 0) 172 GLib.Source.Remove (timer_tick_id); 173 } 174 StartRender()175 void StartRender () 176 { 177 is_rendering = true; 178 cancel_render_flag = false; 179 restart_render_flag = false; 180 is_updated = false; 181 182 render_id++; 183 render_exceptions.Clear (); 184 185 current_tile = -1; 186 187 total_tiles = CalculateTotalTiles (); 188 189 Debug.WriteLine ("AsyncEffectRenderer.Start () Render " + render_id + " starting."); 190 191 // Copy the current render id. 192 int renderId = render_id; 193 194 // Start slave render threads. 195 int threadCount = settings.ThreadCount; 196 var slaves = new Thread[threadCount - 1]; 197 for (int threadId = 1; threadId < threadCount; threadId++) 198 slaves[threadId - 1] = StartSlaveThread (renderId, threadId); 199 200 // Start the master render thread. 201 var master = new Thread (() => { 202 203 // Do part of the rendering on the master thread. 204 Render (renderId, 0); 205 206 // Wait for slave threads to complete. 207 foreach (var slave in slaves) 208 slave.Join (); 209 210 // Change back to the UI thread to notify of completion. 211 Gtk.Application.Invoke ((o,e) => HandleRenderCompletion ()); 212 }); 213 214 master.Priority = settings.ThreadPriority; 215 master.Start (); 216 217 // Start timer used to periodically fire update events on the UI thread. 218 timer_tick_id = GLib.Timeout.Add((uint) settings.UpdateMillis, HandleTimerTick); 219 } 220 StartSlaveThread(int renderId, int threadId)221 Thread StartSlaveThread (int renderId, int threadId) 222 { 223 var slave = new Thread(() => { 224 Render (renderId, threadId); 225 }); 226 227 slave.Priority = settings.ThreadPriority; 228 slave.Start (); 229 230 return slave; 231 } 232 233 // Runs on a background thread. Render(int renderId, int threadId)234 void Render (int renderId, int threadId) 235 { 236 // Fetch the next tile index and render it. 237 for (;;) { 238 239 int tileIndex = Interlocked.Increment (ref current_tile); 240 241 if (tileIndex >= total_tiles || cancel_render_flag) 242 return; 243 244 RenderTile (renderId, threadId, tileIndex); 245 } 246 } 247 248 // Runs on a background thread. RenderTile(int renderId, int threadId, int tileIndex)249 void RenderTile (int renderId, int threadId, int tileIndex) 250 { 251 Exception exception = null; 252 Gdk.Rectangle bounds = new Gdk.Rectangle (); 253 254 try { 255 256 bounds = GetTileBounds (tileIndex); 257 258 if (!cancel_render_flag) { 259 dest_surface.Flush (); 260 effect.Render (source_surface, dest_surface, new [] { bounds }); 261 dest_surface.MarkDirty (bounds.ToCairoRectangle ()); 262 } 263 264 } catch (Exception ex) { 265 exception = ex; 266 Debug.WriteLine ("AsyncEffectRenderer Error while rendering effect: " + effect.Name + " exception: " + ex.Message + "\n" + ex.StackTrace); 267 } 268 269 // Ignore completions of tiles after a cancel or from a previous render. 270 if (!IsRendering || renderId != render_id) 271 return; 272 273 // Update bounds to be shown on next expose. 274 lock (updated_lock) { 275 if (is_updated) { 276 updated_x1 = Math.Min (bounds.X, updated_x1); 277 updated_y1 = Math.Min (bounds.Y, updated_y1); 278 updated_x2 = Math.Max (bounds.X + bounds.Width, updated_x2); 279 updated_y2 = Math.Max (bounds.Y + bounds.Height, updated_y2); 280 } else { 281 is_updated = true; 282 updated_x1 = bounds.X; 283 updated_y1 = bounds.Y; 284 updated_x2 = bounds.X + bounds.Width; 285 updated_y2 = bounds.Y + bounds.Height; 286 } 287 } 288 289 if (exception != null) { 290 lock (render_exceptions) { 291 render_exceptions.Add (exception); 292 } 293 } 294 } 295 296 // Runs on a background thread. GetTileBounds(int tileIndex)297 Gdk.Rectangle GetTileBounds (int tileIndex) 298 { 299 int horizTileCount = (int)Math.Ceiling((float)render_bounds.Width 300 / (float)settings.TileWidth); 301 302 int x = ((tileIndex % horizTileCount) * settings.TileWidth) + render_bounds.X; 303 int y = ((tileIndex / horizTileCount) * settings.TileHeight) + render_bounds.Y; 304 int w = Math.Min(settings.TileWidth, render_bounds.GetRight () + 1 - x); 305 int h = Math.Min(settings.TileHeight, render_bounds.GetBottom () + 1 - y); 306 307 return new Gdk.Rectangle (x, y, w, h); 308 } 309 CalculateTotalTiles()310 int CalculateTotalTiles () 311 { 312 return (int)(Math.Ceiling((float)render_bounds.Width / (float)settings.TileWidth) 313 * Math.Ceiling((float)render_bounds.Height / (float)settings.TileHeight)); 314 } 315 316 // Called on the UI thread. HandleTimerTick()317 bool HandleTimerTick () 318 { 319 Debug.WriteLine (DateTime.Now.ToString("HH:mm:ss:ffff") + " Timer tick."); 320 321 Gdk.Rectangle bounds; 322 323 lock (updated_lock) { 324 325 if (!is_updated) 326 return true; 327 328 is_updated = false; 329 330 bounds = new Gdk.Rectangle (updated_x1, 331 updated_y1, 332 updated_x2 - updated_x1, 333 updated_y2 - updated_y1); 334 } 335 336 if (IsRendering && !cancel_render_flag) 337 OnUpdate (Progress, bounds); 338 339 return true; 340 } 341 HandleRenderCompletion()342 void HandleRenderCompletion () 343 { 344 var exceptions = (render_exceptions == null || render_exceptions.Count == 0) 345 ? null 346 : render_exceptions.ToArray (); 347 348 HandleTimerTick (); 349 350 if (timer_tick_id > 0) 351 GLib.Source.Remove (timer_tick_id); 352 353 OnCompletion (cancel_render_flag, exceptions); 354 355 if (restart_render_flag) 356 StartRender (); 357 else 358 is_rendering = false; 359 } 360 } 361 } 362