diff --git a/Builds/MSVC/Installer/Product.wxs b/Builds/MSVC/Installer/Product.wxs
index f6a2232..578bfe9 100644
--- a/Builds/MSVC/Installer/Product.wxs
+++ b/Builds/MSVC/Installer/Product.wxs
@@ -1,3 +1,4 @@
+<<<<<<< HEAD
@@ -360,3 +361,397 @@
+=======
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+>>>>>>> a4bfffaa3f55362a86915c700186ee3ed03b90fa
diff --git a/Source/Fractorium/FinalRenderEmberController.cpp b/Source/Fractorium/FinalRenderEmberController.cpp
index 502bc31..00f6808 100644
--- a/Source/Fractorium/FinalRenderEmberController.cpp
+++ b/Source/Fractorium/FinalRenderEmberController.cpp
@@ -1,3 +1,4 @@
+<<<<<<< HEAD
#include "FractoriumPch.h"
#include "FractoriumEmberController.h"
#include "FinalRenderEmberController.h"
@@ -1183,3 +1184,1187 @@ template class FinalRenderEmberController;
#ifdef DO_DOUBLE
template class FinalRenderEmberController;
#endif
+=======
+#include "FractoriumPch.h"
+#include "FractoriumEmberController.h"
+#include "FinalRenderEmberController.h"
+#include "FinalRenderDialog.h"
+#include "Fractorium.h"
+
+///
+/// Constructor which accepts a pointer to the final render dialog.
+/// It passes a pointer to the main window to the base and initializes members.
+///
+/// Pointer to the final render dialog
+FinalRenderEmberControllerBase::FinalRenderEmberControllerBase(FractoriumFinalRenderDialog* finalRenderDialog)
+ : FractoriumEmberControllerBase(finalRenderDialog->m_Fractorium),
+ m_FinalRenderDialog(finalRenderDialog)
+{
+ m_FinishedImageCount.store(0);
+ m_Settings = FractoriumSettings::DefInstance();
+}
+
+///
+/// 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.
+///
+template
+void FinalRenderEmberController::CancelRender()
+{
+ if (m_Result.isRunning())
+ {
+ std::thread th([&]
+ {
+ m_Run = false;
+
+ if (m_Renderer.get())
+ {
+ m_Renderer->Abort();
+
+ while (m_Renderer->InRender())
+ std::this_thread::sleep_for(std::chrono::milliseconds(10));
+
+ m_Renderer->EnterRender();
+ m_Renderer->EnterFinalAccum();
+ m_Renderer->LeaveFinalAccum();
+ m_Renderer->LeaveRender();
+ }
+ 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.");
+ }
+}
+
+///
+/// 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.
+///
+/// True if a valid renderer is created or if no action is taken, else false.
+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.
+}
+
+///
+/// Thin wrapper around invoking a call to append text to the output.
+///
+/// The string to append
+void FinalRenderEmberControllerBase::Output(const QString& s)
+{
+ QMetaObject::invokeMethod(m_FinalRenderDialog->ui.FinalRenderTextOutput, "append", Qt::QueuedConnection, Q_ARG(const QString&, s));
+}
+
+
+///
+/// Render a single ember.
+///
+/// The ember to render
+/// Is this is a FULL_RENDER or if we should KEEP_ITERATING.
+/// Used to report progress when strips.
+/// True if rendering succeeded.
+template
+bool FinalRenderEmberController::RenderSingleEmber(Ember& ember, bool fullRender, size_t& stripForProgress)
+{
+ if (!m_Renderer.get())
+ {
+ return false;
+ }
+
+ ember.m_TemporalSamples = 1;//No temporal sampling.
+ m_Renderer->SetEmber(ember, fullRender ? eProcessAction::FULL_RENDER : eProcessAction::KEEP_ITERATING, /* updatePointer */ true);
+ m_Renderer->PrepFinalAccumVector(m_FinalImage);//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(m_Renderer.get(), ember, m_FinalImage, 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.
+ },
+ [&](Ember& finalEmber)
+ {
+ m_FinishedImageCount.fetch_add(1);
+ SaveCurrentRender(finalEmber);
+ RenderComplete(finalEmber);
+ HandleFinishedProgress();
+ });//Final strip.
+ return true;
+}
+
+///
+/// Render a single ember from a series of embers.
+/// m_Renderers.SetExternalEmbersPointer should already be set.
+///
+/// Used to coordinate which frame to render.
+/// which index into m_Renderers to use.
+/// True if rendering succeeded.
+template
+bool FinalRenderEmberController::RenderSingleEmberFromSeries(std::atomic* atomfTime, size_t index)
+{
+ if (m_Renderers.size() <= index)
+ {
+ return false;
+ }
+
+ size_t ftime;
+ size_t finalImageIndex = 0;
+ std::thread writeThread;
+ vector finalImages[2];
+ EmberStats stats;
+ EmberImageComments comments;
+ Timing renderTimer;
+ const auto renderer = m_Renderers[index].get();
+
+ //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().
+
+ //Can't use strips render here. Run() must be called directly for animation.
+ if (renderer->Run(finalImages[finalImageIndex], 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
+ {
+ Join(writeThread);
+ stats = renderer->Stats();
+ comments = renderer->ImageComments(stats, 0, true);
+ writeThread = std::thread([&](size_t tempTime, size_t threadFinalImageIndex)
+ {
+ SaveCurrentRender(*m_EmberFile.Get(tempTime),
+ comments,//These all don't change during the renders, so it's ok to access them in the thread.
+ finalImages[threadFinalImageIndex],
+ renderer->FinalRasW(),
+ renderer->FinalRasH(),
+ m_FinalRenderDialog->Png16Bit(),
+ m_FinalRenderDialog->Transparency());
+ }, ftime, finalImageIndex);
+ m_FinishedImageCount.fetch_add(1);
+ RenderComplete(*m_EmberFile.Get(ftime), 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();
+ }
+
+ finalImageIndex ^= 1;//Toggle the index.
+ }
+
+ Join(writeThread);//One final check to make sure all writing is done before exiting this thread.
+ return m_Run;
+}
+
+///
+/// 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.
+///
+/// Pointer to the final render dialog
+template
+FinalRenderEmberController::FinalRenderEmberController(FractoriumFinalRenderDialog* finalRender)
+ : FinalRenderEmberControllerBase(finalRender)
+{
+ m_FinalPreviewRenderer = make_unique>(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(¤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)
+ {
+ m_ImageCount = m_EmberFile.Size();
+
+ //Different action required for rendering as animation or not.
+ if (m_GuiState.m_DoSequence && !m_Renderers.empty())
+ {
+ Ember* prev = nullptr;
+ vector> 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);
+ std::atomic atomfTime(0);
+ vector 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::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.
+ for (auto& it : m_EmberFile.m_Embers)
+ {
+ if (!m_Run)
+ break;
+
+ Output("Image " + ToString(m_FinishedImageCount.load() + 1) + ":\n" + ComposePath(QString::fromStdString(it.m_Name)));
+ RenderSingleEmber(it, /* fullRender */ true, currentStripForProgress);
+ }
+ }
+ 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, /* fullRender= */ !isBump, currentStripForProgress);
+ }
+ else
+ {
+ Output("No renderer present, aborting.");
+ }
+
+ const QString totalTimeString = "All renders completed in: " + QString::fromStdString(m_TotalTimer.Format(m_TotalTimer.Toc())) + ".";
+ Output(totalTimeString);
+ QFile::remove(backup);
+ QMetaObject::invokeMethod(m_FinalRenderDialog, "Pause", Qt::QueuedConnection, Q_ARG(bool, false));
+ m_Run = false;
+ };
+}
+
+///
+/// Virtual functions overridden from FractoriumEmberControllerBase.
+///
+
+///
+/// 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.
+///
+template void FinalRenderEmberController::SetEmberFile(const EmberFile& emberFile, bool move)
+{
+ move ? m_EmberFile = std::move(emberFile) : m_EmberFile = emberFile;
+ m_Ember = m_EmberFile.Get(0);
+}
+template void FinalRenderEmberController::CopyEmberFile(EmberFile& emberFile, bool sequence, std::function& ember)> perEmberOperation)
+{
+ emberFile.m_Filename = m_EmberFile.m_Filename;
+ CopyCont(emberFile.m_Embers, m_EmberFile.m_Embers, perEmberOperation);
+}
+
+#ifdef DO_DOUBLE
+template void FinalRenderEmberController::SetEmberFile(const EmberFile& emberFile, bool move)
+{
+ move ? m_EmberFile = std::move(emberFile) : m_EmberFile = emberFile;
+ m_Ember = m_EmberFile.Get(0);
+}
+template void FinalRenderEmberController::CopyEmberFile(EmberFile& emberFile, bool sequence, std::function& ember)> perEmberOperation)
+{
+ emberFile.m_Filename = m_EmberFile.m_Filename;
+ CopyCont(emberFile.m_Embers, m_EmberFile.m_Embers, perEmberOperation);
+}
+#endif
+
+///
+/// Set the ember at the specified index from the currently opened file as the current Ember.
+/// Clears the undo state.
+/// Resets the rendering process.
+///
+/// The index in the file from which to retrieve the ember
+/// Unused
+template
+void FinalRenderEmberController::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.
+ }
+}
+
+///
+/// Save current ember as Xml using the filename specified.
+///
+/// The filename to save the ember to.
+template
+void FinalRenderEmberController::SaveCurrentAsXml(QString filename)
+{
+ const auto ember = m_Ember;
+ EmberToXml 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.");
+}
+
+///
+/// Start the final rendering process.
+/// Create the needed renderer from the GUI if it has not been created yet.
+///
+///
+template
+bool FinalRenderEmberController::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;
+}
+
+
+///
+/// Increase the quality of the last render and start rendering again.
+/// Note this is only when rendering a single image with no strips.
+///
+/// The amount to increase the quality by, expressed as a decimal percentage. Eg: 0.5 means to increase by 50%.
+/// True if nothing went wrong, else false.
+template
+bool FinalRenderEmberController::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(iterCount));
+ m_FinalRenderDialog->ui.FinalRenderTextOutput->setText("Preparing all parameters.\n");
+ m_Result = QtConcurrent::run(m_FinalRenderFunc);
+ m_Settings->sync();
+ return true;
+}
+
+///
+/// Stop rendering and initialize a new renderer, using the specified type and the options on the final render dialog.
+///
+/// The type of render to create
+/// The platform,device index pairs of the devices to use
+/// Unused
+/// True if shared with OpenGL, else false. Always false in this case.
+/// True if nothing went wrong, else false.
+template
+bool FinalRenderEmberController::CreateRenderer(eRendererType renderType, const vector>& 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 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(renderType, m_Devices, shared, m_OutputTexID, emberReport);
+
+ for (auto& renderer : m_Renderers)
+ if (const auto rendererCL = dynamic_cast(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(::CreateRenderer(renderType, m_Devices, shared, m_OutputTexID, emberReport));
+
+ if (const auto rendererCL = dynamic_cast(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;
+}
+
+///
+/// Progress function.
+/// Take special action to sync options upon finishing.
+/// Note this is only called on the primary renderer.
+///
+/// The ember currently being rendered
+/// An extra dummy parameter, unused.
+/// The progress fraction from 0-100
+/// The stage of iteration. 1 is iterating, 2 is density filtering, 2 is final accumulation.
+/// The estimated milliseconds to completion of the current stage
+/// 0 if the user has clicked cancel, else 1 to continue rendering.
+template
+int FinalRenderEmberController::ProgressFunc(Ember& ember, void* foo, double fraction, int stage, double etaMs)
+{
+ static int count = 0;
+ const size_t strip = *(reinterpret_cast(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(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(m_FinishedImageCount.load() + 1) + " / " + ToString(m_ImageCount) + " Eta: " + QString::fromStdString(m_RenderTimer.Format(etaMs))));
+ QMetaObject::invokeMethod(m_FinalRenderDialog->ui.FinalRenderTextOutput, "update", Qt::QueuedConnection);
+ return m_Run ? 1 : 0;
+}
+
+///
+/// Virtual functions overridden from FinalRenderEmberControllerBase.
+///
+
+///
+/// Copy current ember values to widgets.
+///
+template
+void FinalRenderEmberController::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()));
+}
+
+///
+/// Copy GUI values to either the current ember, or all embers in the file
+/// depending on whether Render All is checked.
+///
+/// Width override to use instead of scaling the original width
+/// Height override to use instead of scaling the original height
+/// Whether to apply width adjustment to the ember
+/// Whether to apply height adjustment to the ember
+template
+void FinalRenderEmberController::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);
+ }
+}
+
+///
+/// Copy GUI values to the renderers.
+///
+template
+bool FinalRenderEmberController::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*>(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*>(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;
+}
+
+///
+/// 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.
+///
+/// Whether to update the scale values
+/// Whether to update the size suffix text
+/// Whether to apply width value to the width scale spinner
+/// Whether to apply height value to the height scale spinner
+template
+void FinalRenderEmberController::SyncCurrentToSizeSpinners(bool scale, bool size, bool doWidth, bool doHeight)
+{
+ if (scale)
+ {
+ if (doWidth)
+ m_FinalRenderDialog->m_WidthScaleSpin->SetValueStealth(static_cast(m_Ember->m_FinalRasW) / m_Ember->m_OrigFinalRasW);//Work backward to determine the scale.
+
+ if (doHeight)
+ m_FinalRenderDialog->m_HeightScaleSpin->SetValueStealth(static_cast(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);
+ }
+}
+
+///
+/// Reset the progress bars.
+///
+/// True to reset render image and total progress bars, else false to only do iter, filter and accum bars.
+template
+void FinalRenderEmberController::ResetProgress(bool total)
+{
+ if (total)
+ {
+ QMetaObject::invokeMethod(m_FinalRenderDialog->ui.FinalRenderImageCountLabel, "setText", Qt::QueuedConnection, Q_ARG(const QString&, "0 / " + ToString(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));
+}
+
+///
+/// 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.
+///
+/// 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.
+template
+tuple FinalRenderEmberController::SyncAndComputeMemory()
+{
+ size_t iterCount = 0;
+ pair 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(strips) + ")");
+ return tuple(p.first, p.second, iterCount);
+}
+
+///
+/// Compose a final output path given a base name.
+/// This includes the base path, the prefix, the name, the suffix and the
+/// extension.
+///
+/// The base filename to compose a full path for
+/// The fully composed path
+template
+QString FinalRenderEmberController::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::UniqueFilename(full) : full;
+}
+
+///
+/// Non-virtual functions declared in FinalRenderEmberController.
+///
+
+///
+/// Return either m_Renderer in the case of running a CPU renderer, else
+/// m_Renderers[0] in the case of running OpenCL.
+///
+/// The primary renderer
+template
+EmberNs::Renderer* FinalRenderEmberController::FirstOrDefaultRenderer()
+{
+ if (m_Renderer.get())
+ return dynamic_cast*>(m_Renderer.get());
+ else if (!m_Renderers.empty())
+ return dynamic_cast*>(m_Renderers[0].get());
+ else
+ {
+ throw "No final renderer, exiting.";
+ return nullptr;
+ }
+}
+
+///
+/// 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.
+///
+/// The full path and filename the image was saved to.
+template
+QString FinalRenderEmberController::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;
+
+ //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(¤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(m_FinalImage, 0, m_GuiState.m_Strips, m_GuiState.m_YAxisUp);
+ m_FinishedImageCount.fetch_add(1);
+ HandleFinishedProgress();
+ m_Run = false;
+ }
+ }
+
+ return SaveCurrentRender(*m_Ember);
+}
+
+///
+/// Save the output of the render.
+///
+/// The ember whose rendered output will be saved
+/// The full path and filename the image was saved to.
+template
+QString FinalRenderEmberController::SaveCurrentRender(Ember& 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());
+}
+
+///
+/// Save the output of the render.
+///
+/// The ember whose rendered output will be saved
+/// The comments to save in the png, jpg or exr
+/// The buffer containing the pixels
+/// The width in pixels of the image
+/// The height in pixels of the image
+/// Whether to use 16 bits per channel per pixel when saving as Png/32-bits per channel when saving as Exr.
+/// Whether to use alpha when saving as Png or Exr.
+/// The full path and filename the image was saved to.
+template
+QString FinalRenderEmberController::SaveCurrentRender(Ember& ember, const EmberImageComments& comments, vector& pixels, size_t width, size_t height, bool png16Bit, bool transparency)
+{
+ const auto filename = ComposePath(QString::fromStdString(ember.m_Name));
+ FractoriumEmberControllerBase::SaveCurrentRender(filename, comments, pixels, width, height, png16Bit, transparency);
+ return filename;
+}
+
+///
+/// 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.
+///
+/// The ember currently being rendered
+template
+void FinalRenderEmberController::RenderComplete(Ember& ember)
+{
+ if (const auto renderer = dynamic_cast*>(m_Renderer.get()))
+ RenderComplete(ember, m_Stats, m_RenderTimer);
+}
+
+///
+/// Pause or resume the renderer(s).
+///
+/// True to pause, false to unpause.
+template
+void FinalRenderEmberController::Pause(bool pause)
+{
+ if (m_Renderer.get())
+ {
+ m_Renderer->Pause(pause);
+ }
+ else
+ {
+ for (auto& r : m_Renderers)
+ r->Pause(pause);
+ }
+}
+
+///
+/// Retrieve the paused state of the renderer(s).
+///
+/// True if the renderer(s) is paused, else false.
+template
+bool FinalRenderEmberController::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;
+ }
+}
+
+///
+/// Handle setting the appropriate progress bar values when an image render has finished.
+/// This handles single image, animations, and strips.
+///
+template
+void FinalRenderEmberController::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((float(finishedCountCached) / static_cast(m_ImageCount)) * 100)));
+ QMetaObject::invokeMethod(m_FinalRenderDialog->ui.FinalRenderImageCountLabel, "setText", Qt::QueuedConnection, Q_ARG(const QString&, ToString(finishedCountCached) + " / " + ToString(m_ImageCount)));
+ 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));
+}
+
+///
+/// Action to take when rendering an image completes.
+///
+/// The ember currently being rendered
+/// The renderer stats
+/// The timer which was started at the beginning of the render
+template
+void FinalRenderEmberController::RenderComplete(Ember& 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(stats.m_Iters);
+ const auto itersPerSecString = ToString(static_cast(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::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->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);
+}
+
+///
+/// Copy widget values to the ember passed in.
+///
+/// The ember whose values will be modified
+/// Width override to use instead of scaling the original width
+/// Height override to use instead of scaling the original height
+/// Whether to use the computed/overridden width value, or use the existing value in the ember
+/// Whether to use the computed/overridden height value, or use the existing value in the ember
+template
+void FinalRenderEmberController::SyncGuiToEmber(Ember& 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(w, 10) : ember.m_FinalRasW;
+ h = doheight ? std::max(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();
+}
+
+///
+/// Set the iteration, density filter, and final accumulation progress bars to the same value.
+/// Usually 0 or 100.
+///
+/// The value to set them to
+template
+void FinalRenderEmberController::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));
+}
+
+///
+/// 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.
+///
+/// A string with an error report if required memory exceeds available memory on any device, else empty string.
+template
+QString FinalRenderEmberController::CheckMemory(const tuple& 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*> 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*>(r.get()))
+ clRenderers.push_back(clr);
+ }
+ else
+ {
+ if (auto clr = dynamic_cast*>(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(histSize) +
+ " is greater than the max OpenCL allocation size of " + ToString(maxAlloc);
+ }
+
+ if (totalSize > totalAvail)
+ {
+ if (err)
+ temp += "\n\n";
+
+ temp += "Total required memory size of " + ToString(totalSize) +
+ " is greater than the max OpenCL available memory of " + ToString(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;
+}
+
+///
+/// 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.
+///
+/// Ignored
+/// Ignored
+template
+void FinalRenderPreviewRenderer::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(maxDim) / e->m_FinalRasW;
+ else
+ scalePercentage = static_cast(maxDim) / e->m_FinalRasH;
+
+ m_PreviewEmber = *e;
+ m_PreviewEmber.m_Quality = 100;
+ m_PreviewEmber.m_TemporalSamples = 1;
+ m_PreviewEmber.m_FinalRasW = std::max(1, std::min(maxDim, static_cast(scalePercentage * e->m_FinalRasW)));//Ensure neither is zero.
+ m_PreviewEmber.m_FinalRasH = std::max(1, std::min(maxDim, static_cast(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(&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& 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(finalEmber.m_FinalRasW), static_cast(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;
+
+#ifdef DO_DOUBLE
+ template class FinalRenderEmberController;
+#endif
+>>>>>>> a4bfffaa3f55362a86915c700186ee3ed03b90fa
diff --git a/Source/Fractorium/FractoriumPalette.cpp b/Source/Fractorium/FractoriumPalette.cpp
index b6e7366..71006ff 100644
--- a/Source/Fractorium/FractoriumPalette.cpp
+++ b/Source/Fractorium/FractoriumPalette.cpp
@@ -1,3 +1,4 @@
+<<<<<<< HEAD
#include "FractoriumPch.h"
#include "Fractorium.h"
#include "PaletteTableWidgetItem.h"
@@ -738,3 +739,745 @@ template class FractoriumEmberController;
#ifdef DO_DOUBLE
template class FractoriumEmberController;
#endif
+=======
+#include "FractoriumPch.h"
+#include "Fractorium.h"
+#include "PaletteTableWidgetItem.h"
+
+///
+/// Initialize the palette UI.
+///
+void Fractorium::InitPaletteUI()
+{
+ int spinHeight = 20, row = 0;
+ auto paletteTable = ui.PaletteListTable;
+ auto palettePreviewTable = ui.PalettePreviewTable;
+ connect(ui.PaletteFilenameCombo, SIGNAL(currentIndexChanged(const QString&)), this, SLOT(OnPaletteFilenameComboChanged(const QString&)), Qt::QueuedConnection);
+ connect(paletteTable, SIGNAL(cellClicked(int, int)), this, SLOT(OnPaletteCellClicked(int, int)), Qt::QueuedConnection);
+ connect(paletteTable, SIGNAL(cellDoubleClicked(int, int)), this, SLOT(OnPaletteCellDoubleClicked(int, int)), Qt::QueuedConnection);
+ connect(palettePreviewTable, SIGNAL(MouseDragged(const QPointF&, const QPoint&)), this, SLOT(OnPreviewPaletteMouseDragged(const QPointF&, const QPoint&)), Qt::QueuedConnection);
+ connect(palettePreviewTable, SIGNAL(MouseReleased()), this, SLOT(OnPreviewPaletteMouseReleased()), Qt::QueuedConnection);
+ connect(palettePreviewTable, SIGNAL(cellDoubleClicked(int, int)), this, SLOT(OnPreviewPaletteCellDoubleClicked(int, int)), Qt::QueuedConnection);
+ connect(palettePreviewTable, SIGNAL(cellPressed(int, int)), this, SLOT(OnPreviewPaletteCellPressed(int, int)), Qt::QueuedConnection);
+ //Palette adjustment table.
+ auto table = ui.PaletteAdjustTable;
+ table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);//Split width over all columns evenly.
+ SetupSpinner(table, this, row, 1, m_PaletteHueSpin, spinHeight, -180, 180, 1, SIGNAL(valueChanged(int)), SLOT(OnPaletteAdjust(int)), true, 0, 0, 0);
+ SetupSpinner(table, this, row, 1, m_PaletteSaturationSpin, spinHeight, -100, 100, 1, SIGNAL(valueChanged(int)), SLOT(OnPaletteAdjust(int)), true, 0, 0, 0);
+ SetupSpinner(table, this, row, 1, m_PaletteBrightnessSpin, spinHeight, -255, 255, 1, SIGNAL(valueChanged(int)), SLOT(OnPaletteAdjust(int)), true, 0, 0, 0);
+ row = 0;
+ SetupSpinner(table, this, row, 3, m_PaletteContrastSpin, spinHeight, -100, 100, 1, SIGNAL(valueChanged(int)), SLOT(OnPaletteAdjust(int)), true, 0, 0, 0);
+ SetupSpinner(table, this, row, 3, m_PaletteBlurSpin, spinHeight, 0, 127, 1, SIGNAL(valueChanged(int)), SLOT(OnPaletteAdjust(int)), true, 0, 0, 0);
+ SetupSpinner(table, this, row, 3, m_PaletteFrequencySpin, spinHeight, 1, 10, 1, SIGNAL(valueChanged(int)), SLOT(OnPaletteAdjust(int)), true, 1, 1, 1);
+ connect(ui.PaletteRandomSelectButton, SIGNAL(clicked(bool)), this, SLOT(OnPaletteRandomSelectButtonClicked(bool)), Qt::QueuedConnection);
+ connect(ui.PaletteRandomAdjustButton, SIGNAL(clicked(bool)), this, SLOT(OnPaletteRandomAdjustButtonClicked(bool)), Qt::QueuedConnection);
+ //Palette editor.
+ connect(ui.PaletteEditorButton, SIGNAL(clicked(bool)), this, SLOT(OnPaletteEditorButtonClicked(bool)), Qt::QueuedConnection);
+ //Preview table.
+ palettePreviewTable->setRowCount(1);
+ palettePreviewTable->setColumnWidth(1, 260);//256 plus small margin on each side.
+ auto previewNameCol = new QTableWidgetItem("");
+ palettePreviewTable->setItem(0, 0, previewNameCol);
+ auto previewPaletteItem = new QTableWidgetItem();
+ palettePreviewTable->setItem(0, 1, previewPaletteItem);
+ connect(ui.PaletteFilterLineEdit, SIGNAL(textChanged(const QString&)), this, SLOT(OnPaletteFilterLineEditTextChanged(const QString&)));
+ connect(ui.PaletteFilterClearButton, SIGNAL(clicked(bool)), this, SLOT(OnPaletteFilterClearButtonClicked(bool)));
+ paletteTable->setColumnWidth(1, 260);//256 plus small margin on each side.
+ paletteTable->horizontalHeader()->setSectionsClickable(true);
+ connect(paletteTable->horizontalHeader(), SIGNAL(sectionClicked(int)), this, SLOT(OnPaletteHeaderSectionClicked(int)), Qt::QueuedConnection);
+ connect(ui.ResetCurvesButton, SIGNAL(clicked(bool)), this, SLOT(OnResetCurvesButtonClicked(bool)), Qt::QueuedConnection);
+ connect(ui.CurvesView, SIGNAL(PointChangedSignal(int, int, const QPointF&)), this, SLOT(OnCurvesPointChanged(int, int, const QPointF&)), Qt::QueuedConnection);
+ connect(ui.CurvesView, SIGNAL(PointAddedSignal(size_t, const QPointF&)), this, SLOT(OnCurvesPointAdded(size_t, const QPointF&)), Qt::QueuedConnection);
+ connect(ui.CurvesView, SIGNAL(PointRemovedSignal(size_t, int)), this, SLOT(OnCurvesPointRemoved(size_t, int)), Qt::QueuedConnection);
+ connect(ui.CurvesAllRadio, SIGNAL(toggled(bool)), this, SLOT(OnCurvesAllRadioButtonToggled(bool)), Qt::QueuedConnection);
+ connect(ui.CurvesRedRadio, SIGNAL(toggled(bool)), this, SLOT(OnCurvesRedRadioButtonToggled(bool)), Qt::QueuedConnection);
+ connect(ui.CurvesGreenRadio, SIGNAL(toggled(bool)), this, SLOT(OnCurvesGreenRadioButtonToggled(bool)), Qt::QueuedConnection);
+ connect(ui.CurvesBlueRadio, SIGNAL(toggled(bool)), this, SLOT(OnCurvesBlueRadioButtonToggled(bool)), Qt::QueuedConnection);
+}
+
+///
+/// Read all palette Xml files in the specified folder and populate the palette list with the contents.
+/// This will clear any previous contents.
+/// Called upon initialization, or controller type change.
+///
+/// The full path to the palette files folder
+/// The number of palettes successfully added
+template
+size_t FractoriumEmberController::InitPaletteList(const QString& s)
+{
+ QDirIterator it(s, QStringList() << "*.xml" << "*.ugr" << "*.gradient" << "*.gradients", QDir::Files, QDirIterator::FollowSymlinks);
+
+ while (it.hasNext())
+ {
+ auto path = it.next();
+ auto qfilename = it.fileName();
+
+ try
+ {
+ if (QFile::exists(path) && m_PaletteList->Add(path.toStdString()))
+ m_Fractorium->ui.PaletteFilenameCombo->addItem(qfilename);
+ }
+ catch (const std::exception& e)
+ {
+ QMessageBox::critical(nullptr, "Palette Parsing Error", QString::fromStdString(e.what()));
+ }
+ catch (const char* e)
+ {
+ QMessageBox::critical(nullptr, "Palette Parsing Error", e);
+ }
+ }
+
+ m_Fractorium->ui.PaletteFilenameCombo->model()->sort(0);
+ return m_PaletteList->Size();
+}
+
+///
+/// Read a palette Xml file and populate the palette table with the contents.
+/// This will clear any previous contents.
+/// Called upon initialization, palette combo index change, and controller type change.
+///
+/// The name of the palette file without the path
+/// True if successful, else false.
+template
+bool FractoriumEmberController::FillPaletteTable(const string& s)
+{
+ if (!s.empty())//This occasionally seems to get called with an empty string for reasons unknown.
+ {
+ auto paletteTable = m_Fractorium->ui.PaletteListTable;
+ m_CurrentPaletteFilePath = s;
+
+ if (::FillPaletteTable(m_CurrentPaletteFilePath, paletteTable, m_PaletteList))
+ {
+ return true;
+ }
+ else
+ {
+ vector errors = m_PaletteList->ErrorReport();
+ m_Fractorium->ErrorReportToQTextEdit(errors, m_Fractorium->ui.InfoFileOpeningTextEdit);
+ m_Fractorium->ShowCritical("Palette Read Error", "Could not load palette file, all images will be black. See info tab for details.");
+ m_PaletteList->ClearErrorReport();
+ }
+ }
+
+ return false;
+}
+
+///
+/// Fill the palette table with the passed in string.
+/// Called when the palette name combo box changes.
+///
+/// The full path to the palette file
+void Fractorium::OnPaletteFilenameComboChanged(const QString& text)
+{
+ auto s = text.toStdString();
+ m_Controller->FillPaletteTable(s);
+ auto fullname = m_Controller->m_PaletteList->GetFullPathFromFilename(s);
+ ui.PaletteFilenameCombo->setToolTip(QString::fromStdString(fullname));
+ ui.PaletteListTable->sortItems(0, m_PaletteSortMode == 0 ? Qt::AscendingOrder : Qt::DescendingOrder);
+}
+
+///
+/// Apply adjustments to the current ember's palette.
+///
+template
+void FractoriumEmberController::ApplyPaletteToEmber()
+{
+ const uint blur = m_Fractorium->m_PaletteBlurSpin->value();
+ const uint freq = m_Fractorium->m_PaletteFrequencySpin->value();
+ const auto sat = m_Fractorium->m_PaletteSaturationSpin->value() / 100.0;
+ const auto brightness = m_Fractorium->m_PaletteBrightnessSpin->value() / 255.0;
+ const auto contrast = double(m_Fractorium->m_PaletteContrastSpin->value() > 0 ? m_Fractorium->m_PaletteContrastSpin->value() * 2.0 : m_Fractorium->m_PaletteContrastSpin->value()) / 100.0;
+ const auto hue = m_Fractorium->m_PaletteHueSpin->value() / 360.0;
+ //Use the temp palette as the base and apply the adjustments gotten from the GUI and save the result in the ember palette.
+ m_TempPalette.MakeAdjustedPalette(m_Ember.m_Palette, m_Fractorium->m_PreviewPaletteRotation, hue, sat, brightness, contrast, blur, freq);
+}
+
+///
+/// Use adjusted palette to update all related GUI controls with new color values.
+/// Resets the rendering process.
+///
+/// The palette to use
+/// Name of the palette
+template
+void FractoriumEmberController::UpdateAdjustedPaletteGUI(Palette& palette)
+{
+ const auto xform = CurrentXform();
+ const auto palettePreviewTable = m_Fractorium->ui.PalettePreviewTable;
+ const auto paletteName = QString::fromStdString(m_Ember.m_Palette.m_Name);
+ auto previewPaletteItem = palettePreviewTable->item(0, 1);
+
+ if (previewPaletteItem)//This can be null if the palette file was moved or corrupted.
+ {
+ //Use the adjusted palette to fill the preview palette control so the user can see the effects of applying the adjustements.
+ vector v = palette.MakeRgbPaletteBlock(PALETTE_CELL_HEIGHT);//Make the palette repeat for PALETTE_CELL_HEIGHT rows.
+ m_FinalPaletteImage = QImage(int(palette.Size()), PALETTE_CELL_HEIGHT, QImage::Format_RGB888);//Create a QImage out of it.
+ memcpy(m_FinalPaletteImage.scanLine(0), v.data(), v.size() * sizeof(v[0]));//Memcpy the data in.
+ QPixmap pixmap(QPixmap::fromImage(m_FinalPaletteImage));//Create a QPixmap out of the QImage.
+ previewPaletteItem->setData(Qt::DecorationRole, pixmap.scaled(QSize(pixmap.width(), palettePreviewTable->rowHeight(0) + 2), Qt::IgnoreAspectRatio, Qt::SmoothTransformation));//Set the pixmap on the palette tab.
+ m_Fractorium->SetPaletteTableItem(&pixmap, m_Fractorium->ui.XformPaletteRefTable, m_Fractorium->m_PaletteRefItem, 0, 0);//Set the palette ref table on the xforms | color tab.
+
+ if (auto previewNameItem = palettePreviewTable->item(0, 0))
+ {
+ previewNameItem->setText(paletteName);//Finally, set the name of the palette to be both the text and the tooltip.
+ previewNameItem->setToolTip(paletteName);
+ }
+ }
+
+ //Update the current xform's color and reset the rendering process.
+ //Update all controls to be safe.
+ if (xform)
+ XformColorIndexChanged(xform->m_ColorX, true, true, true);
+}
+
+///
+/// Apply all adjustments to the selected palette, show it
+/// and assign it to the current ember.
+/// Called when any adjustment spinner is modified.
+/// Resets the rendering process.
+///
+template
+void FractoriumEmberController::PaletteAdjust()
+{
+ Update([&]()
+ {
+ ApplyPaletteToEmber();
+ UpdateAdjustedPaletteGUI(m_Ember.m_Palette);
+ });
+}
+
+void Fractorium::OnPaletteAdjust(int d) { m_Controller->PaletteAdjust(); }
+
+///
+/// Set the passed in palette as the current one,
+/// applying any adjustments previously specified.
+/// Resets the rendering process.
+///
+/// The palette to assign to the temporary palette
+template
+void FractoriumEmberController::SetBasePaletteAndAdjust(const Palette& palette)
+{
+ //The temp palette is assigned the palette read when the file was parsed/saved. The user can apply adjustments on the GUI later.
+ //These adjustments will be applied to the temp palette, then assigned back to m_Ember.m_Palette.
+ m_TempPalette = palette;//Deep copy.
+ ApplyPaletteToEmber();//Copy temp palette to ember palette and apply adjustments.
+ UpdateAdjustedPaletteGUI(m_Ember.m_Palette);//Show the adjusted palette.
+}
+
+///
+/// Set the selected palette as the current one,
+/// applying any adjustments previously specified.
+/// Called when a palette cell is clicked. Unfortunately,
+/// this will get called twice on a double click when moving
+/// from one palette to another. It happens quickly so it shouldn't
+/// be too much of a problem.
+/// Resets the rendering process.
+///
+/// The table row clicked
+/// The table column clicked
+template
+void FractoriumEmberController::PaletteCellClicked(int row, int col)
+{
+ if (const auto palette = m_PaletteList->GetPaletteByFilename(m_CurrentPaletteFilePath, row))
+ SetBasePaletteAndAdjust(*palette);
+}
+
+///
+/// Map the palette in the clicked row index to the index
+/// in the palette list, then pass that index to PaletteCellClicked().
+/// This resolves the case where the sort order of the palette table
+/// is different than the internal order of the palette list.
+///
+/// The table row clicked
+/// The table column clicked, ignored
+void Fractorium::OnPaletteCellClicked(int row, int col)
+{
+ if (const auto item = dynamic_cast(ui.PaletteListTable->item(row, 1)))
+ {
+ const auto index = int(item->Index());
+
+ if (m_PreviousPaletteRow != index)
+ {
+ m_Controller->PaletteCellClicked(index, col);
+ m_PreviousPaletteRow = index;//Save for comparison on next click.
+ }
+ }
+}
+
+///
+/// Called when the mouse has been moved while pressed on the palette preview table.
+/// Computes the difference between where the mouse was clicked and where it is now, then
+/// uses that difference as a rotation value to pass into the palette adjustment.
+/// Updates the palette and resets the rendering process.
+///
+/// The local mouse coordinates relative to the palette preview table
+/// The global mouse coordinates
+void Fractorium::OnPreviewPaletteMouseDragged(const QPointF& local, const QPoint& global)
+{
+ if (m_PreviewPaletteMouseDown)
+ {
+ m_PreviewPaletteRotation = m_PreviewPaletteMouseDownRotation + (global.x() - m_PreviewPaletteMouseDownPosition.x());
+ //qDebug() << "Palette preview table drag reached main window event: " << local.x() << ' ' << local.y() << ", global: " << global.x() << ' ' << global.y() << ", final: " << m_PreviewPaletteRotation;
+ m_Controller->PaletteAdjust();
+ }
+}
+
+///
+/// Called when the mouse has been released over the palette preview table.
+/// Does nothing but set the dragging state to false.
+///
+void Fractorium::OnPreviewPaletteMouseReleased()
+{
+ m_PreviewPaletteMouseDown = false;
+}
+
+///
+/// Sets the palette rotation to zero.
+/// Updates the palette and resets the rendering process.
+///
+/// Ignored
+/// Ignored
+void Fractorium::OnPreviewPaletteCellDoubleClicked(int row, int col)
+{
+ m_PreviewPaletteRotation = m_PreviewPaletteMouseDownRotation = 0;
+ m_PreviewPaletteMouseDown = false;
+ m_Controller->PaletteAdjust();
+}
+
+///
+/// Called when the mouse has been pressed on the palette preview table.
+/// Subsequent mouse movements will compute a rotation value to pass into the palette adjustment, based on the location
+/// of the mouse when this slot is called.
+///
+/// Ignored
+/// Ignored
+void Fractorium::OnPreviewPaletteCellPressed(int row, int col)
+{
+ m_PreviewPaletteMouseDown = true;
+ m_PreviewPaletteMouseDownPosition = QCursor::pos();//Get the global mouse position.
+ m_PreviewPaletteMouseDownRotation = m_PreviewPaletteRotation;
+ //qDebug() << "Mouse down with initial pos: " << m_PreviewPaletteMouseDownPosition.x() << " and initial rotation: " << m_PreviewPaletteMouseDownRotation;
+}
+
+///
+/// Set the selected palette as the current one,
+/// resetting any adjustments previously specified.
+/// Called when a palette cell is double clicked.
+/// Resets the rendering process.
+///
+/// The table row clicked
+/// The table column clicked
+void Fractorium::OnPaletteCellDoubleClicked(int row, int col)
+{
+ ResetPaletteControls();
+ m_PreviousPaletteRow = -1;
+ OnPaletteCellClicked(row, col);
+}
+
+///
+/// Set the selected palette to a randomly selected one,
+/// applying any adjustments previously specified if the checked parameter is true.
+/// Called when the Random Palette button is clicked.
+/// Resets the rendering process.
+///
+/// True to clear the current adjustments, else leave current adjustments and apply them to the newly selected palette.
+void Fractorium::OnPaletteRandomSelectButtonClicked(bool checked)
+{
+ uint i = 0;
+ const auto rowCount = ui.PaletteListTable->rowCount();
+
+ if (rowCount > 1)//If only one palette in the current palette file, just use it.
+ while (((i = QTIsaac::LockedRand(rowCount)) == uint(m_PreviousPaletteRow)) || i >= static_cast(rowCount));
+
+ if (checked)
+ OnPaletteCellDoubleClicked(i, 1);//Will clear the adjustments.
+ else
+ OnPaletteCellClicked(i, 1);
+}
+
+///
+/// Apply random adjustments to the selected palette.
+/// Called when the Random Adjustment button is clicked.
+/// Resets the rendering process.
+///
+void Fractorium::OnPaletteRandomAdjustButtonClicked(bool checked)
+{
+ m_PaletteHueSpin->setValue(-180 + QTIsaac::LockedRand(361));
+ m_PaletteSaturationSpin->setValue(-50 + QTIsaac::LockedRand(101));//Full range of these leads to bad palettes, so clamp range.
+ m_PaletteBrightnessSpin->setValue(-50 + QTIsaac::LockedRand(101));
+ m_PaletteContrastSpin->setValue(-50 + QTIsaac::LockedRand(101));
+
+ //Doing frequency and blur together gives bad palettes that are just a solid color.
+ if (QTIsaac::LockedRandBit())
+ {
+ m_PaletteBlurSpin->setValue(QTIsaac::LockedRand(21));
+ m_PaletteFrequencySpin->setValue(1);
+ }
+ else
+ {
+ m_PaletteBlurSpin->setValue(0);
+ m_PaletteFrequencySpin->setValue(1 + QTIsaac::LockedRand(10));
+ }
+
+ OnPaletteAdjust(0);
+}
+
+///
+/// Open the palette editor dialog.
+/// Called when the palette editor button is clicked.
+///
+template
+void FractoriumEmberController::PaletteEditorButtonClicked()
+{
+ size_t i = 0;
+ const auto ed = m_Fractorium->m_PaletteEditor.get();
+ map colorIndices;
+ const auto forceFinal = m_Fractorium->HaveFinal();
+ m_PreviousTempPalette = m_TempPalette; // it's necessary because m_TempPalette is changed when the user make changes in palette editor
+ ed->SetPalette(m_TempPalette);
+
+ while (auto xform = m_Ember.GetTotalXform(i, forceFinal))
+ colorIndices[i++] = xform->m_ColorX;
+
+ ed->SetColorIndices(colorIndices);
+ ed->SetPreviousColorIndices(colorIndices); // also necessary because the colors are changed in palette editor
+ ed->SetPaletteFile(m_CurrentPaletteFilePath);
+#ifdef __linux__
+ ed->show();
+#else
+ SyncPalette(ed->exec() == QDialog::Accepted);
+#endif
+}
+
+///
+/// Slot called when the palette editor changes the palette and the Sync checkbox is checked.
+///
+bool Fractorium::PaletteChanged()
+{
+ return m_PaletteChanged;
+}
+
+///
+/// Open the palette editor dialog.
+/// This creates the palette editor dialog if it has not been created at least once.
+/// Called when the palette editor button is clicked.
+///
+/// Ignored
+void Fractorium::OnPaletteEditorButtonClicked(bool checked)
+{
+ if (!m_PaletteEditor.get())
+ {
+ m_PaletteEditor = std::make_unique(this);
+ connect(m_PaletteEditor.get(), SIGNAL(PaletteChanged()), this, SLOT(OnPaletteEditorColorChanged()), Qt::QueuedConnection);
+ connect(m_PaletteEditor.get(), SIGNAL(PaletteFileChanged()), this, SLOT(OnPaletteEditorFileChanged()), Qt::QueuedConnection);
+ connect(m_PaletteEditor.get(), SIGNAL(ColorIndexChanged(size_t, float)), this, SLOT(OnPaletteEditorColorIndexChanged(size_t, float)), Qt::QueuedConnection);
+#ifdef __linux__
+ connect(m_PaletteEditor.get(), SIGNAL(finished(int)), this, SLOT(OnPaletteEditorFinished(int)), Qt::QueuedConnection);
+#endif
+ }
+
+ m_PaletteChanged = false;
+ m_PaletteFileChanged = false;
+ m_Controller->PaletteEditorButtonClicked();
+}
+
+///
+/// Slot called when palette editor window is closed.
+///
+template
+void FractoriumEmberController::SyncPalette(bool accepted)
+{
+ const auto ed = m_Fractorium->m_PaletteEditor.get();
+ Palette edPal;
+ Palette prevPal = m_PreviousTempPalette;
+ map colorIndices;
+ const auto forceFinal = m_Fractorium->HaveFinal();
+
+ if (accepted)
+ {
+ //Copy all just to be safe, because they may or may not have synced.
+ colorIndices = ed->GetColorIndices();
+
+ for (auto& index : colorIndices)
+ if (auto xform = m_Ember.GetTotalXform(index.first, forceFinal))
+ xform->m_ColorX = index.second;
+
+ edPal = ed->GetPalette(static_cast(prevPal.Size()));
+ SetBasePaletteAndAdjust(edPal);//This will take care of updating the color index controls.
+
+ if (edPal.m_Filename.get() && !edPal.m_Filename->empty())
+ m_Fractorium->SetPaletteFileComboIndex(*edPal.m_Filename);
+ }
+ else if (m_Fractorium->PaletteChanged())//They clicked cancel, but synced at least once, restore the previous palette.
+ {
+ colorIndices = ed->GetPreviousColorIndices();
+
+ for (auto& index : colorIndices)
+ if (auto xform = m_Ember.GetTotalXform(index.first, forceFinal))
+ xform->m_ColorX = index.second;
+
+ SetBasePaletteAndAdjust(prevPal);//This will take care of updating the color index controls.
+ }
+
+ //Whether the current palette file was changed or not, if it's modifiable then reload it just to be safe (even though it might be overkill).
+ if (m_PaletteList->IsModifiable(m_CurrentPaletteFilePath))
+ m_Fractorium->OnPaletteFilenameComboChanged(QString::fromStdString(m_CurrentPaletteFilePath));
+}
+
+///
+/// Slot called every time a color is changed in the palette editor.
+///
+template
+void FractoriumEmberController::PaletteEditorColorChanged()
+{
+ SetBasePaletteAndAdjust(m_Fractorium->m_PaletteEditor->GetPalette(static_cast(m_TempPalette.Size())));
+}
+
+void Fractorium::OnPaletteEditorColorChanged()
+{
+ m_PaletteChanged = true;
+ m_Controller->PaletteEditorColorChanged();
+}
+
+///
+/// Slot called every time a palette file is changed in the palette editor.
+///
+void Fractorium::OnPaletteEditorFileChanged()
+{
+ m_PaletteFileChanged = true;
+}
+
+///
+/// Slot called every time an xform color index is changed in the palette editor.
+/// If a special value of size_t max is passed for index, it means update all color indices.
+///
+/// The index of the xform whose color index has been changed. Special value of size_t max to update all
+/// The value of the color index
+void Fractorium::OnPaletteEditorColorIndexChanged(size_t index, float value)
+{
+ if (index == std::numeric_limits::max())//Update all in this case.
+ {
+ auto indices = m_PaletteEditor->GetColorIndices();
+
+ for (auto& it : indices)
+ OnXformColorIndexChanged(it.second, true, true, true, eXformUpdate::UPDATE_SPECIFIC, it.first);
+ }
+ else//Only update the xform index that was selected and dragged inside of the palette editor.
+ OnXformColorIndexChanged(value, true, true, true, eXformUpdate::UPDATE_SPECIFIC, index);
+}
+
+/// Slot called after EditPallete is closed.
+///
+/// Cancel/OK action
+void Fractorium::OnPaletteEditorFinished(int result)
+{
+ m_Controller->SyncPalette(result == QDialog::Accepted);
+}
+
+///
+/// Apply the text in the palette filter text box to only show palettes whose names
+/// contain the substring.
+/// Called when the user types in the palette filter text box.
+///
+/// The text to filter on
+void Fractorium::OnPaletteFilterLineEditTextChanged(const QString& text)
+{
+ auto table = ui.PaletteListTable;
+ table->setUpdatesEnabled(false);
+
+ for (int i = 0; i < table->rowCount(); i++)
+ {
+ if (auto item = table->item(i, 0))
+ {
+ if (!item->text().contains(text, Qt::CaseInsensitive))
+ table->hideRow(i);
+ else
+ table->showRow(i);
+ }
+ }
+
+ ui.PaletteListTable->sortItems(0, m_PaletteSortMode == 0 ? Qt::AscendingOrder : Qt::DescendingOrder);//Must re-sort every time the filter changes.
+ table->setUpdatesEnabled(true);
+}
+
+///
+/// Clear the palette name filter, which will display all palettes.
+/// Called when clear palette filter button is clicked.
+///
+/// Ignored
+void Fractorium::OnPaletteFilterClearButtonClicked(bool checked)
+{
+ ui.PaletteFilterLineEdit->clear();
+}
+
+///
+/// Change the sorting to be either ascending or descending.
+/// Called when user clicks the table headers.
+///
+/// Column index of the header clicked, ignored.
+void Fractorium::OnPaletteHeaderSectionClicked(int col)
+{
+ m_PaletteSortMode = !m_PaletteSortMode;
+ ui.PaletteListTable->sortItems(0, m_PaletteSortMode == 0 ? Qt::AscendingOrder : Qt::DescendingOrder);
+}
+
+///
+/// Reset the palette controls.
+/// Usually in response to a palette cell double click.
+///
+void Fractorium::ResetPaletteControls()
+{
+ m_PreviewPaletteRotation = m_PreviewPaletteMouseDownRotation = 0;
+ m_PaletteHueSpin->SetValueStealth(0);
+ m_PaletteSaturationSpin->SetValueStealth(0);
+ m_PaletteBrightnessSpin->SetValueStealth(0);
+ m_PaletteContrastSpin->SetValueStealth(0);
+ m_PaletteBlurSpin->SetValueStealth(0);
+ m_PaletteFrequencySpin->SetValueStealth(1);
+}
+
+///
+/// Set the index of the palette file combo box.
+/// This is for display purposes only so the user can see which file, if any,
+/// the current palette came from.
+/// For embedded palettes with no filename, this will have no effect.
+///
+/// The string to set the index to
+void Fractorium::SetPaletteFileComboIndex(const string& filename)
+{
+ if (!filename.empty())
+ ui.PaletteFilenameCombo->setCurrentText(QFileInfo(QString::fromStdString(filename)).fileName());
+}
+
+///
+/// Reset the color curve values for the selected curve in the current ember to their default state and also update the curves control.
+/// Called when ResetCurvesButton is clicked.
+/// Note if they click Reset Curves when the ctrl is pressed, then it clears all curves.
+/// Resets the rendering process at either ACCUM_ONLY by default, or FILTER_AND_ACCUM when using early clip.
+///
+/// The index of the curve to be cleared, 0 to clear all.
+template
+void FractoriumEmberController::ClearColorCurves(int i)
+{
+ UpdateAll([&](Ember& ember, bool isMain)
+ {
+ if (i < 0)
+ ember.m_Curves.Init();
+ else
+ ember.m_Curves.Init(i);
+ }, true, m_Renderer->EarlyClip() ? eProcessAction::FILTER_AND_ACCUM : eProcessAction::ACCUM_ONLY, m_Fractorium->ApplyAll());
+ FillCurvesControl();
+}
+
+void Fractorium::OnResetCurvesButtonClicked(bool checked)
+{
+ if (!QGuiApplication::keyboardModifiers().testFlag(Qt::ControlModifier))
+ {
+ if (ui.CurvesAllRadio->isChecked())
+ m_Controller->ClearColorCurves(0);
+ else if (ui.CurvesRedRadio->isChecked())
+ m_Controller->ClearColorCurves(1);
+ else if (ui.CurvesGreenRadio->isChecked())
+ m_Controller->ClearColorCurves(2);
+ else if (ui.CurvesBlueRadio->isChecked())
+ m_Controller->ClearColorCurves(3);
+ else
+ m_Controller->ClearColorCurves(0);
+ }
+ else
+ {
+ m_Controller->ClearColorCurves(-1);
+ }
+}
+
+///
+/// Set the coordinate of the curve point.
+/// Called when the position of any of the points in the curves editor is is changed.
+/// Resets the rendering process at either ACCUM_ONLY by default, or FILTER_AND_ACCUM when using early clip.
+///
+/// The curve index, 0-3/
+/// The point index within the selected curve, 1-2.
+/// The new coordinate of the point in terms of the curves control rect.
+template
+void FractoriumEmberController::ColorCurveChanged(int curveIndex, int pointIndex, const QPointF& point)
+{
+ Update([&]
+ {
+ m_Ember.m_Curves.m_Points[curveIndex][pointIndex].x = point.x();
+ m_Ember.m_Curves.m_Points[curveIndex][pointIndex].y = point.y();
+ }, true, m_Renderer->EarlyClip() ? eProcessAction::FILTER_AND_ACCUM : eProcessAction::ACCUM_ONLY);
+}
+
+void Fractorium::OnCurvesPointChanged(int curveIndex, int pointIndex, const QPointF& point) { m_Controller->ColorCurveChanged(curveIndex, pointIndex, point); }
+
+///
+/// Remove curve point.
+/// Called when right clicking on a color curve point.
+/// Resets the rendering process at either ACCUM_ONLY by default, or FILTER_AND_ACCUM when using early clip.
+///
+/// The curve index./
+/// The point index within the selected curve.
+template
+void FractoriumEmberController::ColorCurvesPointRemoved(size_t curveIndex, int pointIndex)
+{
+ Update([&]
+ {
+ if (m_Ember.m_Curves.m_Points[curveIndex].size() > 2)
+ {
+ m_Ember.m_Curves.m_Points[curveIndex].erase(m_Ember.m_Curves.m_Points[curveIndex].begin() + pointIndex);
+ std::sort(m_Ember.m_Curves.m_Points[curveIndex].begin(), m_Ember.m_Curves.m_Points[curveIndex].end(), [&](auto & lhs, auto & rhs) { return lhs.x < rhs.x; });
+ }
+ }, true, m_Renderer->EarlyClip() ? eProcessAction::FILTER_AND_ACCUM : eProcessAction::ACCUM_ONLY);
+ FillCurvesControl();
+}
+
+void Fractorium::OnCurvesPointRemoved(size_t curveIndex, int pointIndex) { m_Controller->ColorCurvesPointRemoved(curveIndex, pointIndex); }
+
+///
+/// Add a curve point.
+/// Called when clicking in between points on a color curve.
+/// Resets the rendering process at either ACCUM_ONLY by default, or FILTER_AND_ACCUM when using early clip.
+///
+/// The curve index./
+/// The point to add to the selected curve.
+template
+void FractoriumEmberController::ColorCurvesPointAdded(size_t curveIndex, const QPointF& point)
+{
+ Update([&]
+ {
+ m_Ember.m_Curves.m_Points[curveIndex].push_back({ point.x(), point.y() });
+ std::sort(m_Ember.m_Curves.m_Points[curveIndex].begin(), m_Ember.m_Curves.m_Points[curveIndex].end(), [&](auto & lhs, auto & rhs) { return lhs.x < rhs.x; });
+ }, true, m_Renderer->EarlyClip() ? eProcessAction::FILTER_AND_ACCUM : eProcessAction::ACCUM_ONLY);
+ FillCurvesControl();
+}
+
+void Fractorium::OnCurvesPointAdded(size_t curveIndex, const QPointF& point) { m_Controller->ColorCurvesPointAdded(curveIndex, point); }
+
+///
+/// Set the top most points in the curves control, which makes it easier to
+/// select a point by putting it on top of all the others.
+/// Called when the any of the curve color radio buttons are toggled.
+///
+/// Ignored
+void Fractorium::OnCurvesAllRadioButtonToggled(bool checked) { if (checked) ui.CurvesView->SetTop(CurveIndex::ALL); }
+void Fractorium::OnCurvesRedRadioButtonToggled(bool checked) { if (checked) ui.CurvesView->SetTop(CurveIndex::RED); }
+void Fractorium::OnCurvesGreenRadioButtonToggled(bool checked) { if (checked) ui.CurvesView->SetTop(CurveIndex::GREEN); }
+void Fractorium::OnCurvesBlueRadioButtonToggled(bool checked) { if (checked) ui.CurvesView->SetTop(CurveIndex::BLUE); }
+
+///
+/// Set the points in the curves control to the values of the curve points in the current ember.
+///
+template
+void FractoriumEmberController::FillCurvesControl()
+{
+ m_Fractorium->ui.CurvesView->blockSignals(true);
+ m_Fractorium->ui.CurvesView->Set(m_Ember.m_Curves);
+ m_Fractorium->ui.CurvesView->blockSignals(false);
+ m_Fractorium->ui.CurvesView->update();
+}
+
+template class FractoriumEmberController;
+
+#ifdef DO_DOUBLE
+ template class FractoriumEmberController;
+#endif
+>>>>>>> a4bfffaa3f55362a86915c700186ee3ed03b90fa