#include "FractoriumPch.h" #include "FractoriumEmberController.h" #include "FinalRenderEmberController.h" #include "FinalRenderDialog.h" #include "Fractorium.h" /// <summary> /// Constructor which accepts a pointer to the final render dialog. /// It passes a pointer to the main window to the base and initializes members. /// </summary> /// <param name="finalRender">Pointer to the final render dialog</param> FinalRenderEmberControllerBase::FinalRenderEmberControllerBase(FractoriumFinalRenderDialog* finalRenderDialog) : FractoriumEmberControllerBase(finalRenderDialog->m_Fractorium), m_FinalRenderDialog(finalRenderDialog) { m_FinishedImageCount.store(0); m_Settings = FractoriumSettings::DefInstance(); } template <typename T> FinalRenderEmberController<T>::~FinalRenderEmberController() { m_ThreadedWriter.JoinAll(); } /// <summary> /// Cancel the render by calling Abort(). /// This will block until the cancelling is actually finished. /// It should never take longer than a few milliseconds because the /// renderer checks the m_Abort flag in many places during the process. /// </summary> template <typename T> void FinalRenderEmberController<T>::CancelRender() { if (m_Result.isRunning()) { std::thread th([&] { m_Run = false; if (m_Renderer.get()) { m_Renderer->Reset(); } else { for (auto& renderer : m_Renderers) { renderer->Abort(); while (renderer->InRender()) QApplication::processEvents(); renderer->EnterRender(); renderer->EnterFinalAccum(); renderer->LeaveFinalAccum(); renderer->LeaveRender(); } } }); Join(th); while (m_Result.isRunning()) QApplication::processEvents(); m_FinalRenderDialog->ui.FinalRenderTextOutput->append("Render canceled."); } } /// <summary> /// Create a new renderer based on the options selected on the GUI. /// If a renderer matching the options has already been created, no action is taken. /// </summary> /// <returns>True if a valid renderer is created or if no action is taken, else false.</returns> bool FinalRenderEmberControllerBase::CreateRendererFromGUI() { const auto useOpenCL = m_Info->Ok() && m_FinalRenderDialog->OpenCL(); const auto v = Devices(m_FinalRenderDialog->Devices()); return CreateRenderer((useOpenCL && !v.empty()) ? eRendererType::OPENCL_RENDERER : eRendererType::CPU_RENDERER, v, false, false); //Not shared. } /// <summary> /// Thin wrapper around invoking a call to append text to the output. /// </summary> /// <param name="s">The string to append</param> void FinalRenderEmberControllerBase::Output(const QString& s) { QMetaObject::invokeMethod(m_FinalRenderDialog->ui.FinalRenderTextOutput, "append", Qt::QueuedConnection, Q_ARG(const QString&, s)); } /// <summary> /// Render a single ember. /// </summary> /// <param name="ember">The ember to render</param> /// <param name="fullRender">Is this is a FULL_RENDER or if we should KEEP_ITERATING.</param> /// <param name="stripForProgress">Used to report progress when strips.</param> /// <returns>True if rendering succeeded.</returns> template<typename T> bool FinalRenderEmberController<T>::RenderSingleEmber(Ember<T>& ember, bool fullRender, size_t& stripForProgress) { if (!m_Renderer.get()) return false; auto threadIndex = fullRender ? m_ThreadedWriter.Increment() : m_ThreadedWriter.Current(); auto threadImage = m_ThreadedWriter.GetImage(threadIndex); ember.m_TemporalSamples = 1;//No temporal sampling. m_Renderer->SetEmber(ember, fullRender ? eProcessAction::FULL_RENDER : eProcessAction::KEEP_ITERATING, /* updatePointer */ true); m_Renderer->PrepFinalAccumVector(*threadImage);//Must manually call this first because it could be erroneously made smaller due to strips if called inside Renderer::Run(). m_Stats.Clear(); m_RenderTimer.Tic();//Toc() is called in RenderComplete(). StripsRender<T>(m_Renderer.get(), ember, *threadImage, 0, m_GuiState.m_Strips, m_GuiState.m_YAxisUp, [&](size_t strip) { stripForProgress = strip; },//Pre strip. [&](size_t strip) { m_Stats += m_Renderer->Stats(); },//Post strip. [&](size_t strip)//Error. { Output("Rendering failed.\n"); m_Fractorium->ErrorReportToQTextEdit(m_Renderer->ErrorReport(), m_FinalRenderDialog->ui.FinalRenderTextOutput, false);//Internally calls invoke. m_Run = false; }, [&](Ember<T>& finalEmber) { m_FinishedImageCount.fetch_add(1); auto stats = m_Renderer->Stats(); auto comments = m_Renderer->ImageComments(stats, 0, true); auto rasw = m_Renderer->FinalRasW(); auto rash = m_Renderer->FinalRasH(); auto png16 = m_FinalRenderDialog->Png16Bit(); auto transparency = m_FinalRenderDialog->Transparency(); RenderComplete(finalEmber); HandleFinishedProgress(); auto writeThread = std::thread([ = ](Ember<T> threadEmber)//Pass ember by value. { if (SaveCurrentRender(threadEmber, comments, *threadImage, rasw, rash, png16, transparency) == "") m_Run = false; }, finalEmber); m_ThreadedWriter.SetThread(threadIndex, writeThread); });//Final strip. return m_Run; } /// <summary> /// Render a single ember from a series of embers. /// m_Renderers.SetExternalEmbersPointer should already be set. /// </summary> /// <param name="atomfTime">Used to coordinate which frame to render.</param> /// <param name="index">which index into m_Renderers to use.</param> /// <returns>True if rendering succeeded.</returns> template<typename T> bool FinalRenderEmberController<T>::RenderSingleEmberFromSeries(std::atomic<size_t>* atomfTime, size_t index) { if (m_Renderers.size() <= index) return false; const auto renderer = m_Renderers[index].get(); if (renderer == nullptr) return false; size_t ftime; Timing renderTimer; ThreadedWriter localThreadedWriter(16);//Use a local one for each renderer in a sequence instead of the class member. //Render each image, cancelling if m_Run ever gets set to false. //The conditions of this loop use atomics to synchronize when running on multiple GPUs. //The order is reversed from the usual loop: rather than compare and increment the counter, //it's incremented, then compared. This is done to ensure the GPU on this thread "claims" this //frame before working on it. //The mechanism for incrementing is: // Do an atomic add, which returns the previous value. // Assign the result to the ftime counter. // Do a < comparison to m_EmberFile.Size() and check m_Run. while (((ftime = (atomfTime->fetch_add(1))) < m_EmberFile.Size()) && m_Run)//Needed to set 1 to claim this iter from other threads, so decrement it below to be zero-indexed here. { Output("Image " + ToString(ftime + 1ULL) + ":\n" + ComposePath(QString::fromStdString(m_EmberFile.Get(ftime)->m_Name))); renderer->Reset();//Have to manually set this since the ember is not set each time through. renderTimer.Tic();//Toc() is called in RenderComplete(). auto threadIndex = localThreadedWriter.Increment(); auto threadImage = localThreadedWriter.GetImage(threadIndex); //renderer->PrepFinalAccumVector(threadImage); //Can't use strips render here. Run() must be called directly for animation. if (renderer->Run(*threadImage, T(ftime)) != eRenderStatus::RENDER_OK) { Output("Rendering failed.\n"); m_Fractorium->ErrorReportToQTextEdit(renderer->ErrorReport(), m_FinalRenderDialog->ui.FinalRenderTextOutput, false);//Internally calls invoke. atomfTime->store(m_EmberFile.Size() + 1);//Abort all threads if any of them encounter an error. m_Run = false; break; } else { auto stats = renderer->Stats(); auto comments = renderer->ImageComments(stats, 0, true); auto w = renderer->FinalRasW(); auto h = renderer->FinalRasH(); auto ember = m_EmberFile.Get(ftime); m_FinishedImageCount.fetch_add(1); RenderComplete(*ember, stats, renderTimer); if (!index)//Only first device has a progress callback, so it also makes sense to only manually set the progress on the first device as well. HandleFinishedProgress(); auto writeThread = std::thread([ = ]() { if (SaveCurrentRender(*ember, comments,//These all don't change during the renders, so it's ok to access them in the thread. *threadImage, w, h, m_FinalRenderDialog->Png16Bit(), m_FinalRenderDialog->Transparency()) == "") m_Run = false; }); localThreadedWriter.SetThread(threadIndex, writeThread); } } localThreadedWriter.JoinAll();//One final check to make sure all writing is done before exiting this thread. return m_Run; } /// <summary> /// Constructor which accepts a pointer to the final render dialog and passes it to the base. /// The main final rendering lambda function is constructed here. /// </summary> /// <param name="finalRender">Pointer to the final render dialog</param> template<typename T> FinalRenderEmberController<T>::FinalRenderEmberController(FractoriumFinalRenderDialog* finalRender) : FinalRenderEmberControllerBase(finalRender), m_ThreadedWriter(16) { m_FinalPreviewRenderer = make_unique<FinalRenderPreviewRenderer<T>>(this); //The main rendering function which will be called in a Qt thread. //A backup Xml is made before the rendering process starts just in case it crashes before finishing. //If it finishes successfully, delete the backup file. m_FinalRenderFunc = [&]() { m_Run = true; m_TotalTimer.Tic();//Begin timing for progress of all operations. m_GuiState = m_FinalRenderDialog->State();//Cache render settings from the GUI before running. size_t i = 0; const auto doAll = m_GuiState.m_DoAll && m_EmberFile.Size() > 1; const auto isBump = !doAll && m_IsQualityBump && m_GuiState.m_Strips == 1;//Should never get called with m_IsQualityBump otherwise, but check one last time to be safe. size_t currentStripForProgress = 0;//Sort of a hack to get the strip value to the progress function. const auto path = doAll ? ComposePath(QString::fromStdString(m_EmberFile.m_Embers.begin()->m_Name)) : ComposePath(Name()); const auto backup = path + "_backup.flame"; m_FinishedImageCount.store(0); Pause(false); ResetProgress(); FirstOrDefaultRenderer()->m_ProgressParameter = reinterpret_cast<void*>(¤tStripForProgress);//When animating, only the first (primary) device has a progress parameter. if (!isBump) { //Save backup Xml. if (doAll) m_XmlWriter.Save(backup.toStdString().c_str(), m_EmberFile.m_Embers, 0, true, true, false, true, true); else m_XmlWriter.Save(backup.toStdString().c_str(), *m_Ember, 0, true, true, false, true, true); SyncGuiToRenderer(); m_GuiState.m_Strips = VerifyStrips(m_Ember->m_FinalRasH, m_GuiState.m_Strips, [&](const string& s) { Output(QString::fromStdString(s)); }, //Greater than height. [&](const string& s) { Output(QString::fromStdString(s)); }, //Mod height != 0. [&](const string& s) { Output(QString::fromStdString(s) + "\n"); }); //Final strips value to be set. } //The rendering process is different between doing a single image, and doing multiple. if (doAll) { auto i = m_GuiState.m_StartAt; m_ImageCount = m_EmberFile.Size() - i; ostringstream os; const auto padding = streamsize(std::log10(m_EmberFile.Size())) + 1; os << setfill('0') << setprecision(0) << fixed; //Different action required for rendering as animation or not. if (m_GuiState.m_DoSequence && !m_Renderers.empty()) { Ember<T>* prev = nullptr; vector<Ember<T>> embers; const auto firstEmber = m_EmberFile.m_Embers.begin(); //Need to loop through and set all w, h, q, ts, ss and t vals. for (auto& it : m_EmberFile.m_Embers) { if (!m_Run) break; SyncGuiToEmber(it, firstEmber->m_FinalRasW, firstEmber->m_FinalRasH); if (prev == nullptr)//First. { it.m_Time = 0; } else if (it.m_Time <= prev->m_Time) { it.m_Time = prev->m_Time + 1; } it.m_TemporalSamples = m_GuiState.m_TemporalSamples; prev = ⁢ } //Not supporting strips with animation. //Shouldn't be a problem because animations will be at max 4k x 2k which will take about 1GB //even when using double precision, which most cards at the time of this writing already exceed. m_GuiState.m_Strips = 1; CopyCont(embers, m_EmberFile.m_Embers); if (m_GuiState.m_UseNumbers) { auto i = 0; for (auto& it : embers) { it.m_Time = i++; FormatName(it, os, padding); } } std::atomic<size_t> atomfTime(m_GuiState.m_StartAt); vector<std::thread> threadVec; threadVec.reserve(m_Renderers.size()); for (size_t r = 0; r < m_Renderers.size(); r++) { //All will share a pointer to the original vector to conserve memory with large files. Ok because the vec doesn't get modified. m_Renderers[r]->SetExternalEmbersPointer(&embers); threadVec.push_back(std::thread(&FinalRenderEmberController<T>::RenderSingleEmberFromSeries, this, &atomfTime, r)); } Join(threadVec); HandleFinishedProgress();//One final check that all images were finished. } else if (m_Renderer.get())//Make sure a renderer was created and render all images, but not as an animation sequence (without temporal samples motion blur). { //Render each image, cancelling if m_Run ever gets set to false. while (auto ember = m_EmberFile.Get(i)) { if (!m_Run) break; std::string oldname; if (m_GuiState.m_UseNumbers) { oldname = ember->m_Name; ember->m_Time = i; FormatName(*ember, os, padding); } Output("Image " + ToString<qulonglong>(m_FinishedImageCount.load() + 1) + ":\n" + ComposePath(QString::fromStdString(ember->m_Name))); RenderSingleEmber(*ember, true, currentStripForProgress); i++; if (m_GuiState.m_UseNumbers) ember->m_Name = oldname; } } else { Output("No renderer present, aborting."); } } else if (m_Renderer.get())//Render a single image. { Output(ComposePath(QString::fromStdString(m_Ember->m_Name))); m_ImageCount = 1; m_Ember->m_TemporalSamples = 1; m_Fractorium->m_Controller->ParamsToEmber(*m_Ember, true);//Update color and filter params from the main window controls, which only affect the filter and/or final accumulation stage. RenderSingleEmber(*m_Ember, !isBump, currentStripForProgress); } else { Output("No renderer present, aborting."); } m_ThreadedWriter.JoinAll(); const QString totalTimeString = m_Run ? "All renders completed in: " + QString::fromStdString(m_TotalTimer.Format(m_TotalTimer.Toc())) + "." : "Render aborted."; Output(totalTimeString); QFile::remove(backup); QMetaObject::invokeMethod(m_FinalRenderDialog, "Pause", Qt::QueuedConnection, Q_ARG(bool, false)); QMetaObject::invokeMethod(m_FinalRenderDialog->ui.FinalRenderSaveAgainAsButton, "setEnabled", Qt::QueuedConnection, Q_ARG(bool, !doAll && m_Renderer.get()));//Can do save again with variable number of strips. QMetaObject::invokeMethod(m_FinalRenderDialog->ui.FinalRenderBumpQualityStartButton, "setEnabled", Qt::QueuedConnection, Q_ARG(bool, !doAll && m_Renderer.get() && m_GuiState.m_Strips == 1)); m_Run = false; }; } /// <summary> /// Virtual functions overridden from FractoriumEmberControllerBase. /// </summary> /// <summary> /// Setters for embers and ember files which convert between float and double types. /// These are used to preserve the current ember/file when switching between renderers. /// Note that some precision will be lost when going from double to float. /// </summary> template <typename T> void FinalRenderEmberController<T>::SetEmberFile(const EmberFile<float>& emberFile, bool move) { move ? m_EmberFile = std::move(emberFile) : m_EmberFile = emberFile; m_Ember = m_EmberFile.Get(0); } template <typename T> void FinalRenderEmberController<T>::CopyEmberFile(EmberFile<float>& emberFile, bool sequence, std::function<void(Ember<float>& ember)> perEmberOperation) { emberFile.m_Filename = m_EmberFile.m_Filename; CopyCont(emberFile.m_Embers, m_EmberFile.m_Embers, perEmberOperation); } #ifdef DO_DOUBLE template <typename T> void FinalRenderEmberController<T>::SetEmberFile(const EmberFile<double>& emberFile, bool move) { move ? m_EmberFile = std::move(emberFile) : m_EmberFile = emberFile; m_Ember = m_EmberFile.Get(0); } template <typename T> void FinalRenderEmberController<T>::CopyEmberFile(EmberFile<double>& emberFile, bool sequence, std::function<void(Ember<double>& ember)> perEmberOperation) { emberFile.m_Filename = m_EmberFile.m_Filename; CopyCont(emberFile.m_Embers, m_EmberFile.m_Embers, perEmberOperation); } #endif /// <summary> /// Set the ember at the specified index from the currently opened file as the current Ember. /// Clears the undo state. /// Resets the rendering process. /// </summary> /// <param name="index">The index in the file from which to retrieve the ember</param> /// <param name="verbatim">Unused</param> template <typename T> void FinalRenderEmberController<T>::SetEmber(size_t index, bool verbatim) { if (index < m_EmberFile.Size()) { m_Ember = m_EmberFile.Get(index); SyncCurrentToGui(); } else if (m_EmberFile.Size() > 1) { m_Ember = m_EmberFile.Get(0);//Should never happen. } } /// <summary> /// Save current ember as Xml using the filename specified. /// </summary> /// <param name="filename">The filename to save the ember to.</param> template <typename T> void FinalRenderEmberController<T>::SaveCurrentAsXml(QString filename) { const auto ember = m_Ember; EmberToXml<T> writer; const QFileInfo fileInfo(filename); if (!writer.Save(filename.toStdString().c_str(), *ember, 0, true, true, false, true, true)) m_Fractorium->ShowCritical("Save Failed", "Could not save file, try saving to a different folder."); } /// <summary> /// Start the final rendering process. /// Create the needed renderer from the GUI if it has not been created yet. /// </summary> /// <returns></returns> template<typename T> bool FinalRenderEmberController<T>::Render() { const auto filename = m_FinalRenderDialog->Path(); if (filename == "") { m_Fractorium->ShowCritical("File Error", "Please enter a valid path and filename for the output."); return false; } m_IsQualityBump = false; if (CreateRendererFromGUI()) { m_FinalRenderDialog->ui.FinalRenderTextOutput->setText("Preparing all parameters.\n"); //Note that a Qt thread must be used, rather than a tbb task. //This is because tbb does a very poor job of allocating thread resources //and dedicates an entire core just to this thread which does nothing waiting for the //parallel iteration loops inside of the CPU renderer to finish. The result is that //the renderer ends up using ThreadCount - 1 to iterate, instead of ThreadCount. //By using a Qt thread here, and tbb inside the renderer, all cores can be maxed out. m_Result = QtConcurrent::run(m_FinalRenderFunc); m_Settings->sync(); return true; } else return false; } /// <summary> /// Increase the quality of the last render and start rendering again. /// Note this is only when rendering a single image with no strips. /// </summary> /// <param name="d">The amount to increase the quality by, expressed as a decimal percentage. Eg: 0.5 means to increase by 50%.</param> /// <returns>True if nothing went wrong, else false.</returns> template <typename T> bool FinalRenderEmberController<T>::BumpQualityRender(double d) { m_Ember->m_Quality += std::ceil(m_Ember->m_Quality * d); m_Renderer->SetEmber(*m_Ember, eProcessAction::KEEP_ITERATING, true); QString filename = m_FinalRenderDialog->Path(); if (filename == "") { m_Fractorium->ShowCritical("File Error", "Please enter a valid path and filename for the output."); return false; } m_IsQualityBump = true; const auto iterCount = m_Renderer->TotalIterCount(1); m_FinalRenderDialog->ui.FinalRenderParamsTable->item(m_FinalRenderDialog->m_ItersCellIndex, 1)->setText(ToString<qulonglong>(iterCount)); m_FinalRenderDialog->ui.FinalRenderTextOutput->setText("Preparing all parameters.\n"); m_Result = QtConcurrent::run(m_FinalRenderFunc); m_Settings->sync(); return true; } /// <summary> /// Stop rendering and initialize a new renderer, using the specified type and the options on the final render dialog. /// </summary> /// <param name="renderType">The type of render to create</param> /// <param name="devices">The platform,device index pairs of the devices to use</param> /// <param name="updatePreviews">Unused</param> /// <param name="shared">True if shared with OpenGL, else false. Always false in this case.</param> /// <returns>True if nothing went wrong, else false.</returns> template <typename T> bool FinalRenderEmberController<T>::CreateRenderer(eRendererType renderType, const vector<pair<size_t, size_t>>& devices, bool updatePreviews, bool shared) { bool ok = true; const auto renderTypeMismatch = (m_Renderer.get() && (m_Renderer->RendererType() != renderType)) || (!m_Renderers.empty() && (m_Renderers[0]->RendererType() != renderType)); CancelRender(); if ((!m_FinalRenderDialog->DoSequence() && (!m_Renderer.get() || !m_Renderer->Ok())) || (m_FinalRenderDialog->DoSequence() && m_Renderers.empty()) || renderTypeMismatch || !Equal(m_Devices, devices)) { EmberReport emberReport; vector<string> errorReport; m_Devices = devices;//Store values for re-creation later on. m_OutputTexID = 0;//Don't care about tex ID when doing final render. if (m_FinalRenderDialog->DoSequence()) { m_Renderer.reset(); m_Renderers = ::CreateRenderers<T>(renderType, m_Devices, shared, m_OutputTexID, emberReport); for (auto& renderer : m_Renderers) if (const auto rendererCL = dynamic_cast<RendererCLBase*>(renderer.get())) rendererCL->OptAffine(true);//Optimize empty affines for final renderers, this is normally false for the interactive renderer. } else { m_Renderers.clear(); m_Renderer = unique_ptr<EmberNs::RendererBase>(::CreateRenderer<T>(renderType, m_Devices, shared, m_OutputTexID, emberReport)); if (const auto rendererCL = dynamic_cast<RendererCLBase*>(m_Renderer.get())) rendererCL->OptAffine(true);//Optimize empty affines for final renderers, this is normally false for the interactive renderer. } errorReport = emberReport.ErrorReport(); if (!errorReport.empty()) { ok = false; m_Fractorium->ShowCritical("Renderer Creation Error", "Could not create requested renderer, fallback CPU renderer created. See info tab for details."); m_Fractorium->ErrorReportToQTextEdit(errorReport, m_Fractorium->ui.InfoRenderingTextEdit); } } return SyncGuiToRenderer() && ok; } /// <summary> /// Progress function. /// Take special action to sync options upon finishing. /// Note this is only called on the primary renderer. /// </summary> /// <param name="ember">The ember currently being rendered</param> /// <param name="foo">An extra dummy parameter, unused.</param> /// <param name="fraction">The progress fraction from 0-100</param> /// <param name="stage">The stage of iteration. 1 is iterating, 2 is density filtering, 2 is final accumulation.</param> /// <param name="etaMs">The estimated milliseconds to completion of the current stage</param> /// <returns>0 if the user has clicked cancel, else 1 to continue rendering.</returns> template <typename T> int FinalRenderEmberController<T>::ProgressFunc(Ember<T>& ember, void* foo, double fraction, int stage, double etaMs) { static int count = 0; const size_t strip = *(reinterpret_cast<size_t*>(FirstOrDefaultRenderer()->m_ProgressParameter)); const double fracPerStrip = std::ceil(100.0 / m_GuiState.m_Strips); const double stripsfrac = std::ceil(fracPerStrip * strip) + std::ceil(fraction / m_GuiState.m_Strips); const int intFract = static_cast<int>(stripsfrac); if (stage == 0) QMetaObject::invokeMethod(m_FinalRenderDialog->ui.FinalRenderIterationProgress, "setValue", Qt::QueuedConnection, Q_ARG(int, intFract)); else if (stage == 1) QMetaObject::invokeMethod(m_FinalRenderDialog->ui.FinalRenderFilteringProgress, "setValue", Qt::QueuedConnection, Q_ARG(int, intFract)); else if (stage == 2) QMetaObject::invokeMethod(m_FinalRenderDialog->ui.FinalRenderAccumProgress, "setValue", Qt::QueuedConnection, Q_ARG(int, intFract)); QMetaObject::invokeMethod(m_FinalRenderDialog->ui.FinalRenderImageCountLabel, "setText", Qt::QueuedConnection, Q_ARG(const QString&, ToString<qulonglong>(m_FinishedImageCount.load() + 1) + " / " + ToString<qulonglong>(m_ImageCount) + " Eta: " + QString::fromStdString(m_RenderTimer.Format(etaMs)))); QMetaObject::invokeMethod(m_FinalRenderDialog->ui.FinalRenderTextOutput, "update", Qt::QueuedConnection); return m_Run ? 1 : 0; } /// <summary> /// Virtual functions overridden from FinalRenderEmberControllerBase. /// </summary> /// <summary> /// Copy current ember values to widgets. /// </summary> template <typename T> void FinalRenderEmberController<T>::SyncCurrentToGui() { SyncCurrentToSizeSpinners(true, true); m_FinalRenderDialog->ui.FinalRenderCurrentSpin->setSuffix(" " + Name()); m_FinalRenderDialog->Scale(m_Ember->ScaleType()); m_FinalRenderDialog->m_QualitySpin->SetValueStealth(m_Ember->m_Quality); m_FinalRenderDialog->m_SupersampleSpin->SetValueStealth(m_Ember->m_Supersample); m_FinalRenderDialog->Path(ComposePath(Name())); } /// <summary> /// Copy GUI values to either the current ember, or all embers in the file /// depending on whether Render All is checked. /// </summary> /// <param name="widthOverride">Width override to use instead of scaling the original width</param> /// <param name="heightOverride">Height override to use instead of scaling the original height</param> /// <param name="dowidth">Whether to apply width adjustment to the ember</param> /// <param name="doheight">Whether to apply height adjustment to the ember</param> template <typename T> void FinalRenderEmberController<T>::SyncGuiToEmbers(size_t widthOverride, size_t heightOverride, bool dowidth, bool doheight) { if (m_FinalRenderDialog->ApplyToAll()) { for (auto& ember : m_EmberFile.m_Embers) SyncGuiToEmber(ember, widthOverride, heightOverride, dowidth, doheight); } else { SyncGuiToEmber(*m_Ember, widthOverride, heightOverride, dowidth, doheight); } } /// <summary> /// Copy GUI values to the renderers. /// </summary> template <typename T> bool FinalRenderEmberController<T>::SyncGuiToRenderer() { bool ok = true; if (m_Renderer.get()) { m_Renderer->Callback(this); m_Renderer->EarlyClip(m_FinalRenderDialog->EarlyClip()); m_Renderer->YAxisUp(m_FinalRenderDialog->YAxisUp()); m_Renderer->ThreadCount(m_FinalRenderDialog->ThreadCount()); m_Renderer->Priority((eThreadPriority)m_FinalRenderDialog->ThreadPriority()); if (const auto rendererCL = dynamic_cast<RendererCL<T, float>*>(m_Renderer.get())) rendererCL->SubBatchPercentPerThread(m_FinalRenderDialog->OpenCLSubBatchPct()); } else if (!m_Renderers.empty()) { for (size_t i = 0; i < m_Renderers.size(); i++) { m_Renderers[i]->Callback(!i ? this : nullptr); m_Renderers[i]->EarlyClip(m_FinalRenderDialog->EarlyClip()); m_Renderers[i]->YAxisUp(m_FinalRenderDialog->YAxisUp()); m_Renderers[i]->ThreadCount(m_FinalRenderDialog->ThreadCount()); m_Renderers[i]->Priority((eThreadPriority)m_FinalRenderDialog->ThreadPriority()); if (const auto rendererCL = dynamic_cast<RendererCL<T, float>*>(m_Renderers[i].get())) rendererCL->SubBatchPercentPerThread(m_FinalRenderDialog->OpenCLSubBatchPct()); } } else { ok = false; m_Fractorium->ShowCritical("Renderer Creation Error", "No renderer present, aborting. See info tab for details."); } return ok; } /// <summary> /// Set values for scale spinners based on the ratio of the original dimensions to the current dimensions /// of the current ember. Also update the size suffix text. /// </summary> /// <param name="scale">Whether to update the scale values</param> /// <param name="size">Whether to update the size suffix text</param> /// <param name="dowidth">Whether to apply width value to the width scale spinner</param> /// <param name="doheight">Whether to apply height value to the height scale spinner</param> template <typename T> void FinalRenderEmberController<T>::SyncCurrentToSizeSpinners(bool scale, bool size, bool doWidth, bool doHeight) { if (scale) { if (doWidth) m_FinalRenderDialog->m_WidthScaleSpin->SetValueStealth(static_cast<double>(m_Ember->m_FinalRasW) / m_Ember->m_OrigFinalRasW);//Work backward to determine the scale. if (doHeight) m_FinalRenderDialog->m_HeightScaleSpin->SetValueStealth(static_cast<double>(m_Ember->m_FinalRasH) / m_Ember->m_OrigFinalRasH); } if (size) { if (doWidth) m_FinalRenderDialog->m_WidthSpinnerWidget->m_SpinBox->SetValueStealth(m_Ember->m_FinalRasW); if (doHeight) m_FinalRenderDialog->m_HeightSpinnerWidget->m_SpinBox->SetValueStealth(m_Ember->m_FinalRasH); } } /// <summary> /// Reset the progress bars. /// </summary> /// <param name="total">True to reset render image and total progress bars, else false to only do iter, filter and accum bars.</param> template <typename T> void FinalRenderEmberController<T>::ResetProgress(bool total) { if (total) { QMetaObject::invokeMethod(m_FinalRenderDialog->ui.FinalRenderImageCountLabel, "setText", Qt::QueuedConnection, Q_ARG(const QString&, "0 / " + ToString<qulonglong>(m_ImageCount))); QMetaObject::invokeMethod(m_FinalRenderDialog->ui.FinalRenderTotalProgress, "setValue", Qt::QueuedConnection, Q_ARG(int, 0)); } QMetaObject::invokeMethod(m_FinalRenderDialog->ui.FinalRenderIterationProgress, "setValue", Qt::QueuedConnection, Q_ARG(int, 0)); QMetaObject::invokeMethod(m_FinalRenderDialog->ui.FinalRenderFilteringProgress, "setValue", Qt::QueuedConnection, Q_ARG(int, 0)); QMetaObject::invokeMethod(m_FinalRenderDialog->ui.FinalRenderAccumProgress, "setValue", Qt::QueuedConnection, Q_ARG(int, 0)); } /// <summary> /// Set various parameters in the renderers and current ember with the values /// specified in the widgets and compute the amount of memory required to render. /// This includes the memory needed for the final output image. /// </summary> /// <returns>If successful, a tuple specifying the memory required in bytes for the histogram int he first element, the total memory in the second, and the iter count in the last, else zero.</returns> template <typename T> tuple<size_t, size_t, size_t> FinalRenderEmberController<T>::SyncAndComputeMemory() { size_t iterCount = 0; pair<size_t, size_t> p(0, 0); size_t strips; const uint channels = m_FinalRenderDialog->Ext() == "png" ? 4 : 3;//4 channels for Png, else 3. SyncGuiToEmbers(); if (m_Renderer.get()) { strips = VerifyStrips(m_Ember->m_FinalRasH, m_FinalRenderDialog->Strips(), [&](const string& s) {}, [&](const string& s) {}, [&](const string& s) {}); m_Renderer->SetEmber(*m_Ember, eProcessAction::FULL_RENDER, true); m_FinalPreviewRenderer->Render(UINT_MAX, UINT_MAX); p = m_Renderer->MemoryRequired(strips, true, m_FinalRenderDialog->DoSequence()); iterCount = m_Renderer->TotalIterCount(strips); } else if (!m_Renderers.empty()) { for (auto& renderer : m_Renderers) { renderer->SetEmber(*m_Ember, eProcessAction::FULL_RENDER, true); } m_FinalPreviewRenderer->Render(UINT_MAX, UINT_MAX); strips = 1; p = m_Renderers[0]->MemoryRequired(1, true, m_FinalRenderDialog->DoSequence()); iterCount = m_Renderers[0]->TotalIterCount(strips); } m_FinalRenderDialog->m_StripsSpin->setSuffix(" (" + ToString<qulonglong>(strips) + ")"); return tuple<size_t, size_t, size_t>(p.first, p.second, iterCount); } /// <summary> /// Compose a final output path given a base name. /// This includes the base path, the prefix, the name, the suffix and the /// extension. /// </summary> /// <param name="name">The base filename to compose a full path for</param> /// <returns>The fully composed path</returns> template <typename T> QString FinalRenderEmberController<T>::ComposePath(const QString& name, bool unique) { const auto path = MakeEnd(m_Settings->SaveFolder(), '/');//Base path. const auto full = path + m_FinalRenderDialog->Prefix() + name + m_FinalRenderDialog->Suffix() + "." + m_FinalRenderDialog->Ext(); return unique ? EmberFile<T>::UniqueFilename(full) : full; } /// <summary> /// Non-virtual functions declared in FinalRenderEmberController<T>. /// </summary> /// <summary> /// Return either m_Renderer in the case of running a CPU renderer, else /// m_Renderers[0] in the case of running OpenCL. /// </summary> /// <returns>The primary renderer</returns> template <typename T> EmberNs::Renderer<T, float>* FinalRenderEmberController<T>::FirstOrDefaultRenderer() { if (m_Renderer.get()) return dynamic_cast<EmberNs::Renderer<T, float>*>(m_Renderer.get()); else if (!m_Renderers.empty()) return dynamic_cast<EmberNs::Renderer<T, float>*>(m_Renderers[0].get()); else { throw "No final renderer, exiting."; return nullptr; } } /// <summary> /// Save the output of the last rendered image using the existing image output buffer in the renderer. /// Before rendering, this copies the image coloring/filtering values used in the last step of the rendering /// process, and performs that part of the render, before saving. /// </summary> /// <returns>The full path and filename the image was saved to.</returns> template<typename T> QString FinalRenderEmberController<T>::SaveCurrentAgain() { if (!m_Ember) return ""; if (m_GuiState.m_Strips == 1) { size_t currentStripForProgress = 0; const auto brightness = m_Ember->m_Brightness; const auto gamma = m_Ember->m_Gamma; const auto gammathresh = m_Ember->m_GammaThresh; const auto vibrancy = m_Ember->m_Vibrancy; const auto highlight = m_Ember->m_HighlightPower; const auto k2 = m_Ember->m_K2; const auto sftype = m_Ember->m_SpatialFilterType; const auto sfradius = m_Ember->m_SpatialFilterRadius; const auto minde = m_Ember->m_MinRadDE; const auto maxde = m_Ember->m_MaxRadDE; const auto curvede = m_Ember->m_CurveDE; m_Fractorium->m_Controller->ParamsToEmber(*m_Ember, true);//Update color and filter params from the main window controls, which only affect the filter and/or final accumulation stage. const auto dofilterandaccum = m_GuiState.m_EarlyClip || brightness != m_Ember->m_Brightness || k2 != m_Ember->m_K2 || minde != m_Ember->m_MinRadDE || maxde != m_Ember->m_MaxRadDE || curvede != m_Ember->m_CurveDE; auto threadIndex = m_ThreadedWriter.Current(); auto threadImage = m_ThreadedWriter.GetImage(threadIndex); //This is sort of a hack outside of the normal rendering process above. if (dofilterandaccum || gamma != m_Ember->m_Gamma || gammathresh != m_Ember->m_GammaThresh || vibrancy != m_Ember->m_Vibrancy || highlight != m_Ember->m_HighlightPower || sftype != m_Ember->m_SpatialFilterType || sfradius != m_Ember->m_SpatialFilterRadius ) { m_Run = true; m_FinishedImageCount.store(0); m_Ember->m_TemporalSamples = 1; m_Renderer->m_ProgressParameter = reinterpret_cast<void*>(¤tStripForProgress);//Need to reset this because it was set to a local variable within the render thread. m_Renderer->SetEmber(*m_Ember, dofilterandaccum ? eProcessAction::FILTER_AND_ACCUM : eProcessAction::ACCUM_ONLY); m_Renderer->Run(*threadImage, 0, m_GuiState.m_Strips, m_GuiState.m_YAxisUp); m_FinishedImageCount.fetch_add(1); HandleFinishedProgress(); m_Run = false; } auto stats = m_Renderer->Stats(); auto comments = m_Renderer->ImageComments(stats, 0, true); return SaveCurrentRender(*m_Ember, comments, *threadImage, m_Renderer->FinalRasW(), m_Renderer->FinalRasH(), m_FinalRenderDialog->Png16Bit(), m_FinalRenderDialog->Transparency()); } return ""; } /// <summary> /// Save the output of the render. /// </summary> /// <param name="ember">The ember whose rendered output will be saved</param> /// <returns>The full path and filename the image was saved to.</returns> template<typename T> QString FinalRenderEmberController<T>::SaveCurrentRender(Ember<T>& ember) { auto comments = m_Renderer->ImageComments(m_Stats, 0, true); return SaveCurrentRender(ember, comments, m_FinalImage, m_Renderer->FinalRasW(), m_Renderer->FinalRasH(), m_FinalRenderDialog->Png16Bit(), m_FinalRenderDialog->Transparency()); } /// <summary> /// Save the output of the render. /// </summary> /// <param name="ember">The ember whose rendered output will be saved</param> /// <param name="comments">The comments to save in the png, jpg or exr</param> /// <param name="pixels">The buffer containing the pixels</param> /// <param name="width">The width in pixels of the image</param> /// <param name="height">The height in pixels of the image</param> /// <param name="png16Bit">Whether to use 16 bits per channel per pixel when saving as Png/32-bits per channel when saving as Exr.</param> /// <param name="transparency">Whether to use alpha when saving as Png or Exr.</param> /// <returns>The full path and filename the image was saved to. Empty string is saving failed.</returns> template<typename T> QString FinalRenderEmberController<T>::SaveCurrentRender(Ember<T>& ember, const EmberImageComments& comments, vector<v4F>& pixels, size_t width, size_t height, bool png16Bit, bool transparency) { const auto filename = ComposePath(QString::fromStdString(ember.m_Name)); return FractoriumEmberControllerBase::SaveCurrentRender(filename, comments, pixels, width, height, png16Bit, transparency) ? filename : ""; } /// <summary> /// Action to take when rendering an image completes. /// Thin wrapper around the function of the same name that takes more arguments. /// Just passes m_Renderer and m_FinalImage. /// </summary> /// <param name="ember">The ember currently being rendered</param> template<typename T> void FinalRenderEmberController<T>::RenderComplete(Ember<T>& ember) { if (const auto renderer = dynamic_cast<EmberNs::Renderer<T, float>*>(m_Renderer.get())) RenderComplete(ember, m_Stats, m_RenderTimer); } /// <summary> /// Pause or resume the renderer(s). /// </summary> /// <param name="pause">True to pause, false to unpause.</param> template<typename T> void FinalRenderEmberController<T>::Pause(bool pause) { if (m_Renderer.get()) { m_Renderer->Pause(pause); } else { for (auto& r : m_Renderers) r->Pause(pause); } } /// <summary> /// Retrieve the paused state of the renderer(s). /// </summary> /// <returns>True if the renderer(s) is paused, else false.</returns> template<typename T> bool FinalRenderEmberController<T>::Paused() { if (m_Renderer.get()) { return m_Renderer->Paused(); } else { bool b = !m_Renderers.empty(); for (auto& r : m_Renderers) b &= r->Paused(); return b; } } /// <summary> /// Handle setting the appropriate progress bar values when an image render has finished. /// This handles single image, animations, and strips. /// </summary> template<typename T> void FinalRenderEmberController<T>::HandleFinishedProgress() { const auto finishedCountCached = m_FinishedImageCount.load();//Make sure to use the same value throughout this function even if the atomic is changing. const bool doAll = m_GuiState.m_DoAll && m_EmberFile.Size() > 1; if (m_FinishedImageCount.load() != m_ImageCount) ResetProgress(false); else SetProgressComplete(100);//Just to be safe. QMetaObject::invokeMethod(m_FinalRenderDialog->ui.FinalRenderTotalProgress, "setValue", Qt::QueuedConnection, Q_ARG(int, static_cast<int>((float(finishedCountCached) / static_cast<float>(m_ImageCount)) * 100))); QMetaObject::invokeMethod(m_FinalRenderDialog->ui.FinalRenderImageCountLabel, "setText", Qt::QueuedConnection, Q_ARG(const QString&, ToString<qulonglong>(finishedCountCached) + " / " + ToString<qulonglong>(m_ImageCount))); } /// <summary> /// Action to take when rendering an image completes. /// </summary> /// <param name="ember">The ember currently being rendered</param> /// <param name="stats">The renderer stats</param> /// <param name="renderTimer">The timer which was started at the beginning of the render</param> template<typename T> void FinalRenderEmberController<T>::RenderComplete(Ember<T>& ember, const EmberStats& stats, Timing& renderTimer) { rlg l(m_ProgressCs); const auto renderTimeString = renderTimer.Format(renderTimer.Toc()); QString status; const auto filename = ComposePath(QString::fromStdString(ember.m_Name), false); const auto itersString = ToString<qulonglong>(stats.m_Iters); const auto itersPerSecString = ToString<qulonglong>(static_cast<size_t>(stats.m_Iters / (stats.m_IterMs / 1000.0))); if (m_GuiState.m_SaveXml) { const QFileInfo xmlFileInfo(filename);//Create another one in case it was modified for batch rendering. QString newPath = xmlFileInfo.absolutePath() + '/' + xmlFileInfo.completeBaseName() + ".flame"; newPath = EmberFile<T>::UniqueFilename(newPath); const xmlDocPtr tempEdit = ember.m_Edits; ember.m_Edits = m_XmlWriter.CreateNewEditdoc(&ember, nullptr, "edit", m_Settings->Nick().toStdString(), m_Settings->Url().toStdString(), m_Settings->Id().toStdString(), "", 0, 0); m_XmlWriter.Save(newPath.toStdString().c_str(), ember, 0, true, false, true);//Note that the ember passed is used, rather than m_Ember because it's what was actually rendered. if (tempEdit) xmlFreeDoc(tempEdit); } status = "Render time: " + QString::fromStdString(renderTimeString); Output(status); status = "Total iters: " + itersString + "\nIters/second: " + itersPerSecString + "\n"; Output(status); QMetaObject::invokeMethod(m_FinalRenderDialog, "MoveCursorToEnd", Qt::QueuedConnection); if (m_FinishedImageCount.load() == m_ImageCount)//Finished, save whatever options were specified on the GUI to the settings. { m_Settings->FinalEarlyClip(m_GuiState.m_EarlyClip); m_Settings->FinalYAxisUp(m_GuiState.m_YAxisUp); m_Settings->FinalTransparency(m_GuiState.m_Transparency); m_Settings->FinalOpenCL(m_GuiState.m_OpenCL); m_Settings->FinalDouble(m_GuiState.m_Double); m_Settings->FinalDevices(m_GuiState.m_Devices); m_Settings->FinalSaveXml(m_GuiState.m_SaveXml); m_Settings->FinalDoAll(m_GuiState.m_DoAll); m_Settings->FinalDoSequence(m_GuiState.m_DoSequence); m_Settings->FinalUseNumbers(m_GuiState.m_UseNumbers); m_Settings->FinalPng16Bit(m_GuiState.m_Png16Bit); m_Settings->FinalKeepAspect(m_GuiState.m_KeepAspect); m_Settings->FinalScale(uint(m_GuiState.m_Scale)); m_Settings->FinalExt(m_GuiState.m_Ext); m_Settings->FinalThreadCount(m_GuiState.m_ThreadCount); m_Settings->FinalThreadPriority(m_GuiState.m_ThreadPriority); m_Settings->FinalOpenCLSubBatchPct(m_GuiState.m_SubBatchPct); m_Settings->FinalQuality(m_GuiState.m_Quality); m_Settings->FinalTemporalSamples(m_GuiState.m_TemporalSamples); m_Settings->FinalSupersample(m_GuiState.m_Supersample); m_Settings->FinalStrips(m_GuiState.m_Strips); } QMetaObject::invokeMethod(m_FinalRenderDialog->ui.FinalRenderTextOutput, "update", Qt::QueuedConnection); } /// <summary> /// Copy widget values to the ember passed in. /// </summary> /// <param name="ember">The ember whose values will be modified</param> /// <param name="widthOverride">Width override to use instead of scaling the original width</param> /// <param name="heightOverride">Height override to use instead of scaling the original height</param> /// <param name="dowidth">Whether to use the computed/overridden width value, or use the existing value in the ember</param> /// <param name="doheight">Whether to use the computed/overridden height value, or use the existing value in the ember</param> template <typename T> void FinalRenderEmberController<T>::SyncGuiToEmber(Ember<T>& ember, size_t widthOverride, size_t heightOverride, bool dowidth, bool doheight) { size_t w; size_t h; if (widthOverride && heightOverride) { w = widthOverride; h = heightOverride; } else { const auto wScale = m_FinalRenderDialog->m_WidthScaleSpin->value(); const auto hScale = m_FinalRenderDialog->m_HeightScaleSpin->value(); w = ember.m_OrigFinalRasW * wScale; h = ember.m_OrigFinalRasH * hScale; } w = dowidth ? std::max<size_t>(w, 10) : ember.m_FinalRasW; h = doheight ? std::max<size_t>(h, 10) : ember.m_FinalRasH; ember.SetSizeAndAdjustScale(w, h, false, m_FinalRenderDialog->Scale()); ember.m_Quality = m_FinalRenderDialog->m_QualitySpin->value(); ember.m_Supersample = m_FinalRenderDialog->m_SupersampleSpin->value(); } /// <summary> /// Set the iteration, density filter, and final accumulation progress bars to the same value. /// Usually 0 or 100. /// </summary> /// <param name="val">The value to set them to</param> template <typename T> void FinalRenderEmberController<T>::SetProgressComplete(int val) { QMetaObject::invokeMethod(m_FinalRenderDialog->ui.FinalRenderIterationProgress, "setValue", Qt::QueuedConnection, Q_ARG(int, val));//Just to be safe. QMetaObject::invokeMethod(m_FinalRenderDialog->ui.FinalRenderFilteringProgress, "setValue", Qt::QueuedConnection, Q_ARG(int, val)); QMetaObject::invokeMethod(m_FinalRenderDialog->ui.FinalRenderAccumProgress, "setValue", Qt::QueuedConnection, Q_ARG(int, val)); } /// <summary> /// Check if the amount of required memory is greater than that available on /// all required OpenCL devices. Also check if enough space is available for the max allocation. /// No check is done for CPU renders. /// Report errors if not enough memory is available for any of the selected devices. /// </summary> /// <returns>A string with an error report if required memory exceeds available memory on any device, else empty string.</returns> template <typename T> QString FinalRenderEmberController<T>::CheckMemory(const tuple<size_t, size_t, size_t>& p) { bool error = false; QString s; const auto histSize = get<0>(p); const auto totalSize = get<1>(p); auto selectedDevices = m_FinalRenderDialog->Devices(); static vector<RendererCL<T, float>*> clRenderers; clRenderers.clear(); //Find all OpenCL renderers currently being used and place them in a vector of pointers. if (m_FinalRenderDialog->DoSequence()) { for (auto& r : m_Renderers) if (auto clr = dynamic_cast<RendererCL<T, float>*>(r.get())) clRenderers.push_back(clr); } else { if (auto clr = dynamic_cast<RendererCL<T, float>*>(m_Renderer.get())) clRenderers.push_back(clr); } //Iterate through each renderer and examine each device it's using. for (auto r : clRenderers) { const auto& devices = r->Devices(); for (auto& d : devices) { const auto& wrapper = d->m_Wrapper; const auto index = wrapper.TotalDeviceIndex(); if (selectedDevices.contains(int(index))) { bool err = false; QString temp; const auto maxAlloc = wrapper.MaxAllocSize(); const auto totalAvail = wrapper.GlobalMemSize(); if (histSize > maxAlloc) { err = true; temp = "Histogram/Accumulator memory size of " + ToString<qulonglong>(histSize) + " is greater than the max OpenCL allocation size of " + ToString<qulonglong>(maxAlloc); } if (totalSize > totalAvail) { if (err) temp += "\n\n"; temp += "Total required memory size of " + ToString<qulonglong>(totalSize) + " is greater than the max OpenCL available memory of " + ToString<qulonglong>(totalAvail); } if (!temp.isEmpty()) { error = true; s += QString::fromStdString(wrapper.DeviceName()) + ":\n" + temp + "\n\n"; } } } } if (!s.isEmpty()) s += "Rendering will most likely fail.\n\nMake strips > 1 to fix this. Strips must divide into the height evenly, and will also scale the number of iterations performed."; return s; } /// <summary> /// Thin derivation to handle preview rendering that is specific to the final render dialog. /// This differs from the preview renderers on the main window because they render multiple embers /// to a tree, whereas this renders a single preview. /// </summary> /// <param name="start">Ignored</param> /// <param name="end">Ignored</param> template <typename T> void FinalRenderPreviewRenderer<T>::PreviewRenderFunc(uint start, uint end) { T scalePercentage; const size_t maxDim = 100; const auto d = m_Controller->m_FinalRenderDialog; QLabel* widget = d->ui.FinalRenderPreviewLabel; //Determine how to scale the scaled ember to fit in the label with a max of 100x100. const auto e = m_Controller->m_Ember; const auto settings = FractoriumSettings::Instance(); if (e->m_FinalRasW >= e->m_FinalRasH) scalePercentage = static_cast<T>(maxDim) / e->m_FinalRasW; else scalePercentage = static_cast<T>(maxDim) / e->m_FinalRasH; m_PreviewEmber = *e; m_PreviewEmber.m_Quality = 100; m_PreviewEmber.m_TemporalSamples = 1; m_PreviewEmber.m_FinalRasW = std::max<size_t>(1, std::min<size_t>(maxDim, static_cast<size_t>(scalePercentage * e->m_FinalRasW)));//Ensure neither is zero. m_PreviewEmber.m_FinalRasH = std::max<size_t>(1, std::min<size_t>(maxDim, static_cast<size_t>(scalePercentage * e->m_FinalRasH))); m_PreviewEmber.m_PixelsPerUnit = scalePercentage * e->m_PixelsPerUnit; m_PreviewRenderer.EarlyClip(d->EarlyClip()); m_PreviewRenderer.YAxisUp(d->YAxisUp()); m_PreviewRenderer.Callback(nullptr); m_PreviewRenderer.SetEmber(m_PreviewEmber, eProcessAction::FULL_RENDER, true); m_PreviewRenderer.PrepFinalAccumVector(m_PreviewFinalImage);//Must manually call this first because it could be erroneously made smaller due to strips if called inside Renderer::Run(). auto strips = VerifyStrips(m_PreviewEmber.m_FinalRasH, d->Strips(), [&](const string& s) {}, [&](const string& s) {}, [&](const string& s) {}); StripsRender<T>(&m_PreviewRenderer, m_PreviewEmber, m_PreviewFinalImage, 0, strips, d->YAxisUp(), [&](size_t strip) {},//Pre strip. [&](size_t strip) {},//Post strip. [&](size_t strip) {},//Error. [&](Ember<T>& finalEmber)//Final strip. { m_PreviewVec.resize(finalEmber.m_FinalRasW * finalEmber.m_FinalRasH * 4); Rgba32ToRgba8(m_PreviewFinalImage.data(), m_PreviewVec.data(), finalEmber.m_FinalRasW, finalEmber.m_FinalRasH, d->Transparency()); QImage image(static_cast<int>(finalEmber.m_FinalRasW), static_cast<int>(finalEmber.m_FinalRasH), QImage::Format_RGBA8888);//The label wants RGBA. memcpy(image.scanLine(0), m_PreviewVec.data(), SizeOf(m_PreviewVec));//Memcpy the data in. QPixmap pixmap(QPixmap::fromImage(image)); QMetaObject::invokeMethod(widget, "setPixmap", Qt::QueuedConnection, Q_ARG(QPixmap, pixmap)); }); } template class FinalRenderEmberController<float>; #ifdef DO_DOUBLE template class FinalRenderEmberController<double>; #endif