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