/// <summary> /// Async executes a batch of subtasks on node /// </summary> private async Task <SubTaskBatchResult> ExecuteSubTasksAsync(RenderNode node, Action <RenderSubTask, RenderBatchResult> onResult, params RenderSubTask[] tasks) { List <RenderRequest> reqs = tasks.Select(x => x.GetRenderRequest()).ToList(); List <RenderBatchResult> results = new List <RenderBatchResult>(); Action <RenderNode, RenderBatchResult> onAnyResult = (bnode, result) => { RenderSubTask task = tasks.FirstOrDefault(x => x.ID == result.TaskID); if (task != null) { lock (results) results.Add(result); onResult(task, result); } }; Stopwatch time = new Stopwatch(); time.Start(); try { node.OnBatchResult += onAnyResult; RenderBatchRequest req = RenderSubTask.GetRenderBatchRequest(ID, tasks); req.Settings.ForEach(x => x.RenderType = node.RenderType); RenderBatchResponse resp = await node.RenderBatch(req); if (resp == null) { return(new SubTaskBatchResult(new Exception("Render fail: (null)"))); } if (resp.Success == false) { return(new SubTaskBatchResult(new Exception("Render fail: " + resp.Message))); } if (req.Settings.Count > 0) { decimal pixelsRendered = req.Settings.Sum(x => (x.Height * (x.Y2 - x.Y)) * (x.Width * (x.X2 - x.X))); node.UpdatePerformance((int)pixelsRendered, (int)time.ElapsedMilliseconds); } } finally { node.OnBatchResult -= onAnyResult; time.Stop(); } return(new SubTaskBatchResult(results.ToArray())); }
/// <summary> /// Async executes a subtask on node /// </summary> private async Task <SubTaskResult> ExecuteSubTaskAsync(RenderNode node, RenderSubTask task) { RenderRequest req = task.GetRenderRequest(); Image bitmap = null; Stopwatch time = new Stopwatch(); time.Start(); try { req.Settings.RenderType = node.RenderType; RenderResponse resp = await node.Render(req); if (resp == null) { return(new SubTaskResult(new Exception("Render fail: (null)"))); } if (resp.Success == false) { return(new SubTaskResult(new Exception("Render fail: " + resp.Message))); } //Update Performance node.UpdatePerformance((int)((req.Settings.Height * (req.Settings.Y2 - req.Settings.Y)) * (req.Settings.Width * (req.Settings.X2 - req.Settings.X))), (int)time.ElapsedMilliseconds); using (MemoryStream str = new MemoryStream(resp.Data)) bitmap = Bitmap.FromStream(str); resp = null; } finally { time.Stop(); } return(new SubTaskResult(bitmap)); }
/// <summary> /// Handles an incoming tile and trigger the events as well as drawing the tile to an image/graphics /// </summary> private void ProcessTile(RenderSubTask task, Bitmap part, ref Graphics g, ref Bitmap result, ref object drawLock, bool dontDraw = false) { if (Cancelled) { return; } if (!dontDraw) { lock (drawLock) { if (result == null) { result = new Bitmap(part.Width, part.Height); g = Graphics.FromImage(result); } if (task.Crop) { int tileWidth = ((int)((task.X2 - task.X) * task.Parent.Settings.OutputWidth)); int tileHeight = ((int)((task.Y2 - task.Y) * task.Parent.Settings.OutputHeight)); int posX = (int)(task.X * task.Parent.Settings.OutputWidth); int posY = (int)(task.Parent.Settings.OutputHeight - task.Y * task.Parent.Settings.OutputHeight - tileHeight); g.DrawImage(part, posX, posY, tileWidth, tileHeight); } else { g.DrawImage(part, 0, 0, task.Parent.Settings.OutputWidth, task.Parent.Settings.OutputHeight); } } if (OnResultUpdated != null) { OnResultUpdated(task, result); } } if (OnTileProcessed != null) { OnTileProcessed(task, part); } }
/// <summary> /// Blocking executes a subtask on node (calls async underneath) /// </summary> private SubTaskResult ExecuteSubTask(RenderNode node, RenderSubTask task) { return(ExecuteSubTaskAsync(node, task).GetAwaiter().GetResult()); }
/// <summary> /// Renders file with Settings in pre-defined chunksizes. Chunks will be assigned based on Cores and Performance. /// Renders relatively quick but some overhead, will render using a single blender instance. /// Benefits from live update as tiles finish they are send back to the client /// eg. 3 valid nodes of equal performance with chunk size of about 10% /// Each node will get x chunks of 10% fitting in 33% of the image. /// </summary> private async Task <Bitmap> RenderSplitChunked(List <RenderNode> validNodes, Action <RenderSubTask> onSubTaskFinished = null) { object drawLock = new object(); Bitmap result = new Bitmap(Settings.OutputWidth, Settings.OutputHeight); Graphics g = Graphics.FromImage(result); Dictionary <RenderNode, decimal> shares = GetRelativePerformance(validNodes); List <RenderSubTask> tasks = GetChunkedSubTasks(); ConcurrentQueue <RenderSubTask> queue = GetTaskQueueInOrder(tasks, Settings.Order); Dictionary <RenderNode, List <RenderSubTask> > assignment = validNodes.ToDictionary(x => x, y => new List <RenderSubTask>()); //Divide Tasks //Assume every core has the same performance.. while (queue.Count > 0) { RenderSubTask nextTask = null; queue.TryDequeue(out nextTask); //Single threaded, always true RenderNode nextNode = validNodes.OrderBy(node => { int nrTiles = assignment[node].Count; return(nrTiles * (1 / shares[node])); }).FirstOrDefault(); assignment[nextNode].Add(nextTask); } //Run tasks over all rendernodes return(await Task.Run(() => { ForceParallel(validNodes, (node) => { List <RenderSubTask> nodeTasks = assignment[node]; if (nodeTasks.Count == 0) { return; } try { SubTaskBatchResult resp = ExecuteSubTasks(node, (rsbt, rbr) => { Image bitmap = null; using (MemoryStream str = new MemoryStream(rbr.Data)) bitmap = Bitmap.FromStream(str); ProcessTile(rsbt, (Bitmap)bitmap, ref g, ref result, ref drawLock); onSubTaskFinished?.Invoke(rsbt); }, assignment[node].ToArray()); if (resp?.Exception != null) { node.UpdateException(resp.Exception.Message); } } catch (TaskCanceledException ex) { node.UpdateException("Cancalled"); return; } catch (AggregateException ex) { node.UpdateException(string.Join(", ", ex.InnerExceptions.Select(x => x.Message))); } catch (Exception ex) { node.UpdateException(ex.Message); } }); if (g != null) { g.Dispose(); } return result; })); }
/// <summary> /// Renders file with Settings in pre-defined chunksizes. Each chunk will be rendered independently without batching /// Very slow due to blender being initialized for every chunk. But tasks are consumed optimally /// Benefits from live update as tiles finish they are send back to client /// </summary> private async Task <Bitmap> RenderChunked(List <RenderNode> validNodes, Action <RenderSubTask> onSubTaskFinished = null) { object drawLock = new object(); Bitmap result = new Bitmap(Settings.OutputWidth, Settings.OutputHeight); Graphics g = Graphics.FromImage(result); List <RenderSubTask> tasks = GetChunkedSubTasks(); ConcurrentQueue <RenderSubTask> queue = GetTaskQueueInOrder(tasks, Settings.Order); List <string> exceptions = new List <string>(); //Force parallelization return(await Task.Run(() => { ForceParallel(validNodes, (node) => { int errorCount = 0; while (queue.Count > 0) { RenderSubTask task = null; if (!queue.TryDequeue(out task)) { continue; } try { SubTaskResult taskResult = ExecuteSubTask(node, task); ProcessTile(task, (Bitmap)taskResult.Image, ref g, ref result, ref drawLock); onSubTaskFinished?.Invoke(task); } catch (Exception ex) { errorCount++; if (task != null) { queue.Enqueue(task); } node.UpdateException($"Render fail [{errorCount+1}/3]: {ex.Message}"); exceptions.Add(ex.Message); if (errorCount > 2) { return; } } } }); if (queue.Count > 0) { throw new AggregateException("Not all tiles rendered", exceptions.Select(x => new Exception(x))); } if (g != null) { g.Dispose(); } return result; })); }
//Strategies /// <summary> /// Renders file with Settings in maximum chunks based on Cores and Performance /// eg. 3 valid nodes of equal performance will render a single part on each node with a 0.33 ratio /// </summary> private async Task <Bitmap> RenderSplit(List <RenderNode> validNodes, Action <RenderSubTask> onSubTaskFinished = null, bool isVertical = false) { object drawLock = new object(); Bitmap result = new Bitmap(Settings.OutputWidth, Settings.OutputHeight); Graphics g = Graphics.FromImage(result); Dictionary <RenderNode, RenderSubTask> assignment = GetSplitSubTasks(validNodes, isVertical); int finished = 0; List <string> exceptions = new List <string>(); return(await Task.Run(() => { ForceParallel(validNodes, (node) => { RenderSubTask task = assignment[node]; if (task == null) { return; } string lastException = null; bool rendered = false; for (int i = 0; i < 3; i++) { if (Cancelled) { return; } SubTaskResult taskPart = null; try { taskPart = ExecuteSubTask(node, task); } catch (TaskCanceledException ex) { if (Cancelled) { return; } } catch (Exception ex) { node.UpdateException($"[{i + 1}/3] " + ex.Message); lastException = ex.Message; Thread.Sleep(1000); continue; } if (taskPart.Exception != null) { node.UpdateException($"[{i+1}/3] " + taskPart.Exception.Message); lastException = taskPart.Exception.Message; Thread.Sleep(1000); continue; } Image part = taskPart.Image; ProcessTile(task, (Bitmap)part, ref g, ref result, ref drawLock); onSubTaskFinished?.Invoke(task); finished++; rendered = true; return; } if (!rendered && lastException != null) { exceptions.Add(lastException); } }); if (finished != validNodes.Count) { throw new AggregateException("Not all tiles rendered", exceptions.Select(x => new Exception(x))); } return result; })); }
public async Task <bool> RenderAnimation(int start, int end) { if (Consumed) { throw new InvalidOperationException("Already started render.."); } Consumed = true; try { List <RenderNode> pool = Nodes.Where(x => x.Connected).ToList(); List <RenderNode> validNodes = new List <RenderNode>(); await Task.WhenAll(pool.Select(async x => { bool hasVersion = await x.CheckVersion(Version); if (!hasVersion) { return; } bool hasFile = await x.CheckSyncFile(SessionID, FileID); if (!hasFile) { return; } bool isBusy = await x.IsBusy(); if (isBusy) { return; } lock (validNodes) { validNodes.Add(x); } })); if (validNodes.Count == 0) { throw new InvalidOperationException("No ready nodes available"); } _usedNodes = validNodes; foreach (RenderNode useNode in _usedNodes) { useNode.UpdateException(""); } int framesFinished = 0; int framesTotal = end - start; Action <RenderSubTask> onSubTaskFinished = (task) => { framesFinished++; Progress = (double)framesFinished / framesTotal; TriggerPropUpdate(nameof(Progress)); OnProgress?.Invoke(this, Progress); }; Progress = 0; //StartRenderSplit object drawLock = new object(); Bitmap result = new Bitmap(Settings.OutputWidth, Settings.OutputHeight); Graphics g = Graphics.FromImage(result); ConcurrentQueue <RenderSubTask> queue = new ConcurrentQueue <RenderSubTask>(); for (int i = start; i <= end; i++) { queue.Enqueue(new RenderSubTask(this, 0, 1, 0, 1, i)); } int finished = 0; List <string> exceptions = new List <string>(); return(await Task.Run(() => { ForceParallel(validNodes, (node) => { string lastException = null; while (queue.Count > 0) { RenderSubTask task = null; if (!queue.TryDequeue(out task)) { continue; } SubTaskResult taskPart = null; try { taskPart = ExecuteSubTask(node, task); if (taskPart.Image == null) { throw new Exception(taskPart.Exception?.Message ?? "Unknown Remote Exception"); } ProcessTile(task, (Bitmap)taskPart.Image, ref g, ref result, ref drawLock); onSubTaskFinished?.Invoke(task); finished++; } catch (TaskCanceledException ex) { if (Cancelled) { return; } } catch (Exception ex) { if (task != null) { queue.Enqueue(task); } node.UpdateException($"Render fail: {ex.Message}"); exceptions.Add(ex.Message); return; } if (taskPart.Exception != null) { node.UpdateException(taskPart.Exception.Message); lastException = taskPart.Exception.Message; Thread.Sleep(1000); continue; } } }); if (finished != (end - start) + 1) { throw new AggregateException($"Not all frames rendered ({finished}/{(end-start) + 1})", exceptions.Select(x => new Exception(x))); } return true; })); } catch (Exception ex) { throw; } finally { //Consumed = false; } }