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 + + + + + + + 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