fractorium/Source/Fractorium/FractoriumRender.cpp
2024-01-30 14:24:56 -07:00

869 lines
34 KiB
C++

#include "FractoriumPch.h"
#include "Fractorium.h"
/// <summary>
/// Return whether the render timer is running.
/// </summary>
/// <returns>True if running, else false.</returns>
bool FractoriumEmberControllerBase::RenderTimerRunning()
{
return m_RenderTimer->isActive();
}
/// <summary>
/// Start the render timer.
/// If a renderer has not been created yet, it will be created from the options.
/// </summary>
void FractoriumEmberControllerBase::StartRenderTimer()
{
UpdateRender();
m_RenderTimer->start();
m_RenderElapsedTimer.Tic();
}
/// <summary>
/// Start the render timer after a short delay.
/// If the timer is already running, stop it first.
/// This is useful for stopping and restarting the render
/// process in response to things like a window resize.
/// </summary>
void FractoriumEmberControllerBase::DelayedStartRenderTimer()
{
DeleteRenderer();
m_RenderRestartTimer->setSingleShot(true);
m_RenderRestartTimer->start(300);//Will stop the timer if it's already running, and start again.
}
/// <summary>
/// Stop the render timer and abort the rendering process.
/// Optionally block until stopping is complete.
/// </summary>
/// <param name="wait">True to block, else false.</param>
void FractoriumEmberControllerBase::StopRenderTimer(bool wait)
{
m_RenderTimer->stop();
if (m_Renderer.get())
m_Renderer->Abort();
if (wait)
{
while (m_Rendering || RenderTimerRunning() || (Renderer() && (!m_Renderer->Aborted() || m_Renderer->InRender())))
QApplication::processEvents();
}
}
/// <summary>
/// Stop all timers, rendering and drawing and block until they are done.
/// </summary>
void FractoriumEmberControllerBase::Shutdown()
{
StopRenderTimer(true);
ClearFinalImages();
while (m_Fractorium->ui.GLDisplay->Drawing())
QApplication::processEvents();
}
/// <summary>
/// Clear the output image buffers.
/// </summary>
void FractoriumEmberControllerBase::ClearFinalImages()
{
Memset(m_FinalImage);
//Unsure if we should also call RendererCL::ClearFinal() as well. At the moment it seems unnecessary.
}
/// <summary>
/// Update the state of the renderer.
/// Upon changing values, some intelligence is used to avoid blindly restarting the
/// entire iteration proceess every time a value changes. This is because some values don't affect the
/// iteration, and only affect filtering and final accumulation. They are broken into three categories:
/// 1) Restart the entire process.
/// 2) Log/density filter, then final accum.
/// 3) Final accum only.
/// 4) Continue iterating.
/// </summary>
/// <param name="action">The action to take</param>
void FractoriumEmberControllerBase::UpdateRender(eProcessAction action)
{
AddProcessAction(action);
m_RenderElapsedTimer.Tic();
}
/// <summary>
/// Call Shutdown() then delete the renderer and clear the textures in the output window if there is one.
/// Note the name is somewhat misleading because a new empty renderer is actually created as a placeholder.
/// This is that the program won't crash if the user adjusts any of the controls while the renderer is shut down.
/// </summary>
template <typename T>
void FractoriumEmberController<T>::DeleteRenderer()
{
Shutdown();
m_Renderer = make_unique<EmberNs::Renderer<T, float>>();
if (GLController())
GLController()->ClearWindow();
}
/// <summary>
/// Save the current render results to a file.
/// This could benefit from QImageWriter, however its compression capabilities are
/// severely lacking. A Png file comes out larger than a bitmap, so instead use the
/// Png and Jpg wrapper functions from the command line programs.
/// This will embed the id, url and nick fields from the options in the image comments.
/// </summary>
/// <param name="filename">The full path and filename</param>
/// <param name="comments">The comments to save in the png, jpg or exr</param>
/// <param name="pixels">The buffer containing the pixels</param>
/// <param name="width">The width in pixels of the image</param>
/// <param name="height">The height in pixels of the image</param>
/// <param name="png16Bit">Whether to use 16 bits per channel per pixel when saving as Png/32-bits per channel when saving as Exr.</param>
/// <param name="transparency">Whether to use alpha when saving as Png or Exr.</param>
bool FractoriumEmberControllerBase::SaveCurrentRender(const QString& filename, const EmberImageComments& comments, vector<v4F>& pixels, size_t width, size_t height, bool png16Bit, bool transparency)
{
bool ret = false;
if (filename != "")
{
const auto size = width * height;
auto settings = m_Fractorium->m_Settings;
QFileInfo fileInfo(filename);
QString suffix = fileInfo.suffix();
string s = filename.toStdString();
string id = settings->Id().toStdString();
string url = settings->Url().toStdString();
string nick = settings->Nick().toStdString();
//Ensure dimensions are valid.
if (pixels.size() < size)
{
m_Fractorium->ShowCritical("Save Failed", "Dimensions didn't match, not saving.", true);
return ret;
}
auto data = pixels.data();
if (suffix.endsWith("bmp", Qt::CaseInsensitive) || suffix.endsWith("jpg", Qt::CaseInsensitive))
{
vector<unsigned char> rgb8Image(size * 3);
Rgba32ToRgb8(data, rgb8Image.data(), width, height);
if (suffix.endsWith("bmp", Qt::CaseInsensitive))
ret = WriteBmp(s.c_str(), rgb8Image.data(), width, height);
else if (suffix.endsWith("jpg", Qt::CaseInsensitive))
ret = WriteJpeg(s.c_str(), rgb8Image.data(), width, height, 100, true, comments, id, url, nick);
}
else if (suffix.endsWith("png", Qt::CaseInsensitive))
{
if (!png16Bit)
{
vector<unsigned char> rgba8Image(size * 4);
Rgba32ToRgba8(data, rgba8Image.data(), width, height, transparency);
ret = WritePng(s.c_str(), rgba8Image.data(), width, height, 1, true, comments, id, url, nick);
}
else
{
vector<glm::uint16> rgba16Image(size * 4);
Rgba32ToRgba16(data, rgba16Image.data(), width, height, transparency);
ret = WritePng(s.c_str(), (unsigned char*)rgba16Image.data(), width, height, 2, true, comments, id, url, nick);
}
}
else if (suffix.endsWith("exr", Qt::CaseInsensitive))
{
if (!png16Bit)//Repurpose this for EXR 32-bit.
{
vector<Rgba> rgba32Image(size);
Rgba32ToRgbaExr(data, rgba32Image.data(), width, height, transparency);
ret = WriteExr16(s.c_str(), rgba32Image.data(), width, height, true, comments, id, url, nick);
}
else
{
vector<float> r(size);
vector<float> g(size);
vector<float> b(size);
vector<float> a(size);
Rgba32ToRgba32Exr(data, r.data(), g.data(), b.data(), a.data(), width, height, transparency);
ret = WriteExr32(s.c_str(), r.data(), g.data(), b.data(), a.data(), width, height, true, comments, id, url, nick);
}
}
else
{
m_Fractorium->ShowCritical("Save Failed", "Unrecognized format " + suffix + ", not saving.", true);
return ret;
}
if (ret)
settings->SaveFolder(fileInfo.canonicalPath());
else
m_Fractorium->ShowCritical("Save Failed", "Could not save file, try saving to a different folder.", true);
}
return ret;
}
/// <summary>
/// Add a process action to the list of actions to take.
/// Called in response to the user changing something on the GUI.
/// </summary>
/// <param name="action">The action for the renderer to take</param>
void FractoriumEmberControllerBase::AddProcessAction(eProcessAction action)
{
rlg l(m_Cs);
m_ProcessActions.push_back(action);
if (m_Renderer.get())
m_Renderer->Abort();
}
/// <summary>
/// Condense and clear the process actions into a single action and return.
/// Many actions may be specified, but only the one requiring the greatest amount
/// of processing matters. Extract and return the greatest and clear the vector.
/// </summary>
/// <returns>The most significant processing action desired</returns>
eProcessAction FractoriumEmberControllerBase::CondenseAndClearProcessActions()
{
rlg l(m_Cs);
auto action = eProcessAction::NOTHING;
for (auto a : m_ProcessActions)
if (a > action)
action = a;
m_ProcessActions.clear();
return action;
}
/// <summary>
/// Render progress callback function to update progress bar.
/// </summary>
/// <param name="ember">The ember currently being rendered</param>
/// <param name="foo">An extra dummy parameter</param>
/// <param name="fraction">The progress fraction from 0-100</param>
/// <param name="stage">The stage of iteration. 1 is iterating, 2 is density filtering, 2 is final accumulation.</param>
/// <param name="etaMs">The estimated milliseconds to completion of the current stage</param>
/// <returns>0 if the user has changed anything on the GUI, else 1 to continue rendering.</returns>
template <typename T>
int FractoriumEmberController<T>::ProgressFunc(Ember<T>& ember, void* foo, double fraction, int stage, double etaMs)
{
QString status;
QMetaObject::invokeMethod(m_Fractorium->m_ProgressBar, "setValue", Qt::QueuedConnection, Q_ARG(const int, int(fraction)));//Only really applies to iter and filter, because final accum only gives progress 0 and 100.
if (stage == 0)
status = "Iterating";
else if (stage == 1)
status = "Density Filtering";
else if (stage == 2)
status = "Spatial Filtering + Final Accumulation";
QMetaObject::invokeMethod(m_Fractorium->m_RenderStatusLabel, "setText", Qt::QueuedConnection, Q_ARG(const QString&, status));
return m_ProcessActions.empty() ? 1 : 0;//If they've done anything, abort.
}
/// <summary>
/// Clear the undo list as well as the undo/redo index and state.
/// </summary>
template <typename T>
void FractoriumEmberController<T>::ClearUndo()
{
m_UndoIndex = 0;
m_UndoList.clear();
m_EditState = eEditUndoState::REGULAR_EDIT;
m_LastEditWasUndoRedo = false;
m_Fractorium->ui.ActionUndo->setEnabled(false);
m_Fractorium->ui.ActionRedo->setEnabled(false);
}
/// <summary>
/// The hierarchy/order of sizes is like so:
/// Ember
/// GL Widget
/// Texture (passed to RendererCL)
/// Viewport
/// Since this uses m_GL->SizesMatch(), which uses the renderer's dimensions, this
/// must be called after the renderer has set the current ember.
/// </summary>
/// <returns>True if dimensions had to be resized due to a mismatch, else false.</returns>
template <typename T>
bool FractoriumEmberController<T>::SyncSizes()
{
bool changed = false;
auto gl = m_Fractorium->ui.GLDisplay;
RendererCL<T, float>* rendererCL = nullptr;
if (gl && !m_GLController->SizesMatch())
{
m_GLController->ClearWindow();
gl->SetDimensions(static_cast<int>(m_Ember.m_FinalRasW), static_cast<int>(m_Ember.m_FinalRasH));
gl->Allocate();
gl->SetViewport();
if (m_Renderer->RendererType() == eRendererType::OPENCL_RENDERER && (rendererCL = dynamic_cast<RendererCL<T, float>*>(m_Renderer.get())))
rendererCL->SetOutputTexture(gl->OutputTexID());
m_Fractorium->CenterScrollbars();
changed = true;
}
return changed;
}
/// <summary>
/// The main rendering function.
/// Called whenever the event loop is idle.
/// </summary>
/// <returns>True if nothing went wrong, else false.</returns>
template <typename T>
bool FractoriumEmberController<T>::Render()
{
if (!m_Renderer.get())
return false;
m_Rendering = true;
bool success = true;
auto gl = m_Fractorium->ui.GLDisplay;
RendererCL<T, float>* rendererCL = nullptr;
eProcessAction qualityAction = eProcessAction::NOTHING, action = eProcessAction::NOTHING;
//Quality is the only parameter we update inside the timer.
//This is to allow the user to rapidly increase the quality spinner
//without fully resetting the render. Instead, it will just keep iterating
//where it last left off in response to an increase.
T d = static_cast<T>(m_Fractorium->m_QualitySpin->value());
if (d < m_Ember.m_Quality)//Full restart if quality decreased.
{
m_Ember.m_Quality = d;
qualityAction = eProcessAction::FULL_RENDER;
}
else if (d > m_Ember.m_Quality && ProcessState() == eProcessState::ACCUM_DONE)//If quality increased, keep iterating after current render finishes.
{
m_Ember.m_Quality = d;
qualityAction = eProcessAction::KEEP_ITERATING;
}
else if (IsClose(d, m_Ember.m_Quality))
qualityAction = eProcessAction::NOTHING;
if (qualityAction == eProcessAction::FULL_RENDER)
Update([&] {});//Stop the current render, a full restart is needed.
else if (qualityAction == eProcessAction::KEEP_ITERATING)
m_ProcessActions.push_back(qualityAction);//Special, direct call to avoid resetting the render inside Update() because only KEEP_ITERATING is needed.
action = CondenseAndClearProcessActions();//Combine with all other previously requested actions.
if (m_Renderer->RendererType() == eRendererType::OPENCL_RENDERER)
rendererCL = dynamic_cast<RendererCL<T, float>*>(m_Renderer.get());
//Force temporal samples to always be 1. Perhaps change later when animation is implemented.
m_Ember.m_TemporalSamples = 1;
//Take care of solo xforms and set the current ember and action.
if (action != eProcessAction::NOTHING)
{
size_t i = 0;
const auto solo = m_Ember.m_Solo;
const bool forceFinal = m_Fractorium->HaveFinal();
if (solo != -1)
{
m_TempOpacities.resize(m_Ember.TotalXformCount(forceFinal));
while (auto xform = m_Ember.GetTotalXform(i))
{
m_TempOpacities[i] = xform->m_Opacity;
xform->m_Opacity = i++ == solo ? 1 : 0;
}
}
i = 0;
m_Renderer->SetEmber(m_Ember, action, true);
if (solo != -1)
while (auto xform = m_Ember.GetTotalXform(i, forceFinal))
xform->m_Opacity = m_TempOpacities[i++];
}
//Ensure sizes are equal and if not, update dimensions.
if (SyncSizes())
{
action = eProcessAction::FULL_RENDER;
return true;
}
//Determining if a completely new rendering process is being started.
const auto iterBegin = ProcessState() == eProcessState::NONE;
if (iterBegin)
{
if (m_Renderer->RendererType() == eRendererType::CPU_RENDERER)
m_SubBatchCount = m_Fractorium->m_Settings->CpuSubBatch();
else if (m_Renderer->RendererType() == eRendererType::OPENCL_RENDERER)
m_SubBatchCount = m_Fractorium->m_Settings->OpenCLSubBatch();
m_Fractorium->m_ProgressBar->setValue(0);
const QString status = "Starting";
QMetaObject::invokeMethod(m_Fractorium->m_RenderStatusLabel, "setText", Qt::QueuedConnection, Q_ARG(const QString&, status));
}
//If the rendering process hasn't finished, render with the current specified action.
if (ProcessState() != eProcessState::ACCUM_DONE)
{
//if (m_Renderer->Run(m_FinalImage, 0) == RENDER_OK)//Full, non-incremental render for debugging.
const bool update = iterBegin || m_Fractorium->m_Settings->ContinuousUpdate();
eRenderStatus result;
if ((result = m_Renderer->Run(m_FinalImage, 0, m_SubBatchCount, update)) == eRenderStatus::RENDER_OK)//Force output on iterBegin or if the settings specify to always do it.
{
//The amount to increment sub batch while rendering proceeds is purely empirical.
//Change later if better values can be derived/observed.
if (m_Renderer->RendererType() == eRendererType::OPENCL_RENDERER)
{
if (m_SubBatchCount < (4 * m_Devices.size()))//More than 4 with OpenCL gives a sluggish UI.
m_SubBatchCount += m_Devices.size();
}
else
{
if (m_SubBatchCount < 5)
m_SubBatchCount++;
else if (m_SubBatchCount < 105)//More than 105 with CPU gives a sluggish UI.
m_SubBatchCount += 25;
}
//Rendering has finished, update final stats.
if (ProcessState() == eProcessState::ACCUM_DONE)
{
const auto stats = m_Renderer->Stats();
auto iters = ToString<qulonglong>(stats.m_Iters);
auto scaledQuality = ToString(static_cast<qulonglong>(m_Renderer->ScaledQuality()));
//auto renderTime = m_RenderElapsedTimer.Format(m_RenderElapsedTimer.Toc());
auto renderTime = m_RenderElapsedTimer.Format(stats.m_RenderMs);
m_Fractorium->m_ProgressBar->setValue(100);
//Only certain stats can be reported with OpenCL.
if (m_Renderer->RendererType() == eRendererType::OPENCL_RENDERER)
{
const QString status = "Iters: " + iters + ". Scaled quality: " + scaledQuality + ". Total time: " + QString::fromStdString(renderTime) + ".";
QMetaObject::invokeMethod(m_Fractorium->m_RenderStatusLabel, "setText", Qt::QueuedConnection, Q_ARG(const QString&, status));
}
else
{
const auto percent = static_cast<double>(stats.m_Badvals) / static_cast<double>(stats.m_Iters);
auto badVals = ToString<qulonglong>(stats.m_Badvals);
auto badPercent = QLocale::system().toString(percent * 100, 'f', 2);
const QString status = "Iters: " + iters + ". Scaled quality: " + scaledQuality + ". Bad values: " + badVals + " (" + badPercent + "%). Total time: " + QString::fromStdString(renderTime) + ".";
QMetaObject::invokeMethod(m_Fractorium->m_RenderStatusLabel, "setText", Qt::QueuedConnection, Q_ARG(const QString&, status));
}
if (m_LastEditWasUndoRedo && (m_UndoIndex == m_UndoList.size() - 1))//Traversing through undo list, reached the end, so put back in regular edit mode.
{
m_EditState = eEditUndoState::REGULAR_EDIT;
}
else if (m_EditState == eEditUndoState::REGULAR_EDIT)//Regular edit, just add to the end of the undo list.
{
const auto btn = QApplication::mouseButtons();
if ((action == eProcessAction::ACCUM_ONLY || action == eProcessAction::FILTER_AND_ACCUM) ||
(!btn.testFlag(Qt::LeftButton) && !btn.testFlag(Qt::RightButton) && !btn.testFlag(Qt::MiddleButton)))
{
m_UndoList.push_back(m_Ember);
m_UndoIndex = m_UndoList.size() - 1;
m_Fractorium->ui.ActionUndo->setEnabled(m_UndoList.size() > 1);
m_Fractorium->ui.ActionRedo->setEnabled(false);
if (m_UndoList.size() >= UNDO_SIZE)
m_UndoList.pop_front();
}
//else
// qDebug() << "Mouse was down, not adding to undo list.";
}
else if (!m_LastEditWasUndoRedo &&
m_UndoList.size() &&
m_UndoIndex < m_UndoList.size() - 1)//They were anywhere but the end of the undo list, then did a manual edit, so clear the undo list.
{
Ember<T> ember(m_UndoList[m_UndoIndex]);
ClearUndo();
m_UndoList.push_back(ember);
m_UndoList.push_back(m_Ember);
m_UndoIndex = m_UndoList.size() - 1;
m_Fractorium->ui.ActionUndo->setEnabled(true);
m_Fractorium->ui.ActionRedo->setEnabled(false);
}
m_LastEditWasUndoRedo = false;
m_Fractorium->UpdateHistogramBounds();//Mostly of engineering interest.
FillSummary();//Only update summary on render completion since it's not the type of thing the user needs real-time updates on.
}
//Update the GL window on start or continuous rendering because the output will be forced.
//Update it on finish because the rendering process is completely done.
if (update || ProcessState() == eProcessState::ACCUM_DONE)
{
gl->update();//Queue update.
if (ProcessState() == eProcessState::ACCUM_DONE)
SaveCurrentToOpenedFile();//Will not save if the previews are still rendering.
//Uncomment for debugging kernel build and execution errors.
//m_Fractorium->ui.InfoRenderingTextEdit->setText(QString::fromStdString(m_Fractorium->m_Wrapper.DumpInfo()));
//if (rendererCL)
//{
// string s = "OpenCL Kernels: \r\n" + rendererCL->IterKernel() + "\r\n" + rendererCL->DEKernel() + "\r\n" + rendererCL->FinalAccumKernel();
//
// QMetaObject::invokeMethod(m_Fractorium->ui.InfoRenderingTextEdit, "setText", Qt::QueuedConnection, Q_ARG(const QString&, QString::fromStdString(s)));
//}
}
}
else if (result != eRenderStatus::RENDER_ABORT)//If error was returned, something went very wrong, show error report.
{
auto errors = m_Renderer->ErrorReport();
success = false;
m_FailedRenders++;
const QString status = "Rendering failed, see info tab. Try changing parameters.";
QMetaObject::invokeMethod(m_Fractorium->m_RenderStatusLabel, "setText", Qt::QueuedConnection, Q_ARG(const QString&, status));
m_Fractorium->ErrorReportToQTextEdit(errors, m_Fractorium->ui.InfoRenderingTextEdit);
m_Renderer->ClearErrorReport();
if (m_FailedRenders >= 3)
{
m_Rendering = false;
StopRenderTimer(true);
const QString status2 = "Rendering failed 3 or more times, stopping all rendering, see info tab. Try changing renderer types.";
QMetaObject::invokeMethod(m_Fractorium->m_RenderStatusLabel, "setText", Qt::QueuedConnection, Q_ARG(const QString&, status2));
ClearFinalImages();
m_GLController->ClearWindow();
if (rendererCL)
{
//string s = "OpenCL Kernels: \r\n" + rendererCL->IterKernel() + "\r\n" + rendererCL->DEKernel() + "\r\n" + rendererCL->FinalAccumKernel();
rendererCL->ClearFinal();
//QMetaObject::invokeMethod(m_Fractorium->ui.InfoRenderingTextEdit, "setText", Qt::QueuedConnection, Q_ARG(const QString&, QString::fromStdString(s)));
}
}
}
}
//Upon finishing, or having nothing to do, rest.
if (ProcessState() == eProcessState::ACCUM_DONE)
QThread::msleep(1);
m_Rendering = false;
return success;
}
/// <summary>
/// Stop rendering and initialize a new renderer, using the specified type.
/// Rendering will be left in a stopped state. The caller is responsible for restarting the render loop again.
/// </summary>
/// <param name="renderType">The type of render to create</param>
/// <param name="devices">The platform,device index pairs of the devices to use</param>
/// <param name="updatePreviews">True to re-render the library previews, else false.</param>
/// <param name="shared">True if shared with OpenGL, else false. Default: true.</param>
/// <returns>True if nothing went wrong, else false.</returns>
template <typename T>
bool FractoriumEmberController<T>::CreateRenderer(eRendererType renderType, const vector<pair<size_t, size_t>>& devices, bool updatePreviews, bool shared)
{
bool ok = true;
auto s = m_Fractorium->m_Settings;
auto gl = m_Fractorium->ui.GLDisplay;
if (!m_Renderer.get() || (m_Renderer->RendererType() != renderType) || !Equal(m_Devices, devices) || m_Renderer->Shared() != shared)
{
EmberReport emberReport;
vector<string> errorReport;
DeleteRenderer();//Delete the renderer and refresh the textures.
//Before starting, must take care of allocations.
if (gl->Allocate(true))//Forcing a realloc of the texture is necessary on AMD, but not on nVidia.
{
m_Renderer = unique_ptr<EmberNs::RendererBase>(::CreateRenderer<T>(renderType, devices, shared, gl->OutputTexID(), emberReport));//Always make bucket type float.
errorReport = emberReport.ErrorReport();
if (errorReport.empty())
{
m_Devices = devices;
m_OutputTexID = gl->OutputTexID();
}
else
{
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);
}
}
else
{
ok = false;
m_Fractorium->ShowCritical("Renderer Creation Error", "Could not create OpenGL texture, interactive rendering will be disabled.");
m_Fractorium->ErrorReportToQTextEdit(errorReport, m_Fractorium->ui.InfoRenderingTextEdit);
}
}
if (m_Renderer.get())
{
m_RenderType = m_Renderer->RendererType();
if (m_RenderType == eRendererType::OPENCL_RENDERER)
{
auto val = m_Fractorium->m_Settings->OpenClQuality() * m_Fractorium->m_Settings->Devices().size();
m_Fractorium->m_QualitySpin->DoubleClickZero(val);
m_Fractorium->m_QualitySpin->DoubleClickNonZero(val);
if (m_Fractorium->m_QualitySpin->value() < val)
m_Fractorium->m_QualitySpin->setValue(val);
if (auto rendererCL = dynamic_cast<RendererCL<T, float>*>(m_Renderer.get()))
rendererCL->SubBatchPercentPerThread(static_cast<float>(s->OpenCLSubBatchPct()));
}
else
{
const auto quality = m_Fractorium->m_Settings->CpuQuality();
m_Fractorium->m_QualitySpin->DoubleClickZero(quality);
m_Fractorium->m_QualitySpin->DoubleClickNonZero(quality);
if (m_Fractorium->m_QualitySpin->value() > quality)
m_Fractorium->m_QualitySpin->setValue(quality);
}
m_Renderer->Callback(this);
m_Renderer->ReclaimOnResize(true);
//Give it an initial ember, will be updated many times later.
//Even though the bounds are computed when starting the next render. The OpenGL draw calls use these values, which might get called before the render starts.
m_Renderer->SetEmber(m_Ember, eProcessAction::FULL_RENDER, true);
m_Renderer->EarlyClip(s->EarlyClip());
m_Renderer->YAxisUp(s->YAxisUp());
m_Renderer->ThreadCount(s->ThreadCount());
if (m_Renderer->RendererType() == eRendererType::CPU_RENDERER)
m_Renderer->InteractiveFilter(s->CpuDEFilter() ? eInteractiveFilter::FILTER_DE : eInteractiveFilter::FILTER_LOG);
else
m_Renderer->InteractiveFilter(s->OpenCLDEFilter() ? eInteractiveFilter::FILTER_DE : eInteractiveFilter::FILTER_LOG);
if (updatePreviews)
{
m_LibraryPreviewRenderer = make_unique<TreePreviewRenderer<T>>(this, m_Fractorium->ui.LibraryTree, m_EmberFile, nullptr);//Will take the same settings as the main renderer.
RenderLibraryPreviews();
}
m_FailedRenders = 0;
m_RenderElapsedTimer.Tic();
//Leave rendering in a stopped state. The caller is responsible for restarting the render loop again.
}
else
{
ok = false;
m_Fractorium->ShowCritical("Renderer Creation Error", "Creating a basic CPU renderer failed, something is catastrophically wrong. Exiting program.");
QApplication::quit();
}
return ok;
}
/// <summary>
/// Enable or disable the controls related to changing the renderer.
/// Used when pausing and restarting the renderer.
/// </summary>
/// <param name="enable">True to enable, else false.</param>
void Fractorium::EnableRenderControls(bool enable)
{
ui.ActionCpu->setEnabled(enable);
ui.ActionCL->setEnabled(enable);
ui.ActionSP->setEnabled(enable);
ui.ActionDP->setEnabled(enable);
ui.ActionOptions->setEnabled(enable);
}
/// <summary>
/// Wrapper to stop the timer, shutdown the controller and recreate, then restart the controller and renderer from the options.
/// </summary>
/// <param name="updatePreviews">True to re-render the library previews, else false.</param>
void Fractorium::ShutdownAndRecreateFromOptions(bool updatePreviews)
{
//First completely stop what the current rendering process is doing.
m_Controller->Shutdown();
StartRenderTimer(updatePreviews);//This will recreate the controller and/or the renderer from the options if necessary, then start the render timer.
m_Settings->sync();
}
/// <summary>
/// Create a new renderer from the options.
/// </summary>
/// <param name="updatePreviews">True to re-render the library previews, else false.</param>
/// <returns>True if nothing went wrong, else false.</returns>
bool Fractorium::CreateRendererFromOptions(bool updatePreviews)
{
bool ok = true;
const auto useOpenCL = m_Info->Ok() && m_Settings->OpenCL();
auto v = Devices(m_Settings->Devices());
const auto doOpenCL = useOpenCL && !v.empty();
ui.ActionCopyKernel->setEnabled(doOpenCL);
//The most important option to process is what kind of renderer is desired, so do it first.
if (!m_Controller->CreateRenderer(doOpenCL ? eRendererType::OPENCL_RENDERER : eRendererType::CPU_RENDERER, v, updatePreviews, doOpenCL && m_Settings->SharedTexture()))
{
//If using OpenCL, will only get here if creating RendererCL failed, but creating a backup CPU Renderer succeeded.
ShowCritical("Renderer Creation Error", "Error creating renderer, most likely a GPU problem. Using CPU instead.");
m_Settings->OpenCL(false);
m_Settings->SharedTexture(false);
ui.ActionCpu->setChecked(true);
ui.ActionCL->setChecked(false);
ui.ActionCopyKernel->setEnabled(false);
m_OptionsDialog->ui.OpenCLCheckBox->setChecked(false);
m_OptionsDialog->ui.SharedTextureCheckBox->setChecked(false);
if (m_FinalRenderDialog)
m_FinalRenderDialog->ui.FinalRenderOpenCLCheckBox->setChecked(false);
ok = false;
}
if (auto rendererCL = dynamic_cast<RendererCLBase*>(m_Controller->Renderer()))
{
rendererCL->m_CompileBegun = [&]()
{
const QString status = "Compiling OpenCL kernel...";
QMetaObject::invokeMethod(m_RenderStatusLabel, "setText", Qt::QueuedConnection, Q_ARG(const QString&, status));
//m_RenderStatusLabel->repaint();
QApplication::processEvents();
};
}
return ok;
}
/// <summary>
/// Create a new controller from the options.
/// This does not create the internal renderer or start the timers.
/// </summary>
/// <returns>True if successful, else false.</returns>
bool Fractorium::CreateControllerFromOptions()
{
const size_t elementSize =
#ifdef DO_DOUBLE
m_Settings->Double() ? sizeof(double) :
#endif
sizeof(float);
if (!m_Controller.get() || (m_Controller->SizeOfT() != elementSize))
{
const auto hue = m_PaletteHueSpin->value();
const auto sat = m_PaletteSaturationSpin->value();
const auto bright = m_PaletteBrightnessSpin->value();
const auto con = m_PaletteContrastSpin->value();
const auto blur = m_PaletteBlurSpin->value();
const auto freq = m_PaletteFrequencySpin->value();
const auto rot = m_PreviewPaletteRotation;
double scale = 0;
uint current = 0;
#ifdef DO_DOUBLE
EmberFile<double> efd;
Palette<double> tempPalette;
Ember<double> xaosToggle;
#else
EmberFile<float> efd;
Palette<float> tempPalette;
Ember<float> xaosToggle;
#endif
const QModelIndex index = ui.LibraryTree->currentIndex();
ui.LibraryTree->clear();//This must be here before FillLibraryTree() is called below, else a spurious EmberTreeItemChanged event will be called on a deleted object.
//First check if a controller has already been created, and if so, save its embers and gracefully shut it down.
if (m_Controller.get())
{
scale = m_Controller->LockedScale();
m_Controller->StopAllPreviewRenderers();//Must stop any previews first, else changing controllers will crash the program and SaveCurrentToOpenedFile() will return 0.
m_Controller->CopyTempPalette(tempPalette);//Convert float to double or save double verbatim;
current = m_Controller->SaveCurrentToOpenedFile(false);
//Replace below with this once LLVM fixes a crash in their compiler with default lambda parameters.//TODO
//m_Controller->CopyEmberFile(efd);
m_Controller->CopyXaosToggleEmber(xaosToggle);
#ifdef DO_DOUBLE
m_Controller->CopyEmberFile(efd, false, [&](Ember<double>& ember) { });
#else
m_Controller->CopyEmberFile(efd, false, [&](Ember<float>& ember) { });
#endif
m_Controller->Shutdown();
}
#ifdef DO_DOUBLE
if (m_Settings->Double())
m_Controller = unique_ptr<FractoriumEmberControllerBase>(new FractoriumEmberController<double>(this));
else
#endif
m_Controller = unique_ptr<FractoriumEmberControllerBase>(new FractoriumEmberController<float>(this));
//Restore the ember and ember file.
if (m_Controller.get())
{
if (auto prev = efd.Get(current))//Restore base temp palette. Adjustments will be then be applied and stored back in in m_Ember.m_Palette below.
prev->m_Palette = tempPalette;
m_Controller->SetEmberFile(efd, true);
//Template specific palette table and variations tree setup in controller constructor, but
//must manually setup the library tree here because it's after the embers were assigned.
//Passing row re-selects the item that was previously selected.
//This will eventually call FillParamTablesAndPalette(), which in addition to filling in various fields,
//will apply the palette adjustments.
m_Controller->FillLibraryTree(index.row());
m_Controller->SetEmber(current, true);
m_Controller->SetXaosToggleEmber(xaosToggle);
m_Controller->LockedScale(scale);
//Setting these and updating the GUI overwrites the work of clearing them done in SetEmber() above.
//It's a corner case, but doesn't seem to matter.
m_PaletteHueSpin->SetValueStealth(hue);
m_PaletteSaturationSpin->SetValueStealth(sat);
m_PaletteBrightnessSpin->SetValueStealth(bright);
m_PaletteContrastSpin->SetValueStealth(con);
m_PaletteBlurSpin->SetValueStealth(blur);
m_PaletteFrequencySpin->SetValueStealth(freq);
m_PreviewPaletteRotation = rot;
m_Controller->PaletteAdjust();//Applies the adjustments to temp and saves in m_Ember.m_Palette, then fills in the palette preview widget.
}
return m_Controller.get();
}
return false;
}
/// <summary>
/// Start the render timer.
/// If a renderer has not been created yet, or differs form the options, it will first be created from the options.
/// </summary>
/// <param name="updatePreviews">True to re-render the library previews, else false.</param>
void Fractorium::StartRenderTimer(bool updatePreviews)
{
//Starting the render timer, either for the first time
//or from a paused state, such as resizing or applying new options.
const auto newController = CreateControllerFromOptions();
if (m_Controller.get())
{
//On program startup, the renderer does not get initialized until now.
//If a new controller was created, then previews will have started, so only start the previews if a new controller
//was *not* created and updatePreviews is true.
CreateRendererFromOptions(updatePreviews && !newController);
if (m_Controller->Renderer())
m_Controller->StartRenderTimer();
}
}
/// <summary>
/// Idle timer event which calls the controller's Render() function.
/// </summary>
void Fractorium::IdleTimer() { m_Controller->Render(); }
/// <summary>
/// Thin wrapper to determine if the controllers have been properly initialized.
/// </summary>
/// <returns>True if the ember controller and GL controllers are both not nullptr, else false.</returns>
bool Fractorium::ControllersOk() { return m_Controller.get() && m_Controller->GLController(); }
template class FractoriumEmberController<float>;
#ifdef DO_DOUBLE
template class FractoriumEmberController<double>;
#endif