#include "FractoriumPch.h" #include "Fractorium.h" /// /// Return whether the render timer is running. /// /// True if running, else false. bool FractoriumEmberControllerBase::RenderTimerRunning() { return m_RenderTimer && m_RenderTimer->isActive(); } /// /// Start the render timer. /// If a renderer has not been created yet, it will be created from the options. /// void FractoriumEmberControllerBase::StartRenderTimer() { if (m_RenderTimer) { UpdateRender(); m_RenderTimer->start(); m_RenderElapsedTimer.Tic(); } } /// /// 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. /// void FractoriumEmberControllerBase::DelayedStartRenderTimer() { DeleteRenderer(); if (m_RenderRestartTimer) { m_RenderRestartTimer->setSingleShot(true); m_RenderRestartTimer->start(300);//Will stop the timer if it's already running, and start again. } } /// /// Stop the render timer and abort the rendering process. /// Optionally block until stopping is complete. /// /// True to block, else false. void FractoriumEmberControllerBase::StopRenderTimer(bool wait) { if (m_RenderTimer) 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(); } } /// /// Stop all timers, rendering and drawing and block until they are done. /// void FractoriumEmberControllerBase::Shutdown() { StopRenderTimer(true); while(m_Fractorium->ui.GLDisplay->Drawing()) QApplication::processEvents(); } /// /// 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. /// /// The action to take void FractoriumEmberControllerBase::UpdateRender(eProcessAction action) { AddProcessAction(action); m_RenderElapsedTimer.Tic(); } /// /// Call Shutdown() then delete the renderer and clear the textures in the output window if there is one. /// void FractoriumEmberControllerBase::DeleteRenderer() { Shutdown(); m_Renderer.reset(); if (GLController()) GLController()->ClearWindow(); } /// /// Save the current contents of the GL window to a file. /// This could benefit from QImageWriter, however it's 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. /// /// The full path and filename void FractoriumEmberControllerBase::SaveCurrentRender(QString filename) { if (filename != "") { bool b = false; unsigned int i, j; unsigned int width = m_Renderer->FinalRasW(); unsigned int height = m_Renderer->FinalRasH(); unsigned char* data = NULL; vector vecRgb; QFileInfo fileInfo(filename); QString suffix = fileInfo.suffix(); FractoriumSettings* settings = m_Fractorium->m_Settings; RendererCLBase* rendererCL = dynamic_cast(m_Renderer.get()); if (rendererCL && m_Renderer->PrepFinalAccumVector(m_FinalImage)) { if (!rendererCL->ReadFinal(m_FinalImage.data())) { QMessageBox::critical(m_Fractorium, "GPU Read Error", "Could not read image from the GPU, aborting image save."); return; } } data = m_FinalImage.data();//Png and channels = 4. if ((suffix == "jpg" || suffix == "bmp") && m_Renderer->NumChannels() == 4) { EmberNs::RgbaToRgb(m_FinalImage, vecRgb, width, height); data = vecRgb.data(); } string s = filename.toStdString(); string id = settings->Id().toStdString(); string url = settings->Url().toStdString(); string nick = settings->Nick().toStdString(); EmberImageComments comments = m_Renderer->ImageComments(0, false, true); if (suffix == "png") b = WritePng(s.c_str(), data, width, height, 1, true, comments, id, url, nick); else if (suffix == "jpg") b = WriteJpeg(s.c_str(), data, width, height, 100, true, comments, id, url, nick); else if (suffix == "bmp") b = WriteBmp(s.c_str(), data, width, height); else QMessageBox::critical(m_Fractorium, "Save Failed", "Unrecognized format " + suffix + ", not saving."); if (b) settings->SaveFolder(fileInfo.canonicalPath()); else QMessageBox::critical(m_Fractorium, "Save Failed", "Could not save file, try saving to a different folder."); } } /// /// Add a process action to the list of actions to take. /// Called in response to the user changing something on the GUI. /// /// The action for the renderer to take void FractoriumEmberControllerBase::AddProcessAction(eProcessAction action) { m_Cs.Enter(); m_ProcessActions.push_back(action); if (m_Renderer.get()) m_Renderer->Abort(); m_Cs.Leave(); } /// /// 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. /// /// The most significant processing action desired eProcessAction FractoriumEmberControllerBase::CondenseAndClearProcessActions() { m_Cs.Enter(); eProcessAction action = NOTHING; for (size_t i = 0; i < m_ProcessActions.size(); i++) if (m_ProcessActions[i] > action) action = m_ProcessActions[i]; m_ProcessActions.clear(); m_Cs.Leave(); return action; } /// /// Render progress callback function to update progress bar. /// /// The ember currently being rendered /// An extra dummy parameter /// 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 changed anything on the GUI, else 1 to continue rendering. template int FractoriumEmberController::ProgressFunc(Ember& ember, void* foo, double fraction, int stage, double etaMs) { QString status; m_Fractorium->m_ProgressBar->setValue((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"; m_Fractorium->m_RenderStatusLabel->setText(status); return m_ProcessActions.empty() ? 1 : 0;//If they've done anything, abort. } /// /// Clear the undo list as well as the undo/redo index and state. /// template void FractoriumEmberController::ClearUndo() { m_UndoIndex = 0; m_UndoList.clear(); m_EditState = REGULAR_EDIT; m_LastEditWasUndoRedo = false; m_Fractorium->ui.ActionUndo->setEnabled(false); m_Fractorium->ui.ActionRedo->setEnabled(false); } /// /// The main rendering function. /// Called whenever the event loop is idle. /// template bool FractoriumEmberController::Render() { bool success = true; m_Rendering = true; GLWidget* gl = m_Fractorium->ui.GLDisplay; eProcessAction action = CondenseAndClearProcessActions(); //Set dimensions first. if ((m_Ember.m_FinalRasW != gl->width() || m_Ember.m_FinalRasH != gl->height())) { m_Ember.SetSizeAndAdjustScale(gl->width(), gl->height(), false, SCALE_WIDTH); m_Fractorium->m_ScaleSpin->SetValueStealth(m_Ember.m_PixelsPerUnit); action = FULL_RENDER; } //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 != NOTHING) { int i, solo = m_Fractorium->ui.CurrentXformCombo->property("soloxform").toInt(); if (solo != -1) { m_TempOpacities.resize(m_Ember.TotalXformCount()); for (i = 0; i < m_Ember.TotalXformCount(); i++) { m_TempOpacities[i] = m_Ember.GetTotalXform(i)->m_Opacity; m_Ember.GetTotalXform(i)->m_Opacity = i == solo ? 1 : 0; } } m_Renderer->SetEmber(m_Ember, action); if (solo != -1) { for (i = 0; i < m_Ember.TotalXformCount(); i++) { m_Ember.GetTotalXform(i)->m_Opacity = m_TempOpacities[i]; } } } //Determining if a completely new rendering process is being started. bool iterBegin = ProcessState() == NONE; if (iterBegin) { if (m_Renderer->RendererType() == CPU_RENDERER) m_SubBatchCount = m_Fractorium->m_Settings->CpuSubBatch(); else if (m_Renderer->RendererType() == OPENCL_RENDERER) m_SubBatchCount = m_Fractorium->m_Settings->OpenCLSubBatch(); m_Fractorium->m_ProgressBar->setValue(0); m_Fractorium->m_RenderStatusLabel->setText("Starting"); } //If the rendering process hasn't finished, render with the current specified action. if (ProcessState() != ACCUM_DONE) { //if (m_Renderer->Run(m_FinalImage, 0) == RENDER_OK)//Full, non-incremental render for debugging. if (m_Renderer->Run(m_FinalImage, 0, m_SubBatchCount, iterBegin) == RENDER_OK)//Force output on iterBegin. { //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() == OPENCL_RENDERER) { if (m_SubBatchCount < 3)//More than 3 with OpenCL gives a sluggish UI. m_SubBatchCount++; } 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() == ACCUM_DONE) { EmberStats stats = m_Renderer->Stats(); m_Fractorium->m_ProgressBar->setValue(100); QString iters = QLocale(QLocale::English).toString(stats.m_Iters); QString scaledQuality = QString::number((unsigned int)m_Renderer->ScaledQuality()); string renderTime = m_RenderElapsedTimer.Format(m_RenderElapsedTimer.Toc()); //Only certain status can be reported with OpenCL. if (m_Renderer->RendererType() == OPENCL_RENDERER) { m_Fractorium->m_RenderStatusLabel->setText("Iters: " + iters + ". Scaled quality: " + scaledQuality + ". Total time: " + QString::fromStdString(renderTime)); } else { double percent = (double)stats.m_Badvals / (double)stats.m_Iters; QString badVals = QLocale(QLocale::English).toString(stats.m_Badvals); QString badPercent = QLocale(QLocale::English).toString(percent * 100, 'f', 2); m_Fractorium->m_RenderStatusLabel->setText("Iters: " + iters + ". Scaled quality: " + scaledQuality + ". Bad values: " + badVals + " (" + badPercent + "%). Total time: " + QString::fromStdString(renderTime)); } 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 = REGULAR_EDIT; } else if (m_EditState == REGULAR_EDIT)//Regular edit, just add to the end of the undo list. { 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 if (!m_LastEditWasUndoRedo && m_UndoIndex != m_UndoList.size() - 1)//They were in the middle of the undo list, then did a manual edit, so clear the undo list. { ClearUndo(); m_UndoList.push_back(m_Ember); } m_LastEditWasUndoRedo = false; m_Fractorium->UpdateHistogramBounds();//Mostly of engineering interest. } //Update the GL window on start because the output will be forced. //Update it on finish because the rendering process is completely done. if (iterBegin || ProcessState() == ACCUM_DONE) { if (m_FinalImage.size() == m_Renderer->FinalBufferSize())//Make absolutely sure the correct amount of data is passed. gl->repaint(); //Uncomment for debugging kernel build and execution errors. //m_Fractorium->ui.InfoRenderingTextEdit->setText(QString::fromStdString(m_Fractorium->m_Wrapper.DumpInfo())); //if (RendererCL* rendererCL = dynamic_cast*>(m_Renderer.get())) // m_Fractorium->ui.InfoRenderingTextEdit->setText(QString::fromStdString(rendererCL->IterKernel())); } } else//Something went very wrong, show error report. { vector errors = m_Renderer->ErrorReport(); success = false; m_FailedRenders++; m_Fractorium->m_RenderStatusLabel->setText("Rendering failed, see info tab. Try changing parameters."); m_Fractorium->ErrorReportToQTextEdit(errors, m_Fractorium->ui.InfoRenderingTextEdit); m_Renderer->ClearErrorReport(); if (m_FailedRenders >= 3) { m_Rendering = false; StopRenderTimer(true); m_Fractorium->m_RenderStatusLabel->setText("Rendering failed 3 or more times, stopping all rendering, see info tab. Try changing renderer types."); memset(m_FinalImage.data(), 0, m_FinalImage.size()); if (m_Renderer->RendererType() == OPENCL_RENDERER) ((RendererCL*)m_Renderer.get())->ClearFinal(); m_GLController->ClearWindow(); } } } //Upon finishing, or having nothing to do, rest. if (ProcessState() == ACCUM_DONE) QThread::msleep(1); m_Rendering = false; return success; } /// /// 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. /// /// The type of render to create /// The index platform of the platform to use /// The index device of the device to use /// The texture ID of the shared OpenGL texture if shared /// True if shared with OpenGL, else false. Default: true. /// True if nothing went wrong, else false. template bool FractoriumEmberController::CreateRenderer(eRendererType renderType, unsigned int platform, unsigned int device, bool shared) { bool ok = true; FractoriumSettings* s = m_Fractorium->m_Settings; GLWidget* gl = m_Fractorium->ui.GLDisplay; if (!m_Renderer.get() || (m_Renderer->RendererType() != renderType) || (m_Platform != platform) || (m_Device != device)) { EmberReport emberReport; vector errorReport; DeleteRenderer();//Delete the renderer and refresh the textures. //Before starting, must take care of allocations. gl->Allocate(true);//Forcing a realloc of the texture is necessary on AMD, but not on nVidia. m_Renderer = auto_ptr(::CreateRenderer(renderType, platform, device, shared, gl->OutputTexID(), emberReport)); errorReport = emberReport.ErrorReport(); if (errorReport.empty()) { m_Platform = platform;//Store values for re-creation later on. m_Device = device; m_OutputTexID = gl->OutputTexID(); m_Shared = shared; } else { ok = false; QMessageBox::critical(m_Fractorium, "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); } } if (m_Renderer.get()) { m_RenderType = m_Renderer->RendererType(); if (m_RenderType == OPENCL_RENDERER && m_Fractorium->m_QualitySpin->value() < 30) m_Fractorium->m_QualitySpin->setValue(30); m_Renderer->Callback(this); m_Renderer->NumChannels(4);//Always using 4 since the GL texture is RGBA. m_Renderer->ReclaimOnResize(true); m_Renderer->SetEmber(m_Ember);//Give it an initial ember, will be updated many times later. m_Renderer->EarlyClip(s->EarlyClip()); m_Renderer->YAxisUp(s->YAxisUp()); m_Renderer->ThreadCount(s->ThreadCount()); m_Renderer->Transparency(s->Transparency()); if (m_Renderer->RendererType() == CPU_RENDERER) m_Renderer->InteractiveFilter(s->CpuDEFilter() ? FILTER_DE : FILTER_LOG); else m_Renderer->InteractiveFilter(s->OpenCLDEFilter() ? FILTER_DE : FILTER_LOG); if ((m_Renderer->EarlyClip() != m_PreviewRenderer->EarlyClip()) || (m_Renderer->YAxisUp() != m_PreviewRenderer->YAxisUp())) { StopPreviewRender(); m_PreviewRenderer->EarlyClip(m_Renderer->EarlyClip()); m_PreviewRenderer->YAxisUp(m_Renderer->YAxisUp()); RenderPreviews(); } 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; QMessageBox::critical(m_Fractorium, "Renderer Creation Error", "Creating a basic CPU renderer failed, something is catastrophically wrong. Exiting program."); QApplication::quit(); } return ok; } /// /// Create a new renderer from the options. /// /// True if nothing went wrong, else false. bool Fractorium::CreateRendererFromOptions() { bool ok = true; bool useOpenCL = m_Wrapper.CheckOpenCL() && m_Settings->OpenCL(); //The most important option to process is what kind of renderer is desired, so do it first. if (!m_Controller->CreateRenderer(useOpenCL ? OPENCL_RENDERER : CPU_RENDERER, m_Settings->PlatformIndex(), m_Settings->DeviceIndex())) { //If using OpenCL, will only get here if creating RendererCL failed, but creating a backup CPU Renderer succeeded. QMessageBox::critical(this, "Renderer Creation Error", "Error creating renderer, most likely a GPU problem. Using CPU instead."); m_Settings->OpenCL(false); m_OptionsDialog->ui.OpenCLCheckBox->setChecked(false); m_FinalRenderDialog->ui.FinalRenderOpenCLCheckBox->setChecked(false); ok = false; } return ok; } /// /// Create a new controller from the options. /// This does not create the internal renderer or start the timers. /// /// True if successful, else false. bool Fractorium::CreateControllerFromOptions() { bool ok = true; size_t size = #ifdef DO_DOUBLE m_Settings->Double() ? sizeof(double) : #endif sizeof(float); if (!m_Controller.get() || (m_Controller->SizeOfT() != size)) { double hue = m_PaletteHueSpin->value(); double sat = m_PaletteSaturationSpin->value(); double bright = m_PaletteBrightnessSpin->value(); double con = m_PaletteContrastSpin->value(); double blur = m_PaletteBlurSpin->value(); double freq = m_PaletteFrequencySpin->value(); #ifdef DO_DOUBLE Ember ed; EmberFile efd; Palette tempPalette; #else Ember ed; EmberFile efd; Palette tempPalette; #endif QModelIndex index = ui.LibraryTree->currentIndex(); //First check if a controller has already been created, and if so, save its embers and gracefully shut it down. if (m_Controller.get()) { m_Controller->CopyTempPalette(tempPalette);//Convert float to double or save double verbatim; m_Controller->CopyEmber(ed); m_Controller->CopyEmberFile(efd); m_Controller->Shutdown(); } #ifdef DO_DOUBLE if (m_Settings->Double()) m_Controller = auto_ptr(new FractoriumEmberController(this)); else #endif m_Controller = auto_ptr(new FractoriumEmberController(this)); //Restore the ember and ember file. if (m_Controller.get()) { m_Controller->SetEmber(ed);//Convert float to double or set double verbatim; m_Controller->SetEmberFile(efd); //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. m_Controller->FillLibraryTree(index.row());//Passing row re-selects the item that was previously selected. m_Controller->SetTempPalette(tempPalette);//Restore palette. 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_Controller->PaletteAdjust();//Fills in the palette. } } return m_Controller.get() != NULL; } /// /// 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. /// void Fractorium::StartRenderTimer() { //Starting the render timer, either for the first time //or from a paused state, such as resizing or applying new options. CreateControllerFromOptions(); if (m_Controller.get()) { //On program startup, the renderer does not get initialized until now. CreateRendererFromOptions(); if (m_Controller->Renderer()) m_Controller->StartRenderTimer(); } } /// /// Idle timer event which calls the controller's Render() function. /// void Fractorium::IdleTimer() { m_Controller->Render(); } /// /// Thin wrapper to determine if the controllers have been properly initialized. /// /// True if the ember controller and GL controllers are both not NULL, else false. bool Fractorium::ControllersOk() { return m_Controller.get() && m_Controller->GLController(); }