From f3c49bf4c7b8bfe3ed0b195ca1b51239cd5f7c7b Mon Sep 17 00:00:00 2001 From: Ben Haller Date: Mon, 26 Aug 2024 15:24:08 -0400 Subject: [PATCH] add "Use OpenGL" checkbox in prefs --- QtSLiM/QtSLiM.pro | 17 + QtSLiM/QtSLiMAppDelegate.cpp | 10 + QtSLiM/QtSLiMChromosomeWidget.cpp | 864 ++------------------ QtSLiM/QtSLiMChromosomeWidget.h | 40 +- QtSLiM/QtSLiMChromosomeWidget_GL.cpp | 768 ++++++++++++++++++ QtSLiM/QtSLiMChromosomeWidget_QT.cpp | 764 ++++++++++++++++++ QtSLiM/QtSLiMHaplotypeManager.cpp | 317 ++------ QtSLiM/QtSLiMHaplotypeManager.h | 26 +- QtSLiM/QtSLiMHaplotypeManager_GL.cpp | 238 ++++++ QtSLiM/QtSLiMHaplotypeManager_QT.cpp | 236 ++++++ QtSLiM/QtSLiMIndividualsWidget.cpp | 1067 ++----------------------- QtSLiM/QtSLiMIndividualsWidget.h | 41 +- QtSLiM/QtSLiMIndividualsWidget_GL.cpp | 752 +++++++++++++++++ QtSLiM/QtSLiMIndividualsWidget_QT.cpp | 626 +++++++++++++++ QtSLiM/QtSLiMOpenGL.cpp | 72 ++ QtSLiM/QtSLiMOpenGL.h | 116 +++ QtSLiM/QtSLiMOpenGL_Emulation.h | 77 ++ QtSLiM/QtSLiMPreferences.cpp | 41 +- QtSLiM/QtSLiMPreferences.h | 3 + QtSLiM/QtSLiMPreferences.ui | 36 +- VERSIONS | 1 + core/spatial_map.h | 1 + 22 files changed, 4034 insertions(+), 2079 deletions(-) create mode 100644 QtSLiM/QtSLiMChromosomeWidget_GL.cpp create mode 100644 QtSLiM/QtSLiMChromosomeWidget_QT.cpp create mode 100644 QtSLiM/QtSLiMHaplotypeManager_GL.cpp create mode 100644 QtSLiM/QtSLiMHaplotypeManager_QT.cpp create mode 100644 QtSLiM/QtSLiMIndividualsWidget_GL.cpp create mode 100644 QtSLiM/QtSLiMIndividualsWidget_QT.cpp create mode 100644 QtSLiM/QtSLiMOpenGL.cpp create mode 100644 QtSLiM/QtSLiMOpenGL.h create mode 100644 QtSLiM/QtSLiMOpenGL_Emulation.h diff --git a/QtSLiM/QtSLiM.pro b/QtSLiM/QtSLiM.pro index 2f9dfa68..9c733138 100644 --- a/QtSLiM/QtSLiM.pro +++ b/QtSLiM/QtSLiM.pro @@ -61,6 +61,14 @@ QMAKE_CFLAGS += $$(CFLAGS) DEFINES += EIDOS_GUI DEFINES += SLIMGUI=1 + +# Uncomment this define to disable the use of OpenGL in SLiMgui completely. This, plus removing the +# link dependency on openglwidgets, should allow you to build SLiMgui without linking OpenGL at all. +# I don't expect end users to need to do this; it is for testing purposes, to ensure than the code +# path used when OpenGL is disabled in Preferences does not inadvertently make any OpenGL calls. +#DEFINES += SLIM_NO_OPENGL + + greaterThan(QT_MAJOR_VERSION, 5) { # For Qt6 we require C++17 (because Qt6 requires it), but don't use it ourselves CONFIG += c++17 @@ -160,6 +168,8 @@ else:unix: PRE_TARGETDEPS += $$OUT_PWD/../eidos_zlib/libeidos_zlib.a SOURCES += \ ../cmake/GitSHA1_qmake.cpp \ + QtSLiMChromosomeWidget_GL.cpp \ + QtSLiMChromosomeWidget_QT.cpp \ QtSLiMDebugOutputWindow.cpp \ QtSLiMGraphView_1DPopulationSFS.cpp \ QtSLiMGraphView_1DSampleSFS.cpp \ @@ -172,6 +182,11 @@ SOURCES += \ QtSLiMGraphView_PopFitnessDist.cpp \ QtSLiMGraphView_PopSizeOverTime.cpp \ QtSLiMGraphView_SubpopFitnessDists.cpp \ + QtSLiMHaplotypeManager_GL.cpp \ + QtSLiMHaplotypeManager_QT.cpp \ + QtSLiMIndividualsWidget_GL.cpp \ + QtSLiMIndividualsWidget_QT.cpp \ + QtSLiMOpenGL.cpp \ QtSLiM_Plot.cpp \ main.cpp \ QtSLiMWindow.cpp \ @@ -218,6 +233,8 @@ HEADERS += \ QtSLiMGraphView_PopFitnessDist.h \ QtSLiMGraphView_PopSizeOverTime.h \ QtSLiMGraphView_SubpopFitnessDists.h \ + QtSLiMOpenGL.h \ + QtSLiMOpenGL_Emulation.h \ QtSLiMWindow.h \ QtSLiMAppDelegate.h \ QtSLiMChromosomeWidget.h \ diff --git a/QtSLiM/QtSLiMAppDelegate.cpp b/QtSLiM/QtSLiMAppDelegate.cpp index f2350913..2d39cdcf 100644 --- a/QtSLiM/QtSLiMAppDelegate.cpp +++ b/QtSLiM/QtSLiMAppDelegate.cpp @@ -33,6 +33,7 @@ #include "QtSLiMTablesDrawer.h" #include "QtSLiMDebugOutputWindow.h" #include "QtSLiMConsoleTextEdit.h" +#include "QtSLiMOpenGL.h" #include "QtSLiMExtras.h" #include @@ -196,6 +197,9 @@ QtSLiMAppDelegate::QtSLiMAppDelegate(QObject *p_parent) : QObject(p_parent) // Remember our current working directory, to return to whenever we are not inside SLiM/Eidos app_cwd_ = Eidos_CurrentDirectory(); + + // Initialize our OpenGL wrapper state + QtSLiM_AllocateGLBuffers(); // Set up the format for OpenGL buffers globally, so that it applies to all windows and contexts // This defaults to OpenGL 2.0, which is what we want, so right now we don't customize @@ -258,6 +262,12 @@ QtSLiMAppDelegate::QtSLiMAppDelegate(QObject *p_parent) : QObject(p_parent) QtSLiMAppDelegate::~QtSLiMAppDelegate(void) { + //qDebug() << "QtSLiMAppDelegate::~QtSLiMAppDelegate"; + +#if SLIM_LEAK_CHECKING + QtSLiM_FreeGLBuffers(); +#endif + qtSLiMAppDelegate = nullptr; // kill the global shared instance, for safety } diff --git a/QtSLiM/QtSLiMChromosomeWidget.cpp b/QtSLiM/QtSLiMChromosomeWidget.cpp index 3fd1f0af..4777a5d6 100644 --- a/QtSLiM/QtSLiMChromosomeWidget.cpp +++ b/QtSLiM/QtSLiMChromosomeWidget.cpp @@ -22,6 +22,7 @@ #include "QtSLiMWindow.h" #include "QtSLiMExtras.h" #include "QtSLiMHaplotypeManager.h" +#include "QtSLiMPreferences.h" #include #include @@ -34,65 +35,6 @@ #include -// OpenGL constants -static const int kMaxGLRects = 4000; // 4000 rects -static const int kMaxVertices = kMaxGLRects * 4; // 4 vertices each - -// OpenGL macros -#define SLIM_GL_PREPARE() \ - int displayListIndex = 0; \ - float *vertices = glArrayVertices, *colors = glArrayColors; \ - \ - glEnableClientState(GL_VERTEX_ARRAY); \ - glVertexPointer(2, GL_FLOAT, 0, glArrayVertices); \ - glEnableClientState(GL_COLOR_ARRAY); \ - glColorPointer(4, GL_FLOAT, 0, glArrayColors); - -#define SLIM_GL_DEFCOORDS(rect) \ - float left = static_cast(rect.left()); \ - float top = static_cast(rect.top()); \ - float right = left + static_cast(rect.width()); \ - float bottom = top + static_cast(rect.height()); - -#define SLIM_GL_PUSHRECT() \ - *(vertices++) = left; \ - *(vertices++) = top; \ - *(vertices++) = left; \ - *(vertices++) = bottom; \ - *(vertices++) = right; \ - *(vertices++) = bottom; \ - *(vertices++) = right; \ - *(vertices++) = top; - -#define SLIM_GL_PUSHRECT_COLORS() \ - for (int j = 0; j < 4; ++j) \ - { \ - *(colors++) = colorRed; \ - *(colors++) = colorGreen; \ - *(colors++) = colorBlue; \ - *(colors++) = colorAlpha; \ - } - -#define SLIM_GL_CHECKBUFFERS() \ - displayListIndex++; \ - \ - if (displayListIndex == kMaxGLRects) \ - { \ - glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); \ - \ - vertices = glArrayVertices; \ - colors = glArrayColors; \ - displayListIndex = 0; \ - } - -#define SLIM_GL_FINISH() \ - if (displayListIndex) \ - glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); \ - \ - glDisableClientState(GL_VERTEX_ARRAY); \ - glDisableClientState(GL_COLOR_ARRAY); - - static const int numberOfTicksPlusOne = 4; static const int tickLength = 5; static const int heightForTicks = 16; @@ -100,34 +42,27 @@ static const int selectionKnobSizeExtension = 2; // a 5-pixel-width knob is 2: 2 static const int selectionKnobSize = selectionKnobSizeExtension + selectionKnobSizeExtension + 1; -QtSLiMChromosomeWidget::QtSLiMChromosomeWidget(QWidget *p_parent, QtSLiMWindow *controller, Species *displaySpecies, Qt::WindowFlags f) : QOpenGLWidget(p_parent, f) +QtSLiMChromosomeWidget::QtSLiMChromosomeWidget(QWidget *p_parent, QtSLiMWindow *controller, Species *displaySpecies, Qt::WindowFlags f) +#ifndef SLIM_NO_OPENGL + : QOpenGLWidget(p_parent, f) +#else + : QWidget(p_parent, f) +#endif { controller_ = controller; setFocalDisplaySpecies(displaySpecies); - if (!glArrayVertices) - glArrayVertices = static_cast(malloc(kMaxVertices * 2 * sizeof(float))); // 2 floats per vertex, kMaxVertices vertices + // We support both OpenGL and non-OpenGL display, because some platforms seem + // to have problems with OpenGL (https://github.com/MesserLab/SLiM/issues/462) + QtSLiMPreferencesNotifier &prefsNotifier = QtSLiMPreferencesNotifier::instance(); - if (!glArrayColors) - glArrayColors = static_cast(malloc(kMaxVertices * 4 * sizeof(float))); // 4 floats per color, kMaxVertices colors + connect(&prefsNotifier, &QtSLiMPreferencesNotifier::useOpenGLPrefChanged, this, [this]() { update(); }); } QtSLiMChromosomeWidget::~QtSLiMChromosomeWidget() { setReferenceChromosomeView(nullptr); - if (glArrayVertices) - { - free(glArrayVertices); - glArrayVertices = nullptr; - } - - if (glArrayColors) - { - free(glArrayColors); - glArrayColors = nullptr; - } - if (haplotype_mgr_) { delete haplotype_mgr_; @@ -182,6 +117,7 @@ void QtSLiMChromosomeWidget::stateChanged(void) update(); } +#ifndef SLIM_NO_OPENGL void QtSLiMChromosomeWidget::initializeGL() { initializeOpenGLFunctions(); @@ -197,10 +133,7 @@ void QtSLiMChromosomeWidget::resizeGL(int w, int h) glLoadIdentity(); glMatrixMode(GL_MODELVIEW); } - -// This is a fast macro for when all we need is the offset of a base from the left edge of interiorRect; interiorRect.origin.x is not added here! -// This is based on the same math as rectEncompassingBase:toBase:interiorRect:displayedRange: below, and must be kept in synch with that method. -#define LEFT_OFFSET_OF_BASE(startBase, interiorRect, displayedRange) (static_cast(floor(((startBase - static_cast(displayedRange.location)) / static_cast(displayedRange.length)) * interiorRect.width()))) +#endif QRect QtSLiMChromosomeWidget::rectEncompassingBaseToBase(slim_position_t startBase, slim_position_t endBase, QRect interiorRect, QtSLiMRange displayedRange) { @@ -351,7 +284,11 @@ QtSLiMRange QtSLiMChromosomeWidget::getDisplayedRange(Species *displaySpecies) } } +#ifndef SLIM_NO_OPENGL void QtSLiMChromosomeWidget::paintGL() +#else +void QtSLiMChromosomeWidget::paintEvent(QPaintEvent * /* p_paint_event */) +#endif { QPainter painter(this); bool inDarkMode = QtSLiMInDarkMode(); @@ -380,11 +317,20 @@ void QtSLiMChromosomeWidget::paintGL() // draw ticks at bottom of content rect drawTicksInContentRect(contentRect, displaySpecies, displayedRange, painter); - - // draw our OpenGL content - painter.beginNativePainting(); - glDrawRect(displaySpecies); - painter.endNativePainting(); + + // do the core drawing, with or without OpenGL according to user preference +#ifndef SLIM_NO_OPENGL + if (QtSLiMPreferencesNotifier::instance().useOpenGLPref()) + { + painter.beginNativePainting(); + glDrawRect(displaySpecies); + painter.endNativePainting(); + } + else +#endif + { + qtDrawRect(displaySpecies, painter); + } // frame near the end, so that any roundoff errors that caused overdrawing by a pixel get cleaned up QtSLiMFrameRect(contentRect, QtSLiMColorWithWhite(inDarkMode ? 0.067 : 0.6, 1.0), painter); @@ -494,736 +440,34 @@ void QtSLiMChromosomeWidget::drawTicksInContentRect(QRect contentRect, __attribu painter.restore(); } -void QtSLiMChromosomeWidget::glDrawRect(Species *displaySpecies) -{ - bool ready = isEnabled() && !controller_->invalidSimulation(); - QRect interiorRect = getInteriorRect(); - - // if the simulation is at tick 0, it is not ready - if (ready) - if (controller_->community->Tick() == 0) - ready = false; - - if (ready) - { - // erase the content area itself - glColor3f(0.0f, 0.0f, 0.0f); - glRecti(interiorRect.left(), interiorRect.top(), interiorRect.left() + interiorRect.width(), interiorRect.top() + interiorRect.height()); - QtSLiMRange displayedRange = getDisplayedRange(displaySpecies); - - bool splitHeight = (shouldDrawRateMaps() && shouldDrawGenomicElements()); - QRect topInteriorRect = interiorRect, bottomInteriorRect = interiorRect; - int halfHeight = static_cast(ceil(interiorRect.height() / 2.0)); - int remainingHeight = interiorRect.height() - halfHeight; - - topInteriorRect.setHeight(halfHeight); - bottomInteriorRect.setHeight(remainingHeight); - bottomInteriorRect.translate(0, halfHeight); - - // draw recombination intervals in interior - if (shouldDrawRateMaps()) - glDrawRateMaps(splitHeight ? topInteriorRect : interiorRect, displaySpecies, displayedRange); - - // draw genomic elements in interior - if (shouldDrawGenomicElements()) - glDrawGenomicElements(splitHeight ? bottomInteriorRect : interiorRect, displaySpecies, displayedRange); - - // figure out which mutation types we're displaying - if (shouldDrawFixedSubstitutions() || shouldDrawMutations()) - updateDisplayedMutationTypes(displaySpecies); - - // draw fixed substitutions in interior - if (shouldDrawFixedSubstitutions()) - glDrawFixedSubstitutions(interiorRect, displaySpecies, displayedRange); - - // draw mutations in interior - if (shouldDrawMutations()) - { - if (displayHaplotypes()) - { - // display mutations as a haplotype plot, courtesy of QtSLiMHaplotypeManager; we use ClusterNearestNeighbor and - // ClusterNoOptimization because they're fast, and NN might also provide a bit more run-to-run continuity - // we cache the haplotype manager here, so our display remains constant across window resizes and other - // invalidations; we toss the cache only when the simulation tells us that the model state has changed - if (!haplotype_mgr_) - { - size_t interiorHeight = static_cast(interiorRect.height()); // one sample per available pixel line, for simplicity and speed; 47, in the current UI layout - haplotype_mgr_ = new QtSLiMHaplotypeManager(nullptr, QtSLiMHaplotypeManager::ClusterNearestNeighbor, QtSLiMHaplotypeManager::ClusterNoOptimization, controller_, displaySpecies, interiorHeight, false); - } - - if (haplotype_mgr_) - haplotype_mgr_->glDrawHaplotypes(interiorRect, false, false, false, &haplotype_previous_bincounts); - } - else - { - // display mutations as a frequency plot; this is the standard display mode - glDrawMutations(interiorRect, displaySpecies, displayedRange); - } - } - } - else - { - // erase the content area itself - glColor3f(0.88f, 0.88f, 0.88f); - glRecti(0, 0, interiorRect.width(), interiorRect.height()); - } -} - -void QtSLiMChromosomeWidget::glDrawGenomicElements(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange) -{ - Chromosome &chromosome = displaySpecies->TheChromosome(); - int previousIntervalLeftEdge = -10000; - - SLIM_GL_PREPARE(); - - for (GenomicElement *genomicElement : chromosome.GenomicElements()) - { - slim_position_t startPosition = genomicElement->start_position_; - slim_position_t endPosition = genomicElement->end_position_; - QRect elementRect = rectEncompassingBaseToBase(startPosition, endPosition, interiorRect, displayedRange); - bool widthOne = (elementRect.width() == 1); - - // We want to avoid overdrawing width-one intervals, which are important but small, so if the previous interval was width-one, - // and we are not, and we are about to overdraw it, then we scoot our left edge over one pixel to leave it alone. - if (!widthOne && (elementRect.left() == previousIntervalLeftEdge)) - elementRect.adjust(1, 0, 0, 0); - - // draw only the visible part, if any - elementRect = elementRect.intersected(interiorRect); - - if (!elementRect.isEmpty()) - { - GenomicElementType *geType = genomicElement->genomic_element_type_ptr_; - float colorRed, colorGreen, colorBlue, colorAlpha; - - if (!geType->color_.empty()) - { - colorRed = geType->color_red_; - colorGreen = geType->color_green_; - colorBlue = geType->color_blue_; - colorAlpha = 1.0; - } - else - { - slim_objectid_t elementTypeID = geType->genomic_element_type_id_; - - controller_->colorForGenomicElementType(geType, elementTypeID, &colorRed, &colorGreen, &colorBlue, &colorAlpha); - } - - SLIM_GL_DEFCOORDS(elementRect); - SLIM_GL_PUSHRECT(); - SLIM_GL_PUSHRECT_COLORS(); - SLIM_GL_CHECKBUFFERS(); - - // if this interval is just one pixel wide, we want to try to make it visible, by avoiding overdrawing it; so we remember its location - if (widthOne) - previousIntervalLeftEdge = elementRect.left(); - else - previousIntervalLeftEdge = -10000; - } - } - - SLIM_GL_FINISH(); -} - void QtSLiMChromosomeWidget::updateDisplayedMutationTypes(Species *displaySpecies) { - // We use a flag in MutationType to indicate whether we're drawing that type or not; we update those flags here, - // before every drawing of mutations, from the vector of mutation type IDs that we keep internally - if (controller_) - { - if (displaySpecies) - { - std::map &muttypes = displaySpecies->mutation_types_; + // We use a flag in MutationType to indicate whether we're drawing that type or not; we update those flags here, + // before every drawing of mutations, from the vector of mutation type IDs that we keep internally + if (controller_) + { + if (displaySpecies) + { + std::map &muttypes = displaySpecies->mutation_types_; std::vector &displayTypes = displayMuttypes(); - - for (auto muttype_iter : muttypes) - { - MutationType *muttype = muttype_iter.second; - - if (displayTypes.size()) - { - slim_objectid_t muttype_id = muttype->mutation_type_id_; - - muttype->mutation_type_displayed_ = (std::find(displayTypes.begin(), displayTypes.end(), muttype_id) != displayTypes.end()); - } - else - { - muttype->mutation_type_displayed_ = true; - } - } - } - } -} - -void QtSLiMChromosomeWidget::glDrawMutations(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange) -{ - double scalingFactor = 0.8; // used to be controller->selectionColorScale; - Population &pop = displaySpecies->population_; - double totalGenomeCount = pop.gui_total_genome_count_; // this includes only genomes in the selected subpopulations - int registry_size; - const MutationIndex *registry = pop.MutationRegistry(®istry_size); - Mutation *mut_block_ptr = gSLiM_Mutation_Block; - - // Set up to draw rects - float colorRed = 0.0f, colorGreen = 0.0f, colorBlue = 0.0f, colorAlpha = 1.0; - - SLIM_GL_PREPARE(); - - if ((registry_size < 1000) || (displayedRange.length < interiorRect.width())) - { - // This is the simple version of the display code, avoiding the memory allocations and such - for (int registry_index = 0; registry_index < registry_size; ++registry_index) - { - const Mutation *mutation = mut_block_ptr + registry[registry_index]; - const MutationType *mutType = mutation->mutation_type_ptr_; - - if (mutType->mutation_type_displayed_) - { - slim_refcount_t mutationRefCount = mutation->gui_reference_count_; // this includes only references made from the selected subpopulations - slim_position_t mutationPosition = mutation->position_; - QRect mutationTickRect = rectEncompassingBaseToBase(mutationPosition, mutationPosition, interiorRect, displayedRange); - - if (!mutType->color_.empty()) - { - colorRed = mutType->color_red_; - colorGreen = mutType->color_green_; - colorBlue = mutType->color_blue_; - } - else - { - RGBForSelectionCoeff(static_cast(mutation->selection_coeff_), &colorRed, &colorGreen, &colorBlue, scalingFactor); - } - - int height_adjust = mutationTickRect.height() - static_cast(ceil((mutationRefCount / totalGenomeCount) * interiorRect.height())); - mutationTickRect.setTop(mutationTickRect.top() + height_adjust); - - SLIM_GL_DEFCOORDS(mutationTickRect); - SLIM_GL_PUSHRECT(); - SLIM_GL_PUSHRECT_COLORS(); - SLIM_GL_CHECKBUFFERS(); - } - } - } - else - { - // We have a lot of mutations, so let's try to be smarter. It's hard to be smarter. The overhead from allocating the NSColors and such - // is pretty negligible; practially all the time is spent in NSRectFill(). Unfortunately, NSRectFillListWithColors() provides basically - // no speedup; Apple doesn't appear to have optimized it. So, here's what I came up with. For each mutation type that uses a fixed DFE, - // and thus a fixed color, we can do a radix sort of mutations into bins corresponding to each pixel in our displayed image. Then we - // can draw each bin just once, making one bar for the highest bar in that bin. Mutations from non-fixed DFEs, and mutations which have - // had their selection coefficient changed, will be drawn at the end in the usual (slow) way. - int displayPixelWidth = interiorRect.width(); - int16_t *heightBuffer = static_cast(malloc(static_cast(displayPixelWidth) * sizeof(int16_t))); - bool *mutationsPlotted = static_cast(calloc(static_cast(registry_size), sizeof(bool))); // faster than using gui_scratch_reference_count_ because of cache locality - int64_t remainingMutations = registry_size; - - // First zero out the scratch refcount, which we use to track which mutations we have drawn already - //for (int mutIndex = 0; mutIndex < mutationCount; ++mutIndex) - // mutations[mutIndex]->gui_scratch_reference_count_ = 0; - - // Then loop through the declared mutation types - std::map &mut_types = displaySpecies->mutation_types_; - bool draw_muttypes_sequentially = (mut_types.size() <= 20); // with a lot of mutation types, the algorithm below becomes very inefficient - - for (auto mutationTypeIter : mut_types) - { - MutationType *mut_type = mutationTypeIter.second; - - if (mut_type->mutation_type_displayed_) - { - if (draw_muttypes_sequentially) - { - bool mut_type_fixed_color = !mut_type->color_.empty(); - - // We optimize fixed-DFE mutation types only, and those using a fixed color set by the user - if ((mut_type->dfe_type_ == DFEType::kFixed) || mut_type_fixed_color) - { - slim_selcoeff_t mut_type_selcoeff = (mut_type_fixed_color ? 0.0 : static_cast(mut_type->dfe_parameters_[0])); - - EIDOS_BZERO(heightBuffer, static_cast(displayPixelWidth) * sizeof(int16_t)); - - // Scan through the mutation list for mutations of this type with the right selcoeff - for (int registry_index = 0; registry_index < registry_size; ++registry_index) - { - const Mutation *mutation = mut_block_ptr + registry[registry_index]; - -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wfloat-equal" -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wfloat-equal" - // We do want to do an exact floating-point equality compare here; we want to see whether the mutation's selcoeff is unmodified from the fixed DFE - if ((mutation->mutation_type_ptr_ == mut_type) && (mut_type_fixed_color || (mutation->selection_coeff_ == mut_type_selcoeff))) -#pragma clang diagnostic pop -#pragma GCC diagnostic pop - { - slim_refcount_t mutationRefCount = mutation->gui_reference_count_; // includes only refs from the selected subpopulations - slim_position_t mutationPosition = mutation->position_; - //NSRect mutationTickRect = [self rectEncompassingBase:mutationPosition toBase:mutationPosition interiorRect:interiorRect displayedRange:displayedRange]; - //int xPos = (int)(mutationTickRect.origin.x - interiorRect.origin.x); - int xPos = LEFT_OFFSET_OF_BASE(mutationPosition, interiorRect, displayedRange); - int16_t barHeight = static_cast(ceil((mutationRefCount / totalGenomeCount) * interiorRect.height())); - - if ((xPos >= 0) && (xPos < displayPixelWidth)) - if (barHeight > heightBuffer[xPos]) - heightBuffer[xPos] = barHeight; - - // tally this mutation as handled - //mutation->gui_scratch_reference_count_ = 1; - mutationsPlotted[registry_index] = true; - --remainingMutations; - } - } - - // Now draw all of the mutations we found, by looping through our radix bins - if (mut_type_fixed_color) - { - colorRed = mut_type->color_red_; - colorGreen = mut_type->color_green_; - colorBlue = mut_type->color_blue_; - } - else - { - RGBForSelectionCoeff(static_cast(mut_type_selcoeff), &colorRed, &colorGreen, &colorBlue, scalingFactor); - } - - for (int binIndex = 0; binIndex < displayPixelWidth; ++binIndex) - { - int barHeight = heightBuffer[binIndex]; - - if (barHeight) - { - QRect mutationTickRect(interiorRect.x() + binIndex, interiorRect.y(), 1, interiorRect.height()); - mutationTickRect.setTop(mutationTickRect.top() + interiorRect.height() - barHeight); - - SLIM_GL_DEFCOORDS(mutationTickRect); - SLIM_GL_PUSHRECT(); - SLIM_GL_PUSHRECT_COLORS(); - SLIM_GL_CHECKBUFFERS(); - } - } - } - } - } - else - { - // We're not displaying this mutation type, so we need to mark off all the mutations belonging to it as handled - for (int registry_index = 0; registry_index < registry_size; ++registry_index) - { - const Mutation *mutation = mut_block_ptr + registry[registry_index]; - - if (mutation->mutation_type_ptr_ == mut_type) - { - // tally this mutation as handled - //mutation->gui_scratch_reference_count_ = 1; - mutationsPlotted[registry_index] = true; - --remainingMutations; - } - } - } - } - - // Draw any undrawn mutations on top; these are guaranteed not to use a fixed color set by the user, since those are all handled above - if (remainingMutations) - { - if (remainingMutations < 1000) - { - // Plot the remainder by brute force, since there are not that many - for (int registry_index = 0; registry_index < registry_size; ++registry_index) - { - //if (mutation->gui_scratch_reference_count_ == 0) - if (!mutationsPlotted[registry_index]) - { - const Mutation *mutation = mut_block_ptr + registry[registry_index]; - slim_refcount_t mutationRefCount = mutation->gui_reference_count_; // this includes only references made from the selected subpopulations - slim_position_t mutationPosition = mutation->position_; - QRect mutationTickRect = rectEncompassingBaseToBase(mutationPosition, mutationPosition, interiorRect, displayedRange); - int height_adjust = mutationTickRect.height() - static_cast(ceil((mutationRefCount / totalGenomeCount) * interiorRect.height())); - - mutationTickRect.setTop(mutationTickRect.top() + height_adjust); - RGBForSelectionCoeff(static_cast(mutation->selection_coeff_), &colorRed, &colorGreen, &colorBlue, scalingFactor); - - SLIM_GL_DEFCOORDS(mutationTickRect); - SLIM_GL_PUSHRECT(); - SLIM_GL_PUSHRECT_COLORS(); - SLIM_GL_CHECKBUFFERS(); - } - } - } - else - { - // OK, we have a lot of mutations left to draw. Here we will again use the radix sort trick, to keep track of only the tallest bar in each column - MutationIndex *mutationBuffer = static_cast(calloc(static_cast(displayPixelWidth), sizeof(MutationIndex))); - - EIDOS_BZERO(heightBuffer, static_cast(displayPixelWidth) * sizeof(int16_t)); - - // Find the tallest bar in each column - for (int registry_index = 0; registry_index < registry_size; ++registry_index) - { - //if (mutation->gui_scratch_reference_count_ == 0) - if (!mutationsPlotted[registry_index]) - { - MutationIndex mutationBlockIndex = registry[registry_index]; - const Mutation *mutation = mut_block_ptr + mutationBlockIndex; - slim_refcount_t mutationRefCount = mutation->gui_reference_count_; // this includes only references made from the selected subpopulations - slim_position_t mutationPosition = mutation->position_; - //NSRect mutationTickRect = [self rectEncompassingBase:mutationPosition toBase:mutationPosition interiorRect:interiorRect displayedRange:displayedRange]; - //int xPos = (int)(mutationTickRect.origin.x - interiorRect.origin.x); - int xPos = LEFT_OFFSET_OF_BASE(mutationPosition, interiorRect, displayedRange); - int16_t barHeight = static_cast(ceil((mutationRefCount / totalGenomeCount) * interiorRect.height())); - - if ((xPos >= 0) && (xPos < displayPixelWidth)) - { - if (barHeight > heightBuffer[xPos]) - { - heightBuffer[xPos] = barHeight; - mutationBuffer[xPos] = mutationBlockIndex; - } - } - } - } - - // Now plot the bars - for (int binIndex = 0; binIndex < displayPixelWidth; ++binIndex) - { - int barHeight = heightBuffer[binIndex]; - - if (barHeight) - { - QRect mutationTickRect(interiorRect.x() + binIndex, interiorRect.y(), 1, interiorRect.height()); - mutationTickRect.setTop(mutationTickRect.top() + interiorRect.height() - barHeight); - - const Mutation *mutation = mut_block_ptr + mutationBuffer[binIndex]; - - RGBForSelectionCoeff(static_cast(mutation->selection_coeff_), &colorRed, &colorGreen, &colorBlue, scalingFactor); - - SLIM_GL_DEFCOORDS(mutationTickRect); - SLIM_GL_PUSHRECT(); - SLIM_GL_PUSHRECT_COLORS(); - SLIM_GL_CHECKBUFFERS(); - } - } - - free(mutationBuffer); - } - } - - free(heightBuffer); - free(mutationsPlotted); - } - - SLIM_GL_FINISH(); -} - -void QtSLiMChromosomeWidget::glDrawFixedSubstitutions(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange) -{ - double scalingFactor = 0.8; // used to be controller->selectionColorScale; - Population &pop = displaySpecies->population_; - Chromosome &chromosome = displaySpecies->TheChromosome(); - bool chromosomeHasDefaultColor = !chromosome.color_sub_.empty(); - std::vector &substitutions = pop.substitutions_; - - // Set up to draw rects - float colorRed = 0.2f, colorGreen = 0.2f, colorBlue = 1.0f, colorAlpha = 1.0; - - if (chromosomeHasDefaultColor) - { - colorRed = chromosome.color_sub_red_; - colorGreen = chromosome.color_sub_green_; - colorBlue = chromosome.color_sub_blue_; - } - - SLIM_GL_PREPARE(); - - if ((substitutions.size() < 1000) || (displayedRange.length < interiorRect.width())) - { - // This is the simple version of the display code, avoiding the memory allocations and such - for (const Substitution *substitution : substitutions) - { - if (substitution->mutation_type_ptr_->mutation_type_displayed_) - { - slim_position_t substitutionPosition = substitution->position_; - QRect substitutionTickRect = rectEncompassingBaseToBase(substitutionPosition, substitutionPosition, interiorRect, displayedRange); - - if (!shouldDrawMutations() || !chromosomeHasDefaultColor) - { - // If we're drawing mutations as well, then substitutions just get colored blue (set above), to contrast - // If we're not drawing mutations as well, then substitutions get colored by selection coefficient, like mutations - const MutationType *mutType = substitution->mutation_type_ptr_; - - if (!mutType->color_sub_.empty()) - { - colorRed = mutType->color_sub_red_; - colorGreen = mutType->color_sub_green_; - colorBlue = mutType->color_sub_blue_; - } - else - { - RGBForSelectionCoeff(static_cast(substitution->selection_coeff_), &colorRed, &colorGreen, &colorBlue, scalingFactor); - } - } - - SLIM_GL_DEFCOORDS(substitutionTickRect); - SLIM_GL_PUSHRECT(); - SLIM_GL_PUSHRECT_COLORS(); - SLIM_GL_CHECKBUFFERS(); - } - } - } - else - { - // We have a lot of substitutions, so do a radix sort, as we do in drawMutationsInInteriorRect: below. - int displayPixelWidth = interiorRect.width(); - const Substitution **subBuffer = static_cast(calloc(static_cast(displayPixelWidth), sizeof(Substitution *))); - - for (const Substitution *substitution : substitutions) - { - if (substitution->mutation_type_ptr_->mutation_type_displayed_) - { - slim_position_t substitutionPosition = substitution->position_; - double startFraction = (substitutionPosition - static_cast(displayedRange.location)) / static_cast(displayedRange.length); - int xPos = static_cast(floor(startFraction * interiorRect.width())); - - if ((xPos >= 0) && (xPos < displayPixelWidth)) - subBuffer[xPos] = substitution; - } - } - - if (shouldDrawMutations() && chromosomeHasDefaultColor) - { - // If we're drawing mutations as well, then substitutions just get colored blue, to contrast - QRect mutationTickRect = interiorRect; - for (int binIndex = 0; binIndex < displayPixelWidth; ++binIndex) - { - const Substitution *substitution = subBuffer[binIndex]; - - if (substitution) - { - mutationTickRect.setX(interiorRect.x() + binIndex); - mutationTickRect.setWidth(1); - - // consolidate adjacent lines together, since they are all the same color - while ((binIndex + 1 < displayPixelWidth) && subBuffer[binIndex + 1]) - { - mutationTickRect.setWidth(mutationTickRect.width() + 1); - binIndex++; - } - - SLIM_GL_DEFCOORDS(mutationTickRect); - SLIM_GL_PUSHRECT(); - SLIM_GL_PUSHRECT_COLORS(); - SLIM_GL_CHECKBUFFERS(); - } - } - } - else - { - // If we're not drawing mutations as well, then substitutions get colored by selection coefficient, like mutations - QRect mutationTickRect = interiorRect; - - for (int binIndex = 0; binIndex < displayPixelWidth; ++binIndex) - { - const Substitution *substitution = subBuffer[binIndex]; - - if (substitution) - { - const MutationType *mutType = substitution->mutation_type_ptr_; - - if (!mutType->color_sub_.empty()) - { - colorRed = mutType->color_sub_red_; - colorGreen = mutType->color_sub_green_; - colorBlue = mutType->color_sub_blue_; - } - else - { - RGBForSelectionCoeff(static_cast(substitution->selection_coeff_), &colorRed, &colorGreen, &colorBlue, scalingFactor); - } - - mutationTickRect.setX(interiorRect.x() + binIndex); - mutationTickRect.setWidth(1); - SLIM_GL_DEFCOORDS(mutationTickRect); - SLIM_GL_PUSHRECT(); - SLIM_GL_PUSHRECT_COLORS(); - SLIM_GL_CHECKBUFFERS(); - } - } - } - - free(subBuffer); - } - - SLIM_GL_FINISH(); -} - -void QtSLiMChromosomeWidget::_glDrawRateMapIntervals(QRect &interiorRect, __attribute__((__unused__)) Species *displaySpecies, QtSLiMRange displayedRange, std::vector &ends, std::vector &rates, double hue) -{ - size_t recombinationIntervalCount = ends.size(); - slim_position_t intervalStartPosition = 0; - int previousIntervalLeftEdge = -10000; - - SLIM_GL_PREPARE(); - - for (size_t interval = 0; interval < recombinationIntervalCount; ++interval) - { - slim_position_t intervalEndPosition = ends[interval]; - double intervalRate = rates[interval]; - QRect intervalRect = rectEncompassingBaseToBase(intervalStartPosition, intervalEndPosition, interiorRect, displayedRange); - bool widthOne = (intervalRect.width() == 1); - - // We want to avoid overdrawing width-one intervals, which are important but small, so if the previous interval was width-one, - // and we are not, and we are about to overdraw it, then we scoot our left edge over one pixel to leave it alone. - if (!widthOne && (intervalRect.left() == previousIntervalLeftEdge)) - intervalRect.adjust(1, 0, 0, 0); - - // draw only the visible part, if any - intervalRect = intervalRect.intersected(interiorRect); - - if (!intervalRect.isEmpty()) - { - // color according to how "hot" the region is - float colorRed, colorGreen, colorBlue, colorAlpha; - - if (intervalRate == 0.0) - { - // a recombination or mutation rate of 0.0 comes out as black, whereas the lowest brightness below is 0.5; we want to distinguish this - colorRed = colorGreen = colorBlue = 0.0; - colorAlpha = 1.0; - } - else - { - // the formula below scales 1e-6 to 1.0 and 1e-9 to 0.0; values outside that range clip, but we want there to be - // reasonable contrast within the range of values commonly used, so we don't want to make the range too wide - double lightness, brightness = 1.0, saturation = 1.0; - - lightness = (log10(intervalRate) + 9.0) / 3.0; - lightness = std::max(lightness, 0.0); - lightness = std::min(lightness, 1.0); - - if (lightness >= 0.5) saturation = 1.0 - ((lightness - 0.5) * 2.0); // goes from 1.0 at lightness 0.5 to 0.0 at lightness 1.0 - else brightness = 0.5 + lightness; // goes from 1.0 at lightness 0.5 to 0.5 at lightness 0.0 - - QColor intervalColor = QtSLiMColorWithHSV(hue, saturation, brightness, 1.0); - -#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) - // In Qt5, getRgbF() expects pointers to qreal, which is double - double r, g, b, a; - intervalColor.getRgbF(&r, &g, &b, &a); + for (auto muttype_iter : muttypes) + { + MutationType *muttype = muttype_iter.second; - colorRed = static_cast(r); - colorGreen = static_cast(g); - colorBlue = static_cast(b); - colorAlpha = static_cast(a); -#else - // In Qt6, getRgbF() expects pointers to float - intervalColor.getRgbF(&colorRed, &colorGreen, &colorBlue, &colorAlpha); -#endif - } - - SLIM_GL_DEFCOORDS(intervalRect); - SLIM_GL_PUSHRECT(); - SLIM_GL_PUSHRECT_COLORS(); - SLIM_GL_CHECKBUFFERS(); - - // if this interval is just one pixel wide, we want to try to make it visible, by avoiding overdrawing it; so we remember its location - if (widthOne) - previousIntervalLeftEdge = intervalRect.left(); - else - previousIntervalLeftEdge = -10000; - } - - // the next interval starts at the next base after this one ended - intervalStartPosition = intervalEndPosition + 1; - } - - SLIM_GL_FINISH(); -} - -void QtSLiMChromosomeWidget::glDrawRecombinationIntervals(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange) -{ - Chromosome &chromosome = displaySpecies->TheChromosome(); - - if (chromosome.single_recombination_map_) - { - _glDrawRateMapIntervals(interiorRect, displaySpecies, displayedRange, chromosome.recombination_end_positions_H_, chromosome.recombination_rates_H_, 0.65); - } - else - { - QRect topInteriorRect = interiorRect, bottomInteriorRect = interiorRect; - int halfHeight = static_cast(ceil(interiorRect.height() / 2.0)); - int remainingHeight = interiorRect.height() - halfHeight; - - topInteriorRect.setHeight(halfHeight); - bottomInteriorRect.setHeight(remainingHeight); - bottomInteriorRect.translate(0, halfHeight); - - _glDrawRateMapIntervals(topInteriorRect, displaySpecies, displayedRange, chromosome.recombination_end_positions_M_, chromosome.recombination_rates_M_, 0.65); - _glDrawRateMapIntervals(bottomInteriorRect, displaySpecies, displayedRange, chromosome.recombination_end_positions_F_, chromosome.recombination_rates_F_, 0.65); - } -} - -void QtSLiMChromosomeWidget::glDrawMutationIntervals(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange) -{ - Chromosome &chromosome = displaySpecies->TheChromosome(); - - if (chromosome.single_mutation_map_) - { - _glDrawRateMapIntervals(interiorRect, displaySpecies, displayedRange, chromosome.mutation_end_positions_H_, chromosome.mutation_rates_H_, 0.75); - } - else - { - QRect topInteriorRect = interiorRect, bottomInteriorRect = interiorRect; - int halfHeight = static_cast(ceil(interiorRect.height() / 2.0)); - int remainingHeight = interiorRect.height() - halfHeight; - - topInteriorRect.setHeight(halfHeight); - bottomInteriorRect.setHeight(remainingHeight); - bottomInteriorRect.translate(0, halfHeight); - - _glDrawRateMapIntervals(topInteriorRect, displaySpecies, displayedRange, chromosome.mutation_end_positions_M_, chromosome.mutation_rates_M_, 0.75); - _glDrawRateMapIntervals(bottomInteriorRect, displaySpecies, displayedRange, chromosome.mutation_end_positions_F_, chromosome.mutation_rates_F_, 0.75); - } -} - -void QtSLiMChromosomeWidget::glDrawRateMaps(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange) -{ - Chromosome &chromosome = displaySpecies->TheChromosome(); - bool recombinationWorthShowing = false; - bool mutationWorthShowing = false; - - if (chromosome.single_mutation_map_) - mutationWorthShowing = (chromosome.mutation_end_positions_H_.size() > 1); - else - mutationWorthShowing = ((chromosome.mutation_end_positions_M_.size() > 1) || (chromosome.mutation_end_positions_F_.size() > 1)); - - if (chromosome.single_recombination_map_) - recombinationWorthShowing = (chromosome.recombination_end_positions_H_.size() > 1); - else - recombinationWorthShowing = ((chromosome.recombination_end_positions_M_.size() > 1) || (chromosome.recombination_end_positions_F_.size() > 1)); - - // If neither map is worth showing, we show just the recombination map, to mirror the behavior of 2.4 and earlier - if ((!mutationWorthShowing && !recombinationWorthShowing) || (!mutationWorthShowing && recombinationWorthShowing)) - { - glDrawRecombinationIntervals(interiorRect, displaySpecies, displayedRange); - } - else if (mutationWorthShowing && !recombinationWorthShowing) - { - glDrawMutationIntervals(interiorRect, displaySpecies, displayedRange); - } - else // mutationWorthShowing && recombinationWorthShowing - { - QRect topInteriorRect = interiorRect, bottomInteriorRect = interiorRect; - int halfHeight = static_cast(ceil(interiorRect.height() / 2.0)); - int remainingHeight = interiorRect.height() - halfHeight; - - topInteriorRect.setHeight(halfHeight); - bottomInteriorRect.setHeight(remainingHeight); - bottomInteriorRect.translate(0, halfHeight); - - glDrawRecombinationIntervals(topInteriorRect, displaySpecies, displayedRange); - glDrawMutationIntervals(bottomInteriorRect, displaySpecies, displayedRange); - } + if (displayTypes.size()) + { + slim_objectid_t muttype_id = muttype->mutation_type_id_; + + muttype->mutation_type_displayed_ = (std::find(displayTypes.begin(), displayTypes.end(), muttype_id) != displayTypes.end()); + } + else + { + muttype->mutation_type_displayed_ = true; + } + } + } + } } void QtSLiMChromosomeWidget::overlaySelection(QRect interiorRect, QtSLiMRange displayedRange, QPainter &painter) diff --git a/QtSLiM/QtSLiMChromosomeWidget.h b/QtSLiM/QtSLiMChromosomeWidget.h index 18d4c9cf..df03a1ae 100644 --- a/QtSLiM/QtSLiMChromosomeWidget.h +++ b/QtSLiM/QtSLiMChromosomeWidget.h @@ -24,8 +24,11 @@ #define GL_SILENCE_DEPRECATION #include + +#ifndef SLIM_NO_OPENGL #include #include +#endif #include "slim_globals.h" @@ -37,6 +40,7 @@ class QtSLiMHaplotypeManager; class QPainter; class QContextMenuEvent; + struct QtSLiMRange { int64_t location, length; @@ -44,7 +48,17 @@ struct QtSLiMRange explicit QtSLiMRange(int64_t p_location, int64_t p_length) : location(p_location), length(p_length) {} }; + +// This is a fast macro for when all we need is the offset of a base from the left edge of interiorRect; interiorRect.origin.x is not added here! +// This is based on the same math as rectEncompassingBase:toBase:interiorRect:displayedRange:, and must be kept in synch with that method. +#define LEFT_OFFSET_OF_BASE(startBase, interiorRect, displayedRange) (static_cast(floor(((startBase - static_cast(displayedRange.location)) / static_cast(displayedRange.length)) * interiorRect.width()))) + + +#ifndef SLIM_NO_OPENGL class QtSLiMChromosomeWidget : public QOpenGLWidget, protected QOpenGLFunctions +#else +class QtSLiMChromosomeWidget : public QWidget +#endif { Q_OBJECT @@ -68,10 +82,6 @@ class QtSLiMChromosomeWidget : public QOpenGLWidget, protected QOpenGLFunctions int trackingXAdjust_ = 0; // to keep the cursor stuck on a knob that is click-dragged //SLiMSelectionMarker *startMarker, *endMarker; - // OpenGL buffers - float *glArrayVertices = nullptr; - float *glArrayColors = nullptr; - // Haplotype display int64_t *haplotype_previous_bincounts = nullptr; // used by QtSLiMHaplotypeManager to keep the sort order stable QtSLiMHaplotypeManager *haplotype_mgr_ = nullptr; // the haplotype manager constructed for the current display; cached @@ -100,9 +110,13 @@ class QtSLiMChromosomeWidget : public QOpenGLWidget, protected QOpenGLFunctions void selectedRangeChanged(void); protected: +#ifndef SLIM_NO_OPENGL virtual void initializeGL() override; virtual void resizeGL(int w, int h) override; virtual void paintGL() override; +#else + virtual void paintEvent(QPaintEvent *event) override; +#endif QRect rectEncompassingBaseToBase(slim_position_t startBase, slim_position_t endBase, QRect interiorRect, QtSLiMRange displayedRange); slim_position_t baseForPosition(double position, QRect interiorRect, QtSLiMRange displayedRange); @@ -111,17 +125,29 @@ class QtSLiMChromosomeWidget : public QOpenGLWidget, protected QOpenGLFunctions void drawTicksInContentRect(QRect contentRect, Species *displaySpecies, QtSLiMRange displayedRange, QPainter &painter); void overlaySelection(QRect interiorRect, QtSLiMRange displayedRange, QPainter &painter); - void glDrawRect(Species *displaySpecies); + void updateDisplayedMutationTypes(Species *displaySpecies); + // OpenGL drawing; this is the primary drawing code +#ifndef SLIM_NO_OPENGL + void glDrawRect(Species *displaySpecies); void glDrawGenomicElements(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange); - void updateDisplayedMutationTypes(Species *displaySpecies); void glDrawFixedSubstitutions(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange); void glDrawMutations(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange); - void _glDrawRateMapIntervals(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange, std::vector &ends, std::vector &rates, double hue); void glDrawRecombinationIntervals(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange); void glDrawMutationIntervals(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange); void glDrawRateMaps(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange); +#endif + + // Qt-based drawing, provided as a backup if OpenGL has problems on a given platform + void qtDrawRect(Species *displaySpecies, QPainter &painter); + void qtDrawGenomicElements(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange, QPainter &painter); + void qtDrawFixedSubstitutions(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange, QPainter &painter); + void qtDrawMutations(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange, QPainter &painter); + void _qtDrawRateMapIntervals(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange, std::vector &ends, std::vector &rates, double hue, QPainter &painter); + void qtDrawRecombinationIntervals(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange, QPainter &painter); + void qtDrawMutationIntervals(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange, QPainter &painter); + void qtDrawRateMaps(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange, QPainter &painter); virtual void mousePressEvent(QMouseEvent *p_event) override; void _mouseTrackEvent(QMouseEvent *p_event); diff --git a/QtSLiM/QtSLiMChromosomeWidget_GL.cpp b/QtSLiM/QtSLiMChromosomeWidget_GL.cpp new file mode 100644 index 00000000..1fa87550 --- /dev/null +++ b/QtSLiM/QtSLiMChromosomeWidget_GL.cpp @@ -0,0 +1,768 @@ +// +// QtSLiMChromosomeWidget_GL.cpp +// SLiM +// +// Created by Ben Haller on 8/25/2024. +// Copyright (c) 2024 Philipp Messer. All rights reserved. +// A product of the Messer Lab, http://messerlab.org/slim/ +// + +// This file is part of SLiM. +// +// SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +// +// SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with SLiM. If not, see . + + +#ifndef SLIM_NO_OPENGL + +#include "QtSLiMChromosomeWidget.h" +#include "QtSLiMHaplotypeManager.h" +#include "QtSLiMOpenGL.h" + +#include + +#include +#include +#include + + +// +// OpenGL-based drawing; maintain this in parallel with the Qt-based drawing! +// + +void QtSLiMChromosomeWidget::glDrawRect(Species *displaySpecies) +{ + bool ready = isEnabled() && !controller_->invalidSimulation(); + QRect interiorRect = getInteriorRect(); + + // if the simulation is at tick 0, it is not ready + if (ready) + if (controller_->community->Tick() == 0) + ready = false; + + if (ready) + { + // erase the content area itself + glColor3f(0.0f, 0.0f, 0.0f); + glRecti(interiorRect.left(), interiorRect.top(), interiorRect.left() + interiorRect.width(), interiorRect.top() + interiorRect.height()); + + QtSLiMRange displayedRange = getDisplayedRange(displaySpecies); + + bool splitHeight = (shouldDrawRateMaps() && shouldDrawGenomicElements()); + QRect topInteriorRect = interiorRect, bottomInteriorRect = interiorRect; + int halfHeight = static_cast(ceil(interiorRect.height() / 2.0)); + int remainingHeight = interiorRect.height() - halfHeight; + + topInteriorRect.setHeight(halfHeight); + bottomInteriorRect.setHeight(remainingHeight); + bottomInteriorRect.translate(0, halfHeight); + + // draw recombination intervals in interior + if (shouldDrawRateMaps()) + glDrawRateMaps(splitHeight ? topInteriorRect : interiorRect, displaySpecies, displayedRange); + + // draw genomic elements in interior + if (shouldDrawGenomicElements()) + glDrawGenomicElements(splitHeight ? bottomInteriorRect : interiorRect, displaySpecies, displayedRange); + + // figure out which mutation types we're displaying + if (shouldDrawFixedSubstitutions() || shouldDrawMutations()) + updateDisplayedMutationTypes(displaySpecies); + + // draw fixed substitutions in interior + if (shouldDrawFixedSubstitutions()) + glDrawFixedSubstitutions(interiorRect, displaySpecies, displayedRange); + + // draw mutations in interior + if (shouldDrawMutations()) + { + if (displayHaplotypes()) + { + // display mutations as a haplotype plot, courtesy of QtSLiMHaplotypeManager; we use ClusterNearestNeighbor and + // ClusterNoOptimization because they're fast, and NN might also provide a bit more run-to-run continuity + // we cache the haplotype manager here, so our display remains constant across window resizes and other + // invalidations; we toss the cache only when the simulation tells us that the model state has changed + if (!haplotype_mgr_) + { + size_t interiorHeight = static_cast(interiorRect.height()); // one sample per available pixel line, for simplicity and speed; 47, in the current UI layout + haplotype_mgr_ = new QtSLiMHaplotypeManager(nullptr, QtSLiMHaplotypeManager::ClusterNearestNeighbor, QtSLiMHaplotypeManager::ClusterNoOptimization, controller_, displaySpecies, interiorHeight, false); + } + + if (haplotype_mgr_) + haplotype_mgr_->glDrawHaplotypes(interiorRect, false, false, false, &haplotype_previous_bincounts); + } + else + { + // display mutations as a frequency plot; this is the standard display mode + glDrawMutations(interiorRect, displaySpecies, displayedRange); + } + } + } + else + { + // erase the content area itself + glColor3f(0.88f, 0.88f, 0.88f); + glRecti(0, 0, interiorRect.width(), interiorRect.height()); + } +} + +void QtSLiMChromosomeWidget::glDrawGenomicElements(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange) +{ + Chromosome &chromosome = displaySpecies->TheChromosome(); + int previousIntervalLeftEdge = -10000; + + SLIM_GL_PREPARE(); + + for (GenomicElement *genomicElement : chromosome.GenomicElements()) + { + slim_position_t startPosition = genomicElement->start_position_; + slim_position_t endPosition = genomicElement->end_position_; + QRect elementRect = rectEncompassingBaseToBase(startPosition, endPosition, interiorRect, displayedRange); + bool widthOne = (elementRect.width() == 1); + + // We want to avoid overdrawing width-one intervals, which are important but small, so if the previous interval was width-one, + // and we are not, and we are about to overdraw it, then we scoot our left edge over one pixel to leave it alone. + if (!widthOne && (elementRect.left() == previousIntervalLeftEdge)) + elementRect.adjust(1, 0, 0, 0); + + // draw only the visible part, if any + elementRect = elementRect.intersected(interiorRect); + + if (!elementRect.isEmpty()) + { + GenomicElementType *geType = genomicElement->genomic_element_type_ptr_; + float colorRed, colorGreen, colorBlue, colorAlpha; + + if (!geType->color_.empty()) + { + colorRed = geType->color_red_; + colorGreen = geType->color_green_; + colorBlue = geType->color_blue_; + colorAlpha = 1.0; + } + else + { + slim_objectid_t elementTypeID = geType->genomic_element_type_id_; + + controller_->colorForGenomicElementType(geType, elementTypeID, &colorRed, &colorGreen, &colorBlue, &colorAlpha); + } + + SLIM_GL_DEFCOORDS(elementRect); + SLIM_GL_PUSHRECT(); + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + + // if this interval is just one pixel wide, we want to try to make it visible, by avoiding overdrawing it; so we remember its location + if (widthOne) + previousIntervalLeftEdge = elementRect.left(); + else + previousIntervalLeftEdge = -10000; + } + } + + SLIM_GL_FINISH(); +} + +void QtSLiMChromosomeWidget::glDrawMutations(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange) +{ + double scalingFactor = 0.8; // used to be controller->selectionColorScale; + Population &pop = displaySpecies->population_; + double totalGenomeCount = pop.gui_total_genome_count_; // this includes only genomes in the selected subpopulations + int registry_size; + const MutationIndex *registry = pop.MutationRegistry(®istry_size); + Mutation *mut_block_ptr = gSLiM_Mutation_Block; + + // Set up to draw rects + float colorRed = 0.0f, colorGreen = 0.0f, colorBlue = 0.0f, colorAlpha = 1.0; + + SLIM_GL_PREPARE(); + + if ((registry_size < 1000) || (displayedRange.length < interiorRect.width())) + { + // This is the simple version of the display code, avoiding the memory allocations and such + for (int registry_index = 0; registry_index < registry_size; ++registry_index) + { + const Mutation *mutation = mut_block_ptr + registry[registry_index]; + const MutationType *mutType = mutation->mutation_type_ptr_; + + if (mutType->mutation_type_displayed_) + { + slim_refcount_t mutationRefCount = mutation->gui_reference_count_; // this includes only references made from the selected subpopulations + slim_position_t mutationPosition = mutation->position_; + QRect mutationTickRect = rectEncompassingBaseToBase(mutationPosition, mutationPosition, interiorRect, displayedRange); + + if (!mutType->color_.empty()) + { + colorRed = mutType->color_red_; + colorGreen = mutType->color_green_; + colorBlue = mutType->color_blue_; + } + else + { + RGBForSelectionCoeff(static_cast(mutation->selection_coeff_), &colorRed, &colorGreen, &colorBlue, scalingFactor); + } + + int height_adjust = mutationTickRect.height() - static_cast(ceil((mutationRefCount / totalGenomeCount) * interiorRect.height())); + mutationTickRect.setTop(mutationTickRect.top() + height_adjust); + + SLIM_GL_DEFCOORDS(mutationTickRect); + SLIM_GL_PUSHRECT(); + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + } + } + } + else + { + // We have a lot of mutations, so let's try to be smarter. It's hard to be smarter. The overhead from allocating the NSColors and such + // is pretty negligible; practially all the time is spent in NSRectFill(). Unfortunately, NSRectFillListWithColors() provides basically + // no speedup; Apple doesn't appear to have optimized it. So, here's what I came up with. For each mutation type that uses a fixed DFE, + // and thus a fixed color, we can do a radix sort of mutations into bins corresponding to each pixel in our displayed image. Then we + // can draw each bin just once, making one bar for the highest bar in that bin. Mutations from non-fixed DFEs, and mutations which have + // had their selection coefficient changed, will be drawn at the end in the usual (slow) way. + int displayPixelWidth = interiorRect.width(); + int16_t *heightBuffer = static_cast(malloc(static_cast(displayPixelWidth) * sizeof(int16_t))); + bool *mutationsPlotted = static_cast(calloc(static_cast(registry_size), sizeof(bool))); // faster than using gui_scratch_reference_count_ because of cache locality + int64_t remainingMutations = registry_size; + + // First zero out the scratch refcount, which we use to track which mutations we have drawn already + //for (int mutIndex = 0; mutIndex < mutationCount; ++mutIndex) + // mutations[mutIndex]->gui_scratch_reference_count_ = 0; + + // Then loop through the declared mutation types + std::map &mut_types = displaySpecies->mutation_types_; + bool draw_muttypes_sequentially = (mut_types.size() <= 20); // with a lot of mutation types, the algorithm below becomes very inefficient + + for (auto mutationTypeIter : mut_types) + { + MutationType *mut_type = mutationTypeIter.second; + + if (mut_type->mutation_type_displayed_) + { + if (draw_muttypes_sequentially) + { + bool mut_type_fixed_color = !mut_type->color_.empty(); + + // We optimize fixed-DFE mutation types only, and those using a fixed color set by the user + if ((mut_type->dfe_type_ == DFEType::kFixed) || mut_type_fixed_color) + { + slim_selcoeff_t mut_type_selcoeff = (mut_type_fixed_color ? 0.0 : static_cast(mut_type->dfe_parameters_[0])); + + EIDOS_BZERO(heightBuffer, static_cast(displayPixelWidth) * sizeof(int16_t)); + + // Scan through the mutation list for mutations of this type with the right selcoeff + for (int registry_index = 0; registry_index < registry_size; ++registry_index) + { + const Mutation *mutation = mut_block_ptr + registry[registry_index]; + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wfloat-equal" +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wfloat-equal" + // We do want to do an exact floating-point equality compare here; we want to see whether the mutation's selcoeff is unmodified from the fixed DFE + if ((mutation->mutation_type_ptr_ == mut_type) && (mut_type_fixed_color || (mutation->selection_coeff_ == mut_type_selcoeff))) +#pragma clang diagnostic pop +#pragma GCC diagnostic pop + { + slim_refcount_t mutationRefCount = mutation->gui_reference_count_; // includes only refs from the selected subpopulations + slim_position_t mutationPosition = mutation->position_; + //NSRect mutationTickRect = [self rectEncompassingBase:mutationPosition toBase:mutationPosition interiorRect:interiorRect displayedRange:displayedRange]; + //int xPos = (int)(mutationTickRect.origin.x - interiorRect.origin.x); + int xPos = LEFT_OFFSET_OF_BASE(mutationPosition, interiorRect, displayedRange); + int16_t barHeight = static_cast(ceil((mutationRefCount / totalGenomeCount) * interiorRect.height())); + + if ((xPos >= 0) && (xPos < displayPixelWidth)) + if (barHeight > heightBuffer[xPos]) + heightBuffer[xPos] = barHeight; + + // tally this mutation as handled + //mutation->gui_scratch_reference_count_ = 1; + mutationsPlotted[registry_index] = true; + --remainingMutations; + } + } + + // Now draw all of the mutations we found, by looping through our radix bins + if (mut_type_fixed_color) + { + colorRed = mut_type->color_red_; + colorGreen = mut_type->color_green_; + colorBlue = mut_type->color_blue_; + } + else + { + RGBForSelectionCoeff(static_cast(mut_type_selcoeff), &colorRed, &colorGreen, &colorBlue, scalingFactor); + } + + for (int binIndex = 0; binIndex < displayPixelWidth; ++binIndex) + { + int barHeight = heightBuffer[binIndex]; + + if (barHeight) + { + QRect mutationTickRect(interiorRect.x() + binIndex, interiorRect.y(), 1, interiorRect.height()); + mutationTickRect.setTop(mutationTickRect.top() + interiorRect.height() - barHeight); + + SLIM_GL_DEFCOORDS(mutationTickRect); + SLIM_GL_PUSHRECT(); + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + } + } + } + } + } + else + { + // We're not displaying this mutation type, so we need to mark off all the mutations belonging to it as handled + for (int registry_index = 0; registry_index < registry_size; ++registry_index) + { + const Mutation *mutation = mut_block_ptr + registry[registry_index]; + + if (mutation->mutation_type_ptr_ == mut_type) + { + // tally this mutation as handled + //mutation->gui_scratch_reference_count_ = 1; + mutationsPlotted[registry_index] = true; + --remainingMutations; + } + } + } + } + + // Draw any undrawn mutations on top; these are guaranteed not to use a fixed color set by the user, since those are all handled above + if (remainingMutations) + { + if (remainingMutations < 1000) + { + // Plot the remainder by brute force, since there are not that many + for (int registry_index = 0; registry_index < registry_size; ++registry_index) + { + //if (mutation->gui_scratch_reference_count_ == 0) + if (!mutationsPlotted[registry_index]) + { + const Mutation *mutation = mut_block_ptr + registry[registry_index]; + slim_refcount_t mutationRefCount = mutation->gui_reference_count_; // this includes only references made from the selected subpopulations + slim_position_t mutationPosition = mutation->position_; + QRect mutationTickRect = rectEncompassingBaseToBase(mutationPosition, mutationPosition, interiorRect, displayedRange); + int height_adjust = mutationTickRect.height() - static_cast(ceil((mutationRefCount / totalGenomeCount) * interiorRect.height())); + + mutationTickRect.setTop(mutationTickRect.top() + height_adjust); + RGBForSelectionCoeff(static_cast(mutation->selection_coeff_), &colorRed, &colorGreen, &colorBlue, scalingFactor); + + SLIM_GL_DEFCOORDS(mutationTickRect); + SLIM_GL_PUSHRECT(); + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + } + } + } + else + { + // OK, we have a lot of mutations left to draw. Here we will again use the radix sort trick, to keep track of only the tallest bar in each column + MutationIndex *mutationBuffer = static_cast(calloc(static_cast(displayPixelWidth), sizeof(MutationIndex))); + + EIDOS_BZERO(heightBuffer, static_cast(displayPixelWidth) * sizeof(int16_t)); + + // Find the tallest bar in each column + for (int registry_index = 0; registry_index < registry_size; ++registry_index) + { + //if (mutation->gui_scratch_reference_count_ == 0) + if (!mutationsPlotted[registry_index]) + { + MutationIndex mutationBlockIndex = registry[registry_index]; + const Mutation *mutation = mut_block_ptr + mutationBlockIndex; + slim_refcount_t mutationRefCount = mutation->gui_reference_count_; // this includes only references made from the selected subpopulations + slim_position_t mutationPosition = mutation->position_; + //NSRect mutationTickRect = [self rectEncompassingBase:mutationPosition toBase:mutationPosition interiorRect:interiorRect displayedRange:displayedRange]; + //int xPos = (int)(mutationTickRect.origin.x - interiorRect.origin.x); + int xPos = LEFT_OFFSET_OF_BASE(mutationPosition, interiorRect, displayedRange); + int16_t barHeight = static_cast(ceil((mutationRefCount / totalGenomeCount) * interiorRect.height())); + + if ((xPos >= 0) && (xPos < displayPixelWidth)) + { + if (barHeight > heightBuffer[xPos]) + { + heightBuffer[xPos] = barHeight; + mutationBuffer[xPos] = mutationBlockIndex; + } + } + } + } + + // Now plot the bars + for (int binIndex = 0; binIndex < displayPixelWidth; ++binIndex) + { + int barHeight = heightBuffer[binIndex]; + + if (barHeight) + { + QRect mutationTickRect(interiorRect.x() + binIndex, interiorRect.y(), 1, interiorRect.height()); + mutationTickRect.setTop(mutationTickRect.top() + interiorRect.height() - barHeight); + + const Mutation *mutation = mut_block_ptr + mutationBuffer[binIndex]; + + RGBForSelectionCoeff(static_cast(mutation->selection_coeff_), &colorRed, &colorGreen, &colorBlue, scalingFactor); + + SLIM_GL_DEFCOORDS(mutationTickRect); + SLIM_GL_PUSHRECT(); + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + } + } + + free(mutationBuffer); + } + } + + free(heightBuffer); + free(mutationsPlotted); + } + + SLIM_GL_FINISH(); +} + +void QtSLiMChromosomeWidget::glDrawFixedSubstitutions(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange) +{ + double scalingFactor = 0.8; // used to be controller->selectionColorScale; + Population &pop = displaySpecies->population_; + Chromosome &chromosome = displaySpecies->TheChromosome(); + bool chromosomeHasDefaultColor = !chromosome.color_sub_.empty(); + std::vector &substitutions = pop.substitutions_; + + // Set up to draw rects + float colorRed = 0.2f, colorGreen = 0.2f, colorBlue = 1.0f, colorAlpha = 1.0; + + if (chromosomeHasDefaultColor) + { + colorRed = chromosome.color_sub_red_; + colorGreen = chromosome.color_sub_green_; + colorBlue = chromosome.color_sub_blue_; + } + + SLIM_GL_PREPARE(); + + if ((substitutions.size() < 1000) || (displayedRange.length < interiorRect.width())) + { + // This is the simple version of the display code, avoiding the memory allocations and such + for (const Substitution *substitution : substitutions) + { + if (substitution->mutation_type_ptr_->mutation_type_displayed_) + { + slim_position_t substitutionPosition = substitution->position_; + QRect substitutionTickRect = rectEncompassingBaseToBase(substitutionPosition, substitutionPosition, interiorRect, displayedRange); + + if (!shouldDrawMutations() || !chromosomeHasDefaultColor) + { + // If we're drawing mutations as well, then substitutions just get colored blue (set above), to contrast + // If we're not drawing mutations as well, then substitutions get colored by selection coefficient, like mutations + const MutationType *mutType = substitution->mutation_type_ptr_; + + if (!mutType->color_sub_.empty()) + { + colorRed = mutType->color_sub_red_; + colorGreen = mutType->color_sub_green_; + colorBlue = mutType->color_sub_blue_; + } + else + { + RGBForSelectionCoeff(static_cast(substitution->selection_coeff_), &colorRed, &colorGreen, &colorBlue, scalingFactor); + } + } + + SLIM_GL_DEFCOORDS(substitutionTickRect); + SLIM_GL_PUSHRECT(); + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + } + } + } + else + { + // We have a lot of substitutions, so do a radix sort, as we do in drawMutationsInInteriorRect: below. + int displayPixelWidth = interiorRect.width(); + const Substitution **subBuffer = static_cast(calloc(static_cast(displayPixelWidth), sizeof(Substitution *))); + + for (const Substitution *substitution : substitutions) + { + if (substitution->mutation_type_ptr_->mutation_type_displayed_) + { + slim_position_t substitutionPosition = substitution->position_; + double startFraction = (substitutionPosition - static_cast(displayedRange.location)) / static_cast(displayedRange.length); + int xPos = static_cast(floor(startFraction * interiorRect.width())); + + if ((xPos >= 0) && (xPos < displayPixelWidth)) + subBuffer[xPos] = substitution; + } + } + + if (shouldDrawMutations() && chromosomeHasDefaultColor) + { + // If we're drawing mutations as well, then substitutions just get colored blue, to contrast + QRect mutationTickRect = interiorRect; + + for (int binIndex = 0; binIndex < displayPixelWidth; ++binIndex) + { + const Substitution *substitution = subBuffer[binIndex]; + + if (substitution) + { + mutationTickRect.setX(interiorRect.x() + binIndex); + mutationTickRect.setWidth(1); + + // consolidate adjacent lines together, since they are all the same color + while ((binIndex + 1 < displayPixelWidth) && subBuffer[binIndex + 1]) + { + mutationTickRect.setWidth(mutationTickRect.width() + 1); + binIndex++; + } + + SLIM_GL_DEFCOORDS(mutationTickRect); + SLIM_GL_PUSHRECT(); + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + } + } + } + else + { + // If we're not drawing mutations as well, then substitutions get colored by selection coefficient, like mutations + QRect mutationTickRect = interiorRect; + + for (int binIndex = 0; binIndex < displayPixelWidth; ++binIndex) + { + const Substitution *substitution = subBuffer[binIndex]; + + if (substitution) + { + const MutationType *mutType = substitution->mutation_type_ptr_; + + if (!mutType->color_sub_.empty()) + { + colorRed = mutType->color_sub_red_; + colorGreen = mutType->color_sub_green_; + colorBlue = mutType->color_sub_blue_; + } + else + { + RGBForSelectionCoeff(static_cast(substitution->selection_coeff_), &colorRed, &colorGreen, &colorBlue, scalingFactor); + } + + mutationTickRect.setX(interiorRect.x() + binIndex); + mutationTickRect.setWidth(1); + SLIM_GL_DEFCOORDS(mutationTickRect); + SLIM_GL_PUSHRECT(); + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + } + } + } + + free(subBuffer); + } + + SLIM_GL_FINISH(); +} + +void QtSLiMChromosomeWidget::_glDrawRateMapIntervals(QRect &interiorRect, __attribute__((__unused__)) Species *displaySpecies, QtSLiMRange displayedRange, std::vector &ends, std::vector &rates, double hue) +{ + size_t recombinationIntervalCount = ends.size(); + slim_position_t intervalStartPosition = 0; + int previousIntervalLeftEdge = -10000; + + SLIM_GL_PREPARE(); + + for (size_t interval = 0; interval < recombinationIntervalCount; ++interval) + { + slim_position_t intervalEndPosition = ends[interval]; + double intervalRate = rates[interval]; + QRect intervalRect = rectEncompassingBaseToBase(intervalStartPosition, intervalEndPosition, interiorRect, displayedRange); + bool widthOne = (intervalRect.width() == 1); + + // We want to avoid overdrawing width-one intervals, which are important but small, so if the previous interval was width-one, + // and we are not, and we are about to overdraw it, then we scoot our left edge over one pixel to leave it alone. + if (!widthOne && (intervalRect.left() == previousIntervalLeftEdge)) + intervalRect.adjust(1, 0, 0, 0); + + // draw only the visible part, if any + intervalRect = intervalRect.intersected(interiorRect); + + if (!intervalRect.isEmpty()) + { + // color according to how "hot" the region is + float colorRed, colorGreen, colorBlue, colorAlpha; + + if (intervalRate == 0.0) + { + // a recombination or mutation rate of 0.0 comes out as black, whereas the lowest brightness below is 0.5; we want to distinguish this + colorRed = colorGreen = colorBlue = 0.0; + colorAlpha = 1.0; + } + else + { + // the formula below scales 1e-6 to 1.0 and 1e-9 to 0.0; values outside that range clip, but we want there to be + // reasonable contrast within the range of values commonly used, so we don't want to make the range too wide + double lightness, brightness = 1.0, saturation = 1.0; + + lightness = (log10(intervalRate) + 9.0) / 3.0; + lightness = std::max(lightness, 0.0); + lightness = std::min(lightness, 1.0); + + if (lightness >= 0.5) saturation = 1.0 - ((lightness - 0.5) * 2.0); // goes from 1.0 at lightness 0.5 to 0.0 at lightness 1.0 + else brightness = 0.5 + lightness; // goes from 1.0 at lightness 0.5 to 0.5 at lightness 0.0 + + QColor intervalColor = QtSLiMColorWithHSV(hue, saturation, brightness, 1.0); + +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + // In Qt5, getRgbF() expects pointers to qreal, which is double + double r, g, b, a; + intervalColor.getRgbF(&r, &g, &b, &a); + + colorRed = static_cast(r); + colorGreen = static_cast(g); + colorBlue = static_cast(b); + colorAlpha = static_cast(a); +#else + // In Qt6, getRgbF() expects pointers to float + intervalColor.getRgbF(&colorRed, &colorGreen, &colorBlue, &colorAlpha); +#endif + } + + SLIM_GL_DEFCOORDS(intervalRect); + SLIM_GL_PUSHRECT(); + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + + // if this interval is just one pixel wide, we want to try to make it visible, by avoiding overdrawing it; so we remember its location + if (widthOne) + previousIntervalLeftEdge = intervalRect.left(); + else + previousIntervalLeftEdge = -10000; + } + + // the next interval starts at the next base after this one ended + intervalStartPosition = intervalEndPosition + 1; + } + + SLIM_GL_FINISH(); +} + +void QtSLiMChromosomeWidget::glDrawRecombinationIntervals(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange) +{ + Chromosome &chromosome = displaySpecies->TheChromosome(); + + if (chromosome.single_recombination_map_) + { + _glDrawRateMapIntervals(interiorRect, displaySpecies, displayedRange, chromosome.recombination_end_positions_H_, chromosome.recombination_rates_H_, 0.65); + } + else + { + QRect topInteriorRect = interiorRect, bottomInteriorRect = interiorRect; + int halfHeight = static_cast(ceil(interiorRect.height() / 2.0)); + int remainingHeight = interiorRect.height() - halfHeight; + + topInteriorRect.setHeight(halfHeight); + bottomInteriorRect.setHeight(remainingHeight); + bottomInteriorRect.translate(0, halfHeight); + + _glDrawRateMapIntervals(topInteriorRect, displaySpecies, displayedRange, chromosome.recombination_end_positions_M_, chromosome.recombination_rates_M_, 0.65); + _glDrawRateMapIntervals(bottomInteriorRect, displaySpecies, displayedRange, chromosome.recombination_end_positions_F_, chromosome.recombination_rates_F_, 0.65); + } +} + +void QtSLiMChromosomeWidget::glDrawMutationIntervals(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange) +{ + Chromosome &chromosome = displaySpecies->TheChromosome(); + + if (chromosome.single_mutation_map_) + { + _glDrawRateMapIntervals(interiorRect, displaySpecies, displayedRange, chromosome.mutation_end_positions_H_, chromosome.mutation_rates_H_, 0.75); + } + else + { + QRect topInteriorRect = interiorRect, bottomInteriorRect = interiorRect; + int halfHeight = static_cast(ceil(interiorRect.height() / 2.0)); + int remainingHeight = interiorRect.height() - halfHeight; + + topInteriorRect.setHeight(halfHeight); + bottomInteriorRect.setHeight(remainingHeight); + bottomInteriorRect.translate(0, halfHeight); + + _glDrawRateMapIntervals(topInteriorRect, displaySpecies, displayedRange, chromosome.mutation_end_positions_M_, chromosome.mutation_rates_M_, 0.75); + _glDrawRateMapIntervals(bottomInteriorRect, displaySpecies, displayedRange, chromosome.mutation_end_positions_F_, chromosome.mutation_rates_F_, 0.75); + } +} + +void QtSLiMChromosomeWidget::glDrawRateMaps(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange) +{ + Chromosome &chromosome = displaySpecies->TheChromosome(); + bool recombinationWorthShowing = false; + bool mutationWorthShowing = false; + + if (chromosome.single_mutation_map_) + mutationWorthShowing = (chromosome.mutation_end_positions_H_.size() > 1); + else + mutationWorthShowing = ((chromosome.mutation_end_positions_M_.size() > 1) || (chromosome.mutation_end_positions_F_.size() > 1)); + + if (chromosome.single_recombination_map_) + recombinationWorthShowing = (chromosome.recombination_end_positions_H_.size() > 1); + else + recombinationWorthShowing = ((chromosome.recombination_end_positions_M_.size() > 1) || (chromosome.recombination_end_positions_F_.size() > 1)); + + // If neither map is worth showing, we show just the recombination map, to mirror the behavior of 2.4 and earlier + if ((!mutationWorthShowing && !recombinationWorthShowing) || (!mutationWorthShowing && recombinationWorthShowing)) + { + glDrawRecombinationIntervals(interiorRect, displaySpecies, displayedRange); + } + else if (mutationWorthShowing && !recombinationWorthShowing) + { + glDrawMutationIntervals(interiorRect, displaySpecies, displayedRange); + } + else // mutationWorthShowing && recombinationWorthShowing + { + QRect topInteriorRect = interiorRect, bottomInteriorRect = interiorRect; + int halfHeight = static_cast(ceil(interiorRect.height() / 2.0)); + int remainingHeight = interiorRect.height() - halfHeight; + + topInteriorRect.setHeight(halfHeight); + bottomInteriorRect.setHeight(remainingHeight); + bottomInteriorRect.translate(0, halfHeight); + + glDrawRecombinationIntervals(topInteriorRect, displaySpecies, displayedRange); + glDrawMutationIntervals(bottomInteriorRect, displaySpecies, displayedRange); + } +} + +#endif + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/QtSLiM/QtSLiMChromosomeWidget_QT.cpp b/QtSLiM/QtSLiMChromosomeWidget_QT.cpp new file mode 100644 index 00000000..5b7b9440 --- /dev/null +++ b/QtSLiM/QtSLiMChromosomeWidget_QT.cpp @@ -0,0 +1,764 @@ +// +// QtSLiMChromosomeWidget_QT.cpp +// SLiM +// +// Created by Ben Haller on 8/25/2024. +// Copyright (c) 2024 Philipp Messer. All rights reserved. +// A product of the Messer Lab, http://messerlab.org/slim/ +// + +// This file is part of SLiM. +// +// SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +// +// SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with SLiM. If not, see . + + +#include "QtSLiMChromosomeWidget.h" +#include "QtSLiMHaplotypeManager.h" +#include "QtSLiMOpenGL_Emulation.h" + +#include + +#include +#include +#include + + +// +// OpenGL-based drawing; maintain this in parallel with the Qt-based drawing! +// + +void QtSLiMChromosomeWidget::qtDrawRect(Species *displaySpecies, QPainter &painter) +{ + bool ready = isEnabled() && !controller_->invalidSimulation(); + QRect interiorRect = getInteriorRect(); + + // if the simulation is at tick 0, it is not ready + if (ready) + if (controller_->community->Tick() == 0) + ready = false; + + if (ready) + { + // erase the content area itself + painter.fillRect(interiorRect, Qt::black); + + QtSLiMRange displayedRange = getDisplayedRange(displaySpecies); + + bool splitHeight = (shouldDrawRateMaps() && shouldDrawGenomicElements()); + QRect topInteriorRect = interiorRect, bottomInteriorRect = interiorRect; + int halfHeight = static_cast(ceil(interiorRect.height() / 2.0)); + int remainingHeight = interiorRect.height() - halfHeight; + + topInteriorRect.setHeight(halfHeight); + bottomInteriorRect.setHeight(remainingHeight); + bottomInteriorRect.translate(0, halfHeight); + + // draw recombination intervals in interior + if (shouldDrawRateMaps()) + qtDrawRateMaps(splitHeight ? topInteriorRect : interiorRect, displaySpecies, displayedRange, painter); + + // draw genomic elements in interior + if (shouldDrawGenomicElements()) + qtDrawGenomicElements(splitHeight ? bottomInteriorRect : interiorRect, displaySpecies, displayedRange, painter); + + // figure out which mutation types we're displaying + if (shouldDrawFixedSubstitutions() || shouldDrawMutations()) + updateDisplayedMutationTypes(displaySpecies); + + // draw fixed substitutions in interior + if (shouldDrawFixedSubstitutions()) + qtDrawFixedSubstitutions(interiorRect, displaySpecies, displayedRange, painter); + + // draw mutations in interior + if (shouldDrawMutations()) + { + if (displayHaplotypes()) + { + // display mutations as a haplotype plot, courtesy of QtSLiMHaplotypeManager; we use ClusterNearestNeighbor and + // ClusterNoOptimization because they're fast, and NN might also provide a bit more run-to-run continuity + // we cache the haplotype manager here, so our display remains constant across window resizes and other + // invalidations; we toss the cache only when the simulation tells us that the model state has changed + if (!haplotype_mgr_) + { + size_t interiorHeight = static_cast(interiorRect.height()); // one sample per available pixel line, for simplicity and speed; 47, in the current UI layout + haplotype_mgr_ = new QtSLiMHaplotypeManager(nullptr, QtSLiMHaplotypeManager::ClusterNearestNeighbor, QtSLiMHaplotypeManager::ClusterNoOptimization, controller_, displaySpecies, interiorHeight, false); + } + + if (haplotype_mgr_) + haplotype_mgr_->qtDrawHaplotypes(interiorRect, false, false, false, &haplotype_previous_bincounts, painter); + } + else + { + // display mutations as a frequency plot; this is the standard display mode + qtDrawMutations(interiorRect, displaySpecies, displayedRange, painter); + } + } + } + else + { + // erase the content area itself + painter.fillRect(QRect(0, 0, interiorRect.width(), interiorRect.height()), QtSLiMColorWithWhite(0.88, 1.0)); + } +} + +void QtSLiMChromosomeWidget::qtDrawGenomicElements(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange, QPainter &painter) +{ + Chromosome &chromosome = displaySpecies->TheChromosome(); + int previousIntervalLeftEdge = -10000; + + SLIM_GL_PREPARE(); + + for (GenomicElement *genomicElement : chromosome.GenomicElements()) + { + slim_position_t startPosition = genomicElement->start_position_; + slim_position_t endPosition = genomicElement->end_position_; + QRect elementRect = rectEncompassingBaseToBase(startPosition, endPosition, interiorRect, displayedRange); + bool widthOne = (elementRect.width() == 1); + + // We want to avoid overdrawing width-one intervals, which are important but small, so if the previous interval was width-one, + // and we are not, and we are about to overdraw it, then we scoot our left edge over one pixel to leave it alone. + if (!widthOne && (elementRect.left() == previousIntervalLeftEdge)) + elementRect.adjust(1, 0, 0, 0); + + // draw only the visible part, if any + elementRect = elementRect.intersected(interiorRect); + + if (!elementRect.isEmpty()) + { + GenomicElementType *geType = genomicElement->genomic_element_type_ptr_; + float colorRed, colorGreen, colorBlue, colorAlpha; + + if (!geType->color_.empty()) + { + colorRed = geType->color_red_; + colorGreen = geType->color_green_; + colorBlue = geType->color_blue_; + colorAlpha = 1.0; + } + else + { + slim_objectid_t elementTypeID = geType->genomic_element_type_id_; + + controller_->colorForGenomicElementType(geType, elementTypeID, &colorRed, &colorGreen, &colorBlue, &colorAlpha); + } + + SLIM_GL_DEFCOORDS(elementRect); + SLIM_GL_PUSHRECT(); + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + + // if this interval is just one pixel wide, we want to try to make it visible, by avoiding overdrawing it; so we remember its location + if (widthOne) + previousIntervalLeftEdge = elementRect.left(); + else + previousIntervalLeftEdge = -10000; + } + } + + SLIM_GL_FINISH(); +} + +void QtSLiMChromosomeWidget::qtDrawMutations(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange, QPainter &painter) +{ + double scalingFactor = 0.8; // used to be controller->selectionColorScale; + Population &pop = displaySpecies->population_; + double totalGenomeCount = pop.gui_total_genome_count_; // this includes only genomes in the selected subpopulations + int registry_size; + const MutationIndex *registry = pop.MutationRegistry(®istry_size); + Mutation *mut_block_ptr = gSLiM_Mutation_Block; + + // Set up to draw rects + float colorRed = 0.0f, colorGreen = 0.0f, colorBlue = 0.0f, colorAlpha = 1.0; + + SLIM_GL_PREPARE(); + + if ((registry_size < 1000) || (displayedRange.length < interiorRect.width())) + { + // This is the simple version of the display code, avoiding the memory allocations and such + for (int registry_index = 0; registry_index < registry_size; ++registry_index) + { + const Mutation *mutation = mut_block_ptr + registry[registry_index]; + const MutationType *mutType = mutation->mutation_type_ptr_; + + if (mutType->mutation_type_displayed_) + { + slim_refcount_t mutationRefCount = mutation->gui_reference_count_; // this includes only references made from the selected subpopulations + slim_position_t mutationPosition = mutation->position_; + QRect mutationTickRect = rectEncompassingBaseToBase(mutationPosition, mutationPosition, interiorRect, displayedRange); + + if (!mutType->color_.empty()) + { + colorRed = mutType->color_red_; + colorGreen = mutType->color_green_; + colorBlue = mutType->color_blue_; + } + else + { + RGBForSelectionCoeff(static_cast(mutation->selection_coeff_), &colorRed, &colorGreen, &colorBlue, scalingFactor); + } + + int height_adjust = mutationTickRect.height() - static_cast(ceil((mutationRefCount / totalGenomeCount) * interiorRect.height())); + mutationTickRect.setTop(mutationTickRect.top() + height_adjust); + + SLIM_GL_DEFCOORDS(mutationTickRect); + SLIM_GL_PUSHRECT(); + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + } + } + } + else + { + // We have a lot of mutations, so let's try to be smarter. It's hard to be smarter. The overhead from allocating the NSColors and such + // is pretty negligible; practially all the time is spent in NSRectFill(). Unfortunately, NSRectFillListWithColors() provides basically + // no speedup; Apple doesn't appear to have optimized it. So, here's what I came up with. For each mutation type that uses a fixed DFE, + // and thus a fixed color, we can do a radix sort of mutations into bins corresponding to each pixel in our displayed image. Then we + // can draw each bin just once, making one bar for the highest bar in that bin. Mutations from non-fixed DFEs, and mutations which have + // had their selection coefficient changed, will be drawn at the end in the usual (slow) way. + int displayPixelWidth = interiorRect.width(); + int16_t *heightBuffer = static_cast(malloc(static_cast(displayPixelWidth) * sizeof(int16_t))); + bool *mutationsPlotted = static_cast(calloc(static_cast(registry_size), sizeof(bool))); // faster than using gui_scratch_reference_count_ because of cache locality + int64_t remainingMutations = registry_size; + + // First zero out the scratch refcount, which we use to track which mutations we have drawn already + //for (int mutIndex = 0; mutIndex < mutationCount; ++mutIndex) + // mutations[mutIndex]->gui_scratch_reference_count_ = 0; + + // Then loop through the declared mutation types + std::map &mut_types = displaySpecies->mutation_types_; + bool draw_muttypes_sequentially = (mut_types.size() <= 20); // with a lot of mutation types, the algorithm below becomes very inefficient + + for (auto mutationTypeIter : mut_types) + { + MutationType *mut_type = mutationTypeIter.second; + + if (mut_type->mutation_type_displayed_) + { + if (draw_muttypes_sequentially) + { + bool mut_type_fixed_color = !mut_type->color_.empty(); + + // We optimize fixed-DFE mutation types only, and those using a fixed color set by the user + if ((mut_type->dfe_type_ == DFEType::kFixed) || mut_type_fixed_color) + { + slim_selcoeff_t mut_type_selcoeff = (mut_type_fixed_color ? 0.0 : static_cast(mut_type->dfe_parameters_[0])); + + EIDOS_BZERO(heightBuffer, static_cast(displayPixelWidth) * sizeof(int16_t)); + + // Scan through the mutation list for mutations of this type with the right selcoeff + for (int registry_index = 0; registry_index < registry_size; ++registry_index) + { + const Mutation *mutation = mut_block_ptr + registry[registry_index]; + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wfloat-equal" +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wfloat-equal" + // We do want to do an exact floating-point equality compare here; we want to see whether the mutation's selcoeff is unmodified from the fixed DFE + if ((mutation->mutation_type_ptr_ == mut_type) && (mut_type_fixed_color || (mutation->selection_coeff_ == mut_type_selcoeff))) +#pragma clang diagnostic pop +#pragma GCC diagnostic pop + { + slim_refcount_t mutationRefCount = mutation->gui_reference_count_; // includes only refs from the selected subpopulations + slim_position_t mutationPosition = mutation->position_; + //NSRect mutationTickRect = [self rectEncompassingBase:mutationPosition toBase:mutationPosition interiorRect:interiorRect displayedRange:displayedRange]; + //int xPos = (int)(mutationTickRect.origin.x - interiorRect.origin.x); + int xPos = LEFT_OFFSET_OF_BASE(mutationPosition, interiorRect, displayedRange); + int16_t barHeight = static_cast(ceil((mutationRefCount / totalGenomeCount) * interiorRect.height())); + + if ((xPos >= 0) && (xPos < displayPixelWidth)) + if (barHeight > heightBuffer[xPos]) + heightBuffer[xPos] = barHeight; + + // tally this mutation as handled + //mutation->gui_scratch_reference_count_ = 1; + mutationsPlotted[registry_index] = true; + --remainingMutations; + } + } + + // Now draw all of the mutations we found, by looping through our radix bins + if (mut_type_fixed_color) + { + colorRed = mut_type->color_red_; + colorGreen = mut_type->color_green_; + colorBlue = mut_type->color_blue_; + } + else + { + RGBForSelectionCoeff(static_cast(mut_type_selcoeff), &colorRed, &colorGreen, &colorBlue, scalingFactor); + } + + for (int binIndex = 0; binIndex < displayPixelWidth; ++binIndex) + { + int barHeight = heightBuffer[binIndex]; + + if (barHeight) + { + QRect mutationTickRect(interiorRect.x() + binIndex, interiorRect.y(), 1, interiorRect.height()); + mutationTickRect.setTop(mutationTickRect.top() + interiorRect.height() - barHeight); + + SLIM_GL_DEFCOORDS(mutationTickRect); + SLIM_GL_PUSHRECT(); + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + } + } + } + } + } + else + { + // We're not displaying this mutation type, so we need to mark off all the mutations belonging to it as handled + for (int registry_index = 0; registry_index < registry_size; ++registry_index) + { + const Mutation *mutation = mut_block_ptr + registry[registry_index]; + + if (mutation->mutation_type_ptr_ == mut_type) + { + // tally this mutation as handled + //mutation->gui_scratch_reference_count_ = 1; + mutationsPlotted[registry_index] = true; + --remainingMutations; + } + } + } + } + + // Draw any undrawn mutations on top; these are guaranteed not to use a fixed color set by the user, since those are all handled above + if (remainingMutations) + { + if (remainingMutations < 1000) + { + // Plot the remainder by brute force, since there are not that many + for (int registry_index = 0; registry_index < registry_size; ++registry_index) + { + //if (mutation->gui_scratch_reference_count_ == 0) + if (!mutationsPlotted[registry_index]) + { + const Mutation *mutation = mut_block_ptr + registry[registry_index]; + slim_refcount_t mutationRefCount = mutation->gui_reference_count_; // this includes only references made from the selected subpopulations + slim_position_t mutationPosition = mutation->position_; + QRect mutationTickRect = rectEncompassingBaseToBase(mutationPosition, mutationPosition, interiorRect, displayedRange); + int height_adjust = mutationTickRect.height() - static_cast(ceil((mutationRefCount / totalGenomeCount) * interiorRect.height())); + + mutationTickRect.setTop(mutationTickRect.top() + height_adjust); + RGBForSelectionCoeff(static_cast(mutation->selection_coeff_), &colorRed, &colorGreen, &colorBlue, scalingFactor); + + SLIM_GL_DEFCOORDS(mutationTickRect); + SLIM_GL_PUSHRECT(); + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + } + } + } + else + { + // OK, we have a lot of mutations left to draw. Here we will again use the radix sort trick, to keep track of only the tallest bar in each column + MutationIndex *mutationBuffer = static_cast(calloc(static_cast(displayPixelWidth), sizeof(MutationIndex))); + + EIDOS_BZERO(heightBuffer, static_cast(displayPixelWidth) * sizeof(int16_t)); + + // Find the tallest bar in each column + for (int registry_index = 0; registry_index < registry_size; ++registry_index) + { + //if (mutation->gui_scratch_reference_count_ == 0) + if (!mutationsPlotted[registry_index]) + { + MutationIndex mutationBlockIndex = registry[registry_index]; + const Mutation *mutation = mut_block_ptr + mutationBlockIndex; + slim_refcount_t mutationRefCount = mutation->gui_reference_count_; // this includes only references made from the selected subpopulations + slim_position_t mutationPosition = mutation->position_; + //NSRect mutationTickRect = [self rectEncompassingBase:mutationPosition toBase:mutationPosition interiorRect:interiorRect displayedRange:displayedRange]; + //int xPos = (int)(mutationTickRect.origin.x - interiorRect.origin.x); + int xPos = LEFT_OFFSET_OF_BASE(mutationPosition, interiorRect, displayedRange); + int16_t barHeight = static_cast(ceil((mutationRefCount / totalGenomeCount) * interiorRect.height())); + + if ((xPos >= 0) && (xPos < displayPixelWidth)) + { + if (barHeight > heightBuffer[xPos]) + { + heightBuffer[xPos] = barHeight; + mutationBuffer[xPos] = mutationBlockIndex; + } + } + } + } + + // Now plot the bars + for (int binIndex = 0; binIndex < displayPixelWidth; ++binIndex) + { + int barHeight = heightBuffer[binIndex]; + + if (barHeight) + { + QRect mutationTickRect(interiorRect.x() + binIndex, interiorRect.y(), 1, interiorRect.height()); + mutationTickRect.setTop(mutationTickRect.top() + interiorRect.height() - barHeight); + + const Mutation *mutation = mut_block_ptr + mutationBuffer[binIndex]; + + RGBForSelectionCoeff(static_cast(mutation->selection_coeff_), &colorRed, &colorGreen, &colorBlue, scalingFactor); + + SLIM_GL_DEFCOORDS(mutationTickRect); + SLIM_GL_PUSHRECT(); + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + } + } + + free(mutationBuffer); + } + } + + free(heightBuffer); + free(mutationsPlotted); + } + + SLIM_GL_FINISH(); +} + +void QtSLiMChromosomeWidget::qtDrawFixedSubstitutions(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange, QPainter &painter) +{ + double scalingFactor = 0.8; // used to be controller->selectionColorScale; + Population &pop = displaySpecies->population_; + Chromosome &chromosome = displaySpecies->TheChromosome(); + bool chromosomeHasDefaultColor = !chromosome.color_sub_.empty(); + std::vector &substitutions = pop.substitutions_; + + // Set up to draw rects + float colorRed = 0.2f, colorGreen = 0.2f, colorBlue = 1.0f, colorAlpha = 1.0; + + if (chromosomeHasDefaultColor) + { + colorRed = chromosome.color_sub_red_; + colorGreen = chromosome.color_sub_green_; + colorBlue = chromosome.color_sub_blue_; + } + + SLIM_GL_PREPARE(); + + if ((substitutions.size() < 1000) || (displayedRange.length < interiorRect.width())) + { + // This is the simple version of the display code, avoiding the memory allocations and such + for (const Substitution *substitution : substitutions) + { + if (substitution->mutation_type_ptr_->mutation_type_displayed_) + { + slim_position_t substitutionPosition = substitution->position_; + QRect substitutionTickRect = rectEncompassingBaseToBase(substitutionPosition, substitutionPosition, interiorRect, displayedRange); + + if (!shouldDrawMutations() || !chromosomeHasDefaultColor) + { + // If we're drawing mutations as well, then substitutions just get colored blue (set above), to contrast + // If we're not drawing mutations as well, then substitutions get colored by selection coefficient, like mutations + const MutationType *mutType = substitution->mutation_type_ptr_; + + if (!mutType->color_sub_.empty()) + { + colorRed = mutType->color_sub_red_; + colorGreen = mutType->color_sub_green_; + colorBlue = mutType->color_sub_blue_; + } + else + { + RGBForSelectionCoeff(static_cast(substitution->selection_coeff_), &colorRed, &colorGreen, &colorBlue, scalingFactor); + } + } + + SLIM_GL_DEFCOORDS(substitutionTickRect); + SLIM_GL_PUSHRECT(); + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + } + } + } + else + { + // We have a lot of substitutions, so do a radix sort, as we do in drawMutationsInInteriorRect: below. + int displayPixelWidth = interiorRect.width(); + const Substitution **subBuffer = static_cast(calloc(static_cast(displayPixelWidth), sizeof(Substitution *))); + + for (const Substitution *substitution : substitutions) + { + if (substitution->mutation_type_ptr_->mutation_type_displayed_) + { + slim_position_t substitutionPosition = substitution->position_; + double startFraction = (substitutionPosition - static_cast(displayedRange.location)) / static_cast(displayedRange.length); + int xPos = static_cast(floor(startFraction * interiorRect.width())); + + if ((xPos >= 0) && (xPos < displayPixelWidth)) + subBuffer[xPos] = substitution; + } + } + + if (shouldDrawMutations() && chromosomeHasDefaultColor) + { + // If we're drawing mutations as well, then substitutions just get colored blue, to contrast + QRect mutationTickRect = interiorRect; + + for (int binIndex = 0; binIndex < displayPixelWidth; ++binIndex) + { + const Substitution *substitution = subBuffer[binIndex]; + + if (substitution) + { + mutationTickRect.setX(interiorRect.x() + binIndex); + mutationTickRect.setWidth(1); + + // consolidate adjacent lines together, since they are all the same color + while ((binIndex + 1 < displayPixelWidth) && subBuffer[binIndex + 1]) + { + mutationTickRect.setWidth(mutationTickRect.width() + 1); + binIndex++; + } + + SLIM_GL_DEFCOORDS(mutationTickRect); + SLIM_GL_PUSHRECT(); + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + } + } + } + else + { + // If we're not drawing mutations as well, then substitutions get colored by selection coefficient, like mutations + QRect mutationTickRect = interiorRect; + + for (int binIndex = 0; binIndex < displayPixelWidth; ++binIndex) + { + const Substitution *substitution = subBuffer[binIndex]; + + if (substitution) + { + const MutationType *mutType = substitution->mutation_type_ptr_; + + if (!mutType->color_sub_.empty()) + { + colorRed = mutType->color_sub_red_; + colorGreen = mutType->color_sub_green_; + colorBlue = mutType->color_sub_blue_; + } + else + { + RGBForSelectionCoeff(static_cast(substitution->selection_coeff_), &colorRed, &colorGreen, &colorBlue, scalingFactor); + } + + mutationTickRect.setX(interiorRect.x() + binIndex); + mutationTickRect.setWidth(1); + SLIM_GL_DEFCOORDS(mutationTickRect); + SLIM_GL_PUSHRECT(); + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + } + } + } + + free(subBuffer); + } + + SLIM_GL_FINISH(); +} + +void QtSLiMChromosomeWidget::_qtDrawRateMapIntervals(QRect &interiorRect, __attribute__((__unused__)) Species *displaySpecies, QtSLiMRange displayedRange, std::vector &ends, std::vector &rates, double hue, QPainter &painter) +{ + size_t recombinationIntervalCount = ends.size(); + slim_position_t intervalStartPosition = 0; + int previousIntervalLeftEdge = -10000; + + SLIM_GL_PREPARE(); + + for (size_t interval = 0; interval < recombinationIntervalCount; ++interval) + { + slim_position_t intervalEndPosition = ends[interval]; + double intervalRate = rates[interval]; + QRect intervalRect = rectEncompassingBaseToBase(intervalStartPosition, intervalEndPosition, interiorRect, displayedRange); + bool widthOne = (intervalRect.width() == 1); + + // We want to avoid overdrawing width-one intervals, which are important but small, so if the previous interval was width-one, + // and we are not, and we are about to overdraw it, then we scoot our left edge over one pixel to leave it alone. + if (!widthOne && (intervalRect.left() == previousIntervalLeftEdge)) + intervalRect.adjust(1, 0, 0, 0); + + // draw only the visible part, if any + intervalRect = intervalRect.intersected(interiorRect); + + if (!intervalRect.isEmpty()) + { + // color according to how "hot" the region is + float colorRed, colorGreen, colorBlue, colorAlpha; + + if (intervalRate == 0.0) + { + // a recombination or mutation rate of 0.0 comes out as black, whereas the lowest brightness below is 0.5; we want to distinguish this + colorRed = colorGreen = colorBlue = 0.0; + colorAlpha = 1.0; + } + else + { + // the formula below scales 1e-6 to 1.0 and 1e-9 to 0.0; values outside that range clip, but we want there to be + // reasonable contrast within the range of values commonly used, so we don't want to make the range too wide + double lightness, brightness = 1.0, saturation = 1.0; + + lightness = (log10(intervalRate) + 9.0) / 3.0; + lightness = std::max(lightness, 0.0); + lightness = std::min(lightness, 1.0); + + if (lightness >= 0.5) saturation = 1.0 - ((lightness - 0.5) * 2.0); // goes from 1.0 at lightness 0.5 to 0.0 at lightness 1.0 + else brightness = 0.5 + lightness; // goes from 1.0 at lightness 0.5 to 0.5 at lightness 0.0 + + QColor intervalColor = QtSLiMColorWithHSV(hue, saturation, brightness, 1.0); + +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + // In Qt5, getRgbF() expects pointers to qreal, which is double + double r, g, b, a; + intervalColor.getRgbF(&r, &g, &b, &a); + + colorRed = static_cast(r); + colorGreen = static_cast(g); + colorBlue = static_cast(b); + colorAlpha = static_cast(a); +#else + // In Qt6, getRgbF() expects pointers to float + intervalColor.getRgbF(&colorRed, &colorGreen, &colorBlue, &colorAlpha); +#endif + } + + SLIM_GL_DEFCOORDS(intervalRect); + SLIM_GL_PUSHRECT(); + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + + // if this interval is just one pixel wide, we want to try to make it visible, by avoiding overdrawing it; so we remember its location + if (widthOne) + previousIntervalLeftEdge = intervalRect.left(); + else + previousIntervalLeftEdge = -10000; + } + + // the next interval starts at the next base after this one ended + intervalStartPosition = intervalEndPosition + 1; + } + + SLIM_GL_FINISH(); +} + +void QtSLiMChromosomeWidget::qtDrawRecombinationIntervals(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange, QPainter &painter) +{ + Chromosome &chromosome = displaySpecies->TheChromosome(); + + if (chromosome.single_recombination_map_) + { + _qtDrawRateMapIntervals(interiorRect, displaySpecies, displayedRange, chromosome.recombination_end_positions_H_, chromosome.recombination_rates_H_, 0.65, painter); + } + else + { + QRect topInteriorRect = interiorRect, bottomInteriorRect = interiorRect; + int halfHeight = static_cast(ceil(interiorRect.height() / 2.0)); + int remainingHeight = interiorRect.height() - halfHeight; + + topInteriorRect.setHeight(halfHeight); + bottomInteriorRect.setHeight(remainingHeight); + bottomInteriorRect.translate(0, halfHeight); + + _qtDrawRateMapIntervals(topInteriorRect, displaySpecies, displayedRange, chromosome.recombination_end_positions_M_, chromosome.recombination_rates_M_, 0.65, painter); + _qtDrawRateMapIntervals(bottomInteriorRect, displaySpecies, displayedRange, chromosome.recombination_end_positions_F_, chromosome.recombination_rates_F_, 0.65, painter); + } +} + +void QtSLiMChromosomeWidget::qtDrawMutationIntervals(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange, QPainter &painter) +{ + Chromosome &chromosome = displaySpecies->TheChromosome(); + + if (chromosome.single_mutation_map_) + { + _qtDrawRateMapIntervals(interiorRect, displaySpecies, displayedRange, chromosome.mutation_end_positions_H_, chromosome.mutation_rates_H_, 0.75, painter); + } + else + { + QRect topInteriorRect = interiorRect, bottomInteriorRect = interiorRect; + int halfHeight = static_cast(ceil(interiorRect.height() / 2.0)); + int remainingHeight = interiorRect.height() - halfHeight; + + topInteriorRect.setHeight(halfHeight); + bottomInteriorRect.setHeight(remainingHeight); + bottomInteriorRect.translate(0, halfHeight); + + _qtDrawRateMapIntervals(topInteriorRect, displaySpecies, displayedRange, chromosome.mutation_end_positions_M_, chromosome.mutation_rates_M_, 0.75, painter); + _qtDrawRateMapIntervals(bottomInteriorRect, displaySpecies, displayedRange, chromosome.mutation_end_positions_F_, chromosome.mutation_rates_F_, 0.75, painter); + } +} + +void QtSLiMChromosomeWidget::qtDrawRateMaps(QRect &interiorRect, Species *displaySpecies, QtSLiMRange displayedRange, QPainter &painter) +{ + Chromosome &chromosome = displaySpecies->TheChromosome(); + bool recombinationWorthShowing = false; + bool mutationWorthShowing = false; + + if (chromosome.single_mutation_map_) + mutationWorthShowing = (chromosome.mutation_end_positions_H_.size() > 1); + else + mutationWorthShowing = ((chromosome.mutation_end_positions_M_.size() > 1) || (chromosome.mutation_end_positions_F_.size() > 1)); + + if (chromosome.single_recombination_map_) + recombinationWorthShowing = (chromosome.recombination_end_positions_H_.size() > 1); + else + recombinationWorthShowing = ((chromosome.recombination_end_positions_M_.size() > 1) || (chromosome.recombination_end_positions_F_.size() > 1)); + + // If neither map is worth showing, we show just the recombination map, to mirror the behavior of 2.4 and earlier + if ((!mutationWorthShowing && !recombinationWorthShowing) || (!mutationWorthShowing && recombinationWorthShowing)) + { + qtDrawRecombinationIntervals(interiorRect, displaySpecies, displayedRange, painter); + } + else if (mutationWorthShowing && !recombinationWorthShowing) + { + qtDrawMutationIntervals(interiorRect, displaySpecies, displayedRange, painter); + } + else // mutationWorthShowing && recombinationWorthShowing + { + QRect topInteriorRect = interiorRect, bottomInteriorRect = interiorRect; + int halfHeight = static_cast(ceil(interiorRect.height() / 2.0)); + int remainingHeight = interiorRect.height() - halfHeight; + + topInteriorRect.setHeight(halfHeight); + bottomInteriorRect.setHeight(remainingHeight); + bottomInteriorRect.translate(0, halfHeight); + + qtDrawRecombinationIntervals(topInteriorRect, displaySpecies, displayedRange, painter); + qtDrawMutationIntervals(bottomInteriorRect, displaySpecies, displayedRange, painter); + } +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/QtSLiM/QtSLiMHaplotypeManager.cpp b/QtSLiM/QtSLiMHaplotypeManager.cpp index 4f8519da..2faadb00 100644 --- a/QtSLiM/QtSLiMHaplotypeManager.cpp +++ b/QtSLiM/QtSLiMHaplotypeManager.cpp @@ -21,6 +21,7 @@ #include "QtSLiMWindow.h" #include "QtSLiMHaplotypeOptions.h" #include "QtSLiMHaplotypeProgress.h" +#include "QtSLiMPreferences.h" #include "QtSLiMExtras.h" #include @@ -547,105 +548,6 @@ void QtSLiMHaplotypeManager::configureDisplayBuffers(void) } } -static const int kMaxGLRects = 2000; // 2000 rects -static const int kMaxVertices = kMaxGLRects * 4; // 4 vertices each - -static float *glArrayVertices = nullptr; -static float *glArrayColors = nullptr; - -void QtSLiMHaplotypeManager::allocateGLBuffers(void) -{ - // Set up the vertex and color arrays - if (!glArrayVertices) - glArrayVertices = static_cast(malloc(kMaxVertices * 2 * sizeof(float))); // 2 floats per vertex, kMaxVertices vertices - - if (!glArrayColors) - glArrayColors = static_cast(malloc(kMaxVertices * 4 * sizeof(float))); // 4 floats per color, kMaxVertices colors -} - -void QtSLiMHaplotypeManager::drawSubpopStripsInRect(QRect interior) -{ - int displayListIndex; - float *vertices = nullptr, *colors = nullptr; - - // Set up to draw rects - displayListIndex = 0; - - vertices = glArrayVertices; - glEnableClientState(GL_VERTEX_ARRAY); - glVertexPointer(2, GL_FLOAT, 0, glArrayVertices); - - colors = glArrayColors; - glEnableClientState(GL_COLOR_ARRAY); - glColorPointer(4, GL_FLOAT, 0, glArrayColors); - - // Loop through the genomes and draw them; we do this in two passes, neutral mutations underneath selected mutations - size_t genome_index = 0, genome_count = genomeSubpopIDs.size(); - float height_divisor = genome_count; - float left = static_cast(interior.x()); - float right = static_cast(interior.x() + interior.width()); - - for (slim_objectid_t genome_subpop_id : genomeSubpopIDs) - { - float top = interior.y() + (genome_index / height_divisor) * interior.height(); - float bottom = interior.y() + ((genome_index + 1) / height_divisor) * interior.height(); - - if (bottom - top > 1.0f) - { - // If the range spans a width of more than one pixel, then use the maximal pixel range - top = floorf(top); - bottom = ceilf(bottom); - } - else - { - // If the range spans a pixel or less, make sure that we end up with a range that is one pixel wide, even if the positions span a pixel boundary - top = floorf(top); - bottom = top + 1; - } - - *(vertices++) = left; *(vertices++) = top; - *(vertices++) = left; *(vertices++) = bottom; - *(vertices++) = right; *(vertices++) = bottom; - *(vertices++) = right; *(vertices++) = top; - - float colorRed, colorGreen, colorBlue; - double hue = (genome_subpop_id - minSubpopID) / static_cast(maxSubpopID - minSubpopID + 1); - QColor hsbColor = QtSLiMColorWithHSV(hue, 1.0, 1.0, 1.0); - QColor rgbColor = hsbColor.toRgb(); - - colorRed = static_cast(rgbColor.redF()); - colorGreen = static_cast(rgbColor.greenF()); - colorBlue = static_cast(rgbColor.blueF()); - - for (int j = 0; j < 4; ++j) { - *(colors++) = colorRed; *(colors++) = colorGreen; *(colors++) = colorBlue; *(colors++) = 1.0; - } - - displayListIndex++; - - // If we've filled our buffers, get ready to draw more - if (displayListIndex == kMaxGLRects) - { - // Draw our arrays - glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); - - // And get ready to draw more - vertices = glArrayVertices; - colors = glArrayColors; - displayListIndex = 0; - } - - genome_index++; - } - - // Draw any leftovers - if (displayListIndex) - glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); - - glDisableClientState(GL_VERTEX_ARRAY); - glDisableClientState(GL_COLOR_ARRAY); -} - void QtSLiMHaplotypeManager::tallyBincounts(int64_t *bincounts, std::vector &genomeList) { EIDOS_BZERO(bincounts, 1024 * sizeof(int64_t)); @@ -664,154 +566,7 @@ int64_t QtSLiMHaplotypeManager::distanceForBincounts(int64_t *bincounts1, int64_ return distance; } -void QtSLiMHaplotypeManager::drawDisplayListInRect(QRect interior, bool displayBW, int64_t **previousFirstBincounts) -{ - int displayListIndex; - float *vertices = nullptr, *colors = nullptr; - - // Set up to draw rects - displayListIndex = 0; - - vertices = glArrayVertices; - glEnableClientState(GL_VERTEX_ARRAY); - glVertexPointer(2, GL_FLOAT, 0, glArrayVertices); - - colors = glArrayColors; - glEnableClientState(GL_COLOR_ARRAY); - glColorPointer(4, GL_FLOAT, 0, glArrayColors); - - // decide whether to plot in ascending order or descending order; we do this based on a calculated - // similarity to the previously displayed first genome, so that we maximize visual continuity - size_t genome_count = displayList->size(); - bool ascending = true; - - if (previousFirstBincounts && (genome_count > 1)) - { - std::vector &first_genome_list = (*displayList)[0]; - std::vector &last_genome_list = (*displayList)[genome_count - 1]; - static int64_t *first_genome_bincounts = nullptr; - static int64_t *last_genome_bincounts = nullptr; - - if (!first_genome_bincounts) first_genome_bincounts = static_cast(malloc(1024 * sizeof(int64_t))); - if (!last_genome_bincounts) last_genome_bincounts = static_cast(malloc(1024 * sizeof(int64_t))); - - tallyBincounts(first_genome_bincounts, first_genome_list); - tallyBincounts(last_genome_bincounts, last_genome_list); - - if (*previousFirstBincounts) - { - int64_t first_genome_distance = distanceForBincounts(first_genome_bincounts, *previousFirstBincounts); - int64_t last_genome_distance = distanceForBincounts(last_genome_bincounts, *previousFirstBincounts); - - if (first_genome_distance > last_genome_distance) - ascending = false; - - free(*previousFirstBincounts); - } - - // take over one of our buffers, to avoid having to copy values - if (ascending) { - *previousFirstBincounts = first_genome_bincounts; - first_genome_bincounts = nullptr; - } else { - *previousFirstBincounts = last_genome_bincounts; - last_genome_bincounts = nullptr; - } - } - - // Loop through the genomes and draw them; we do this in two passes, neutral mutations underneath selected mutations - for (int pass_count = 0; pass_count <= 1; ++pass_count) - { - bool plotting_neutral = (pass_count == 0); - float height_divisor = genome_count; - float width_subtractor = (usingSubrange ? subrangeFirstBase : 0); - float width_divisor = (usingSubrange ? (subrangeLastBase - subrangeFirstBase + 1) : (mutationLastPosition + 1)); - - for (size_t genome_index = 0; genome_index < genome_count; ++genome_index) - { - std::vector &genome_list = (ascending ? (*displayList)[genome_index] : (*displayList)[(genome_count - 1) - genome_index]); - float top = interior.y() + (genome_index / height_divisor) * interior.height(); - float bottom = interior.y() + ((genome_index + 1) / height_divisor) * interior.height(); - - if (bottom - top > 1.0f) - { - // If the range spans a width of more than one pixel, then use the maximal pixel range - top = floorf(top); - bottom = ceilf(bottom); - } - else - { - // If the range spans a pixel or less, make sure that we end up with a range that is one pixel wide, even if the positions span a pixel boundary - top = floorf(top); - bottom = top + 1; - } - - for (MutationIndex mut_index : genome_list) - { - HaploMutation &mut_info = mutationInfo[mut_index]; - - if (mut_info.neutral_ == plotting_neutral) - { - slim_position_t mut_position = mut_info.position_; - float left = interior.x() + ((mut_position - width_subtractor) / width_divisor) * interior.width(); - float right = interior.x() + ((mut_position - width_subtractor + 1) / width_divisor) * interior.width(); - - if (right - left > 1.0f) - { - // If the range spans a width of more than one pixel, then use the maximal pixel range - left = floorf(left); - right = ceilf(right); - } - else - { - // If the range spans a pixel or less, make sure that we end up with a range that is one pixel wide, even if the positions span a pixel boundary - left = floorf(left); - right = left + 1; - } - - *(vertices++) = left; *(vertices++) = top; - *(vertices++) = left; *(vertices++) = bottom; - *(vertices++) = right; *(vertices++) = bottom; - *(vertices++) = right; *(vertices++) = top; - - float colorRed, colorGreen, colorBlue; - - if (displayBW) { - colorRed = 0; colorGreen = 0; colorBlue = 0; - } else { - colorRed = mut_info.red_; colorGreen = mut_info.green_; colorBlue = mut_info.blue_; - } - - for (int j = 0; j < 4; ++j) { - *(colors++) = colorRed; *(colors++) = colorGreen; *(colors++) = colorBlue; *(colors++) = 1.0; - } - - displayListIndex++; - - // If we've filled our buffers, get ready to draw more - if (displayListIndex == kMaxGLRects) - { - // Draw our arrays - glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); - - // And get ready to draw more - vertices = glArrayVertices; - colors = glArrayColors; - displayListIndex = 0; - } - } - } - } - } - - // Draw any leftovers - if (displayListIndex) - glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); - - glDisableClientState(GL_VERTEX_ARRAY); - glDisableClientState(GL_COLOR_ARRAY); -} - +#ifndef SLIM_NO_OPENGL void QtSLiMHaplotypeManager::glDrawHaplotypes(QRect interior, bool displayBW, bool showSubpopStrips, bool eraseBackground, int64_t **previousFirstBincounts) { // Erase the background to either black or white, depending on displayBW @@ -824,9 +579,6 @@ void QtSLiMHaplotypeManager::glDrawHaplotypes(QRect interior, bool displayBW, bo glRecti(interior.x(), interior.y(), interior.x() + interior.width(), interior.y() + interior.height()); } - // Make sure our GL data buffers are allocated; these are shared among all instances and drawing routines - allocateGLBuffers(); - // Draw subpopulation strips if requested if (showSubpopStrips) { @@ -834,13 +586,38 @@ void QtSLiMHaplotypeManager::glDrawHaplotypes(QRect interior, bool displayBW, bo QRect subpopStripRect = interior; subpopStripRect.setWidth(stripWidth); - drawSubpopStripsInRect(subpopStripRect); + glDrawSubpopStripsInRect(subpopStripRect); interior.adjust(stripWidth, 0, 0, 0); } // Draw the haplotypes in the remaining portion of the interior - drawDisplayListInRect(interior, displayBW, previousFirstBincounts); + glDrawDisplayListInRect(interior, displayBW, previousFirstBincounts); +} +#endif + +void QtSLiMHaplotypeManager::qtDrawHaplotypes(QRect interior, bool displayBW, bool showSubpopStrips, bool eraseBackground, int64_t **previousFirstBincounts, QPainter &painter) +{ + // Erase the background to either black or white, depending on displayBW + if (eraseBackground) + { + painter.fillRect(interior, displayBW ? Qt::white : Qt::black); + } + + // Draw subpopulation strips if requested + if (showSubpopStrips) + { + const int stripWidth = 15; + QRect subpopStripRect = interior; + + subpopStripRect.setWidth(stripWidth); + qtDrawSubpopStripsInRect(subpopStripRect, painter); + + interior.adjust(stripWidth, 0, 0, 0); + } + + // Draw the haplotypes in the remaining portion of the interior + qtDrawDisplayListInRect(interior, displayBW, previousFirstBincounts, painter); } // Traveling Salesman Problem code @@ -1708,9 +1485,18 @@ void QtSLiMHaplotypeManager::do2optOptimizationOfSolution(std::vector &path // This class is private to QtSLiMHaplotypeManager, but is declared here so MOC gets it automatically // -QtSLiMHaplotypeView::QtSLiMHaplotypeView(QWidget *p_parent, Qt::WindowFlags f) : - QOpenGLWidget(p_parent, f) +QtSLiMHaplotypeView::QtSLiMHaplotypeView(QWidget *p_parent, Qt::WindowFlags f) +#ifndef SLIM_NO_OPENGL + : QOpenGLWidget(p_parent, f) +#else + : QWidget(p_parent, f) +#endif { + // We support both OpenGL and non-OpenGL display, because some platforms seem + // to have problems with OpenGL (https://github.com/MesserLab/SLiM/issues/462) + QtSLiMPreferencesNotifier &prefsNotifier = QtSLiMPreferencesNotifier::instance(); + + connect(&prefsNotifier, &QtSLiMPreferencesNotifier::useOpenGLPrefChanged, this, [this]() { update(); }); } QtSLiMHaplotypeView::~QtSLiMHaplotypeView(void) @@ -1718,6 +1504,7 @@ QtSLiMHaplotypeView::~QtSLiMHaplotypeView(void) delegate_ = nullptr; } +#ifndef SLIM_NO_OPENGL void QtSLiMHaplotypeView::initializeGL() { initializeOpenGLFunctions(); @@ -1733,8 +1520,13 @@ void QtSLiMHaplotypeView::resizeGL(int w, int h) glLoadIdentity(); glMatrixMode(GL_MODELVIEW); } +#endif +#ifndef SLIM_NO_OPENGL void QtSLiMHaplotypeView::paintGL() +#else +void QtSLiMHaplotypeView::paintEvent(QPaintEvent * /* p_paint_event */) +#endif { QPainter painter(this); @@ -1749,9 +1541,18 @@ void QtSLiMHaplotypeView::paintGL() if (delegate_) { - painter.beginNativePainting(); - delegate_->glDrawHaplotypes(interior, displayBlackAndWhite_, showSubpopulationStrips_, true, nullptr); - painter.endNativePainting(); +#ifndef SLIM_NO_OPENGL + if (QtSLiMPreferencesNotifier::instance().useOpenGLPref()) + { + painter.beginNativePainting(); + delegate_->glDrawHaplotypes(interior, displayBlackAndWhite_, showSubpopulationStrips_, true, nullptr); + painter.endNativePainting(); + } + else +#endif + { + delegate_->qtDrawHaplotypes(interior, displayBlackAndWhite_, showSubpopulationStrips_, true, nullptr, painter); + } } } @@ -1792,6 +1593,7 @@ void QtSLiMHaplotypeView::contextMenuEvent(QContextMenuEvent *p_event) showSubpopulationStrips_ = !showSubpopulationStrips_; update(); } +#ifndef SLIM_NO_OPENGL if (action == copyPlot) { QImage snap = grabFramebuffer(); @@ -1817,6 +1619,7 @@ void QtSLiMHaplotypeView::contextMenuEvent(QContextMenuEvent *p_event) interior.save(fileName, "PNG", 100); // JPG does not come out well; colors washed out } } +#endif } } diff --git a/QtSLiM/QtSLiMHaplotypeManager.h b/QtSLiM/QtSLiMHaplotypeManager.h index be593597..ad0dd2dd 100644 --- a/QtSLiM/QtSLiMHaplotypeManager.h +++ b/QtSLiM/QtSLiMHaplotypeManager.h @@ -27,8 +27,11 @@ #include #include #include + +#ifndef SLIM_NO_OPENGL #include #include +#endif #include @@ -64,7 +67,10 @@ class QtSLiMHaplotypeManager : public QObject QtSLiMWindow *controller, Species *displaySpecies, size_t sampleSize, bool showProgress); ~QtSLiMHaplotypeManager(void); +#ifndef SLIM_NO_OPENGL void glDrawHaplotypes(QRect interior, bool displayBW, bool showSubpopStrips, bool eraseBackground, int64_t **previousFirstBincounts); +#endif + void qtDrawHaplotypes(QRect interior, bool displayBW, bool showSubpopStrips, bool eraseBackground, int64_t **previousFirstBincounts, QPainter &painter); // Public properties QString titleString; @@ -122,10 +128,18 @@ class QtSLiMHaplotypeManager : public QObject void sortGenomes(void); void configureDisplayBuffers(void); void allocateGLBuffers(void); - void drawSubpopStripsInRect(QRect interior); void tallyBincounts(int64_t *bincounts, std::vector &genomeList); int64_t distanceForBincounts(int64_t *bincounts1, int64_t *bincounts2); - void drawDisplayListInRect(QRect interior, bool displayBW, int64_t **previousFirstBincounts); + + // OpenGL drawing; this is the primary drawing code +#ifndef SLIM_NO_OPENGL + void glDrawSubpopStripsInRect(QRect interior); + void glDrawDisplayListInRect(QRect interior, bool displayBW, int64_t **previousFirstBincounts); +#endif + + // Qt-based drawing, provided as a backup if OpenGL has problems on a given platform + void qtDrawSubpopStripsInRect(QRect interior, QPainter &painter); + void qtDrawDisplayListInRect(QRect interior, bool displayBW, int64_t **previousFirstBincounts, QPainter &painter); int64_t *buildDistanceArray(void); int64_t *buildDistanceArrayForSubrange(void); @@ -146,7 +160,11 @@ class QtSLiMHaplotypeManager : public QObject // This class is private to QtSLiMHaplotypeManager, but is declared here so MOC gets it automatically // +#ifndef SLIM_NO_OPENGL class QtSLiMHaplotypeView : public QOpenGLWidget, protected QOpenGLFunctions +#else +class QtSLiMHaplotypeView : public QWidget +#endif { Q_OBJECT @@ -165,9 +183,13 @@ public slots: bool displayBlackAndWhite_ = false; bool showSubpopulationStrips_ = false; +#ifndef SLIM_NO_OPENGL virtual void initializeGL() override; virtual void resizeGL(int w, int h) override; virtual void paintGL() override; +#else + virtual void paintEvent(QPaintEvent *event) override; +#endif virtual void contextMenuEvent(QContextMenuEvent *p_event) override; }; diff --git a/QtSLiM/QtSLiMHaplotypeManager_GL.cpp b/QtSLiM/QtSLiMHaplotypeManager_GL.cpp new file mode 100644 index 00000000..07ae011d --- /dev/null +++ b/QtSLiM/QtSLiMHaplotypeManager_GL.cpp @@ -0,0 +1,238 @@ +// +// QtSLiMHaplotypeManager_GL.h +// SLiM +// +// Created by Ben Haller on 8/26/2024. +// Copyright (c) 2024 Philipp Messer. All rights reserved. +// A product of the Messer Lab, http://messerlab.org/slim/ +// + +// This file is part of SLiM. +// +// SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +// +// SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with SLiM. If not, see . + + +#ifndef SLIM_NO_OPENGL + +#include "QtSLiMHaplotypeManager.h" +#include "QtSLiMExtras.h" +#include "QtSLiMOpenGL.h" + +#include + +#include +#include +#include + + +void QtSLiMHaplotypeManager::glDrawSubpopStripsInRect(QRect interior) +{ + // Set up to draw rects + SLIM_GL_PREPARE(); + + // Loop through the genomes and draw them; we do this in two passes, neutral mutations underneath selected mutations + size_t genome_index = 0, genome_count = genomeSubpopIDs.size(); + float height_divisor = genome_count; + float left = static_cast(interior.x()); + float right = static_cast(interior.x() + interior.width()); + + for (slim_objectid_t genome_subpop_id : genomeSubpopIDs) + { + float top = interior.y() + (genome_index / height_divisor) * interior.height(); + float bottom = interior.y() + ((genome_index + 1) / height_divisor) * interior.height(); + + if (bottom - top > 1.0f) + { + // If the range spans a width of more than one pixel, then use the maximal pixel range + top = floorf(top); + bottom = ceilf(bottom); + } + else + { + // If the range spans a pixel or less, make sure that we end up with a range that is one pixel wide, even if the positions span a pixel boundary + top = floorf(top); + bottom = top + 1; + } + + SLIM_GL_PUSHRECT(); + + float colorRed, colorGreen, colorBlue, colorAlpha; + double hue = (genome_subpop_id - minSubpopID) / static_cast(maxSubpopID - minSubpopID + 1); + QColor hsbColor = QtSLiMColorWithHSV(hue, 1.0, 1.0, 1.0); + QColor rgbColor = hsbColor.toRgb(); + + colorRed = static_cast(rgbColor.redF()); + colorGreen = static_cast(rgbColor.greenF()); + colorBlue = static_cast(rgbColor.blueF()); + colorAlpha = 1.0; + + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + + genome_index++; + } + + // Draw any leftovers + SLIM_GL_FINISH(); +} + +void QtSLiMHaplotypeManager::glDrawDisplayListInRect(QRect interior, bool displayBW, int64_t **previousFirstBincounts) +{ + // Set up to draw rects + SLIM_GL_PREPARE(); + + // decide whether to plot in ascending order or descending order; we do this based on a calculated + // similarity to the previously displayed first genome, so that we maximize visual continuity + size_t genome_count = displayList->size(); + bool ascending = true; + + if (previousFirstBincounts && (genome_count > 1)) + { + std::vector &first_genome_list = (*displayList)[0]; + std::vector &last_genome_list = (*displayList)[genome_count - 1]; + static int64_t *first_genome_bincounts = nullptr; + static int64_t *last_genome_bincounts = nullptr; + + if (!first_genome_bincounts) first_genome_bincounts = static_cast(malloc(1024 * sizeof(int64_t))); + if (!last_genome_bincounts) last_genome_bincounts = static_cast(malloc(1024 * sizeof(int64_t))); + + tallyBincounts(first_genome_bincounts, first_genome_list); + tallyBincounts(last_genome_bincounts, last_genome_list); + + if (*previousFirstBincounts) + { + int64_t first_genome_distance = distanceForBincounts(first_genome_bincounts, *previousFirstBincounts); + int64_t last_genome_distance = distanceForBincounts(last_genome_bincounts, *previousFirstBincounts); + + if (first_genome_distance > last_genome_distance) + ascending = false; + + free(*previousFirstBincounts); + } + + // take over one of our buffers, to avoid having to copy values + if (ascending) { + *previousFirstBincounts = first_genome_bincounts; + first_genome_bincounts = nullptr; + } else { + *previousFirstBincounts = last_genome_bincounts; + last_genome_bincounts = nullptr; + } + } + + // Loop through the genomes and draw them; we do this in two passes, neutral mutations underneath selected mutations + for (int pass_count = 0; pass_count <= 1; ++pass_count) + { + bool plotting_neutral = (pass_count == 0); + float height_divisor = genome_count; + float width_subtractor = (usingSubrange ? subrangeFirstBase : 0); + float width_divisor = (usingSubrange ? (subrangeLastBase - subrangeFirstBase + 1) : (mutationLastPosition + 1)); + + for (size_t genome_index = 0; genome_index < genome_count; ++genome_index) + { + std::vector &genome_list = (ascending ? (*displayList)[genome_index] : (*displayList)[(genome_count - 1) - genome_index]); + float top = interior.y() + (genome_index / height_divisor) * interior.height(); + float bottom = interior.y() + ((genome_index + 1) / height_divisor) * interior.height(); + + if (bottom - top > 1.0f) + { + // If the range spans a width of more than one pixel, then use the maximal pixel range + top = floorf(top); + bottom = ceilf(bottom); + } + else + { + // If the range spans a pixel or less, make sure that we end up with a range that is one pixel wide, even if the positions span a pixel boundary + top = floorf(top); + bottom = top + 1; + } + + for (MutationIndex mut_index : genome_list) + { + HaploMutation &mut_info = mutationInfo[mut_index]; + + if (mut_info.neutral_ == plotting_neutral) + { + slim_position_t mut_position = mut_info.position_; + float left = interior.x() + ((mut_position - width_subtractor) / width_divisor) * interior.width(); + float right = interior.x() + ((mut_position - width_subtractor + 1) / width_divisor) * interior.width(); + + if (right - left > 1.0f) + { + // If the range spans a width of more than one pixel, then use the maximal pixel range + left = floorf(left); + right = ceilf(right); + } + else + { + // If the range spans a pixel or less, make sure that we end up with a range that is one pixel wide, even if the positions span a pixel boundary + left = floorf(left); + right = left + 1; + } + + SLIM_GL_PUSHRECT(); + + float colorRed, colorGreen, colorBlue, colorAlpha = 1.0; + + if (displayBW) { + colorRed = 0; colorGreen = 0; colorBlue = 0; + } else { + colorRed = mut_info.red_; colorGreen = mut_info.green_; colorBlue = mut_info.blue_; + } + + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + } + } + } + } + + // Draw any leftovers + SLIM_GL_FINISH(); +} + +#endif + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/QtSLiM/QtSLiMHaplotypeManager_QT.cpp b/QtSLiM/QtSLiMHaplotypeManager_QT.cpp new file mode 100644 index 00000000..ea3b2512 --- /dev/null +++ b/QtSLiM/QtSLiMHaplotypeManager_QT.cpp @@ -0,0 +1,236 @@ +// +// QtSLiMHaplotypeManager_QT.h +// SLiM +// +// Created by Ben Haller on 8/26/2024. +// Copyright (c) 2024 Philipp Messer. All rights reserved. +// A product of the Messer Lab, http://messerlab.org/slim/ +// + +// This file is part of SLiM. +// +// SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +// +// SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with SLiM. If not, see . + + +#include "QtSLiMHaplotypeManager.h" +#include "QtSLiMExtras.h" +#include "QtSLiMOpenGL_Emulation.h" + +#include + +#include +#include +#include + + +void QtSLiMHaplotypeManager::qtDrawSubpopStripsInRect(QRect interior, QPainter &painter) +{ + // Set up to draw rects + SLIM_GL_PREPARE(); + + // Loop through the genomes and draw them; we do this in two passes, neutral mutations underneath selected mutations + size_t genome_index = 0, genome_count = genomeSubpopIDs.size(); + float height_divisor = genome_count; + float left = static_cast(interior.x()); + float right = static_cast(interior.x() + interior.width()); + + for (slim_objectid_t genome_subpop_id : genomeSubpopIDs) + { + float top = interior.y() + (genome_index / height_divisor) * interior.height(); + float bottom = interior.y() + ((genome_index + 1) / height_divisor) * interior.height(); + + if (bottom - top > 1.0f) + { + // If the range spans a width of more than one pixel, then use the maximal pixel range + top = floorf(top); + bottom = ceilf(bottom); + } + else + { + // If the range spans a pixel or less, make sure that we end up with a range that is one pixel wide, even if the positions span a pixel boundary + top = floorf(top); + bottom = top + 1; + } + + SLIM_GL_PUSHRECT(); + + float colorRed, colorGreen, colorBlue, colorAlpha; + double hue = (genome_subpop_id - minSubpopID) / static_cast(maxSubpopID - minSubpopID + 1); + QColor hsbColor = QtSLiMColorWithHSV(hue, 1.0, 1.0, 1.0); + QColor rgbColor = hsbColor.toRgb(); + + colorRed = static_cast(rgbColor.redF()); + colorGreen = static_cast(rgbColor.greenF()); + colorBlue = static_cast(rgbColor.blueF()); + colorAlpha = 1.0; + + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS_NORECT(); + + genome_index++; + } + + // Draw any leftovers + SLIM_GL_FINISH(); +} + +void QtSLiMHaplotypeManager::qtDrawDisplayListInRect(QRect interior, bool displayBW, int64_t **previousFirstBincounts, QPainter &painter) +{ + // Set up to draw rects + SLIM_GL_PREPARE(); + + // decide whether to plot in ascending order or descending order; we do this based on a calculated + // similarity to the previously displayed first genome, so that we maximize visual continuity + size_t genome_count = displayList->size(); + bool ascending = true; + + if (previousFirstBincounts && (genome_count > 1)) + { + std::vector &first_genome_list = (*displayList)[0]; + std::vector &last_genome_list = (*displayList)[genome_count - 1]; + static int64_t *first_genome_bincounts = nullptr; + static int64_t *last_genome_bincounts = nullptr; + + if (!first_genome_bincounts) first_genome_bincounts = static_cast(malloc(1024 * sizeof(int64_t))); + if (!last_genome_bincounts) last_genome_bincounts = static_cast(malloc(1024 * sizeof(int64_t))); + + tallyBincounts(first_genome_bincounts, first_genome_list); + tallyBincounts(last_genome_bincounts, last_genome_list); + + if (*previousFirstBincounts) + { + int64_t first_genome_distance = distanceForBincounts(first_genome_bincounts, *previousFirstBincounts); + int64_t last_genome_distance = distanceForBincounts(last_genome_bincounts, *previousFirstBincounts); + + if (first_genome_distance > last_genome_distance) + ascending = false; + + free(*previousFirstBincounts); + } + + // take over one of our buffers, to avoid having to copy values + if (ascending) { + *previousFirstBincounts = first_genome_bincounts; + first_genome_bincounts = nullptr; + } else { + *previousFirstBincounts = last_genome_bincounts; + last_genome_bincounts = nullptr; + } + } + + // Loop through the genomes and draw them; we do this in two passes, neutral mutations underneath selected mutations + for (int pass_count = 0; pass_count <= 1; ++pass_count) + { + bool plotting_neutral = (pass_count == 0); + float height_divisor = genome_count; + float width_subtractor = (usingSubrange ? subrangeFirstBase : 0); + float width_divisor = (usingSubrange ? (subrangeLastBase - subrangeFirstBase + 1) : (mutationLastPosition + 1)); + + for (size_t genome_index = 0; genome_index < genome_count; ++genome_index) + { + std::vector &genome_list = (ascending ? (*displayList)[genome_index] : (*displayList)[(genome_count - 1) - genome_index]); + float top = interior.y() + (genome_index / height_divisor) * interior.height(); + float bottom = interior.y() + ((genome_index + 1) / height_divisor) * interior.height(); + + if (bottom - top > 1.0f) + { + // If the range spans a width of more than one pixel, then use the maximal pixel range + top = floorf(top); + bottom = ceilf(bottom); + } + else + { + // If the range spans a pixel or less, make sure that we end up with a range that is one pixel wide, even if the positions span a pixel boundary + top = floorf(top); + bottom = top + 1; + } + + for (MutationIndex mut_index : genome_list) + { + HaploMutation &mut_info = mutationInfo[mut_index]; + + if (mut_info.neutral_ == plotting_neutral) + { + slim_position_t mut_position = mut_info.position_; + float left = interior.x() + ((mut_position - width_subtractor) / width_divisor) * interior.width(); + float right = interior.x() + ((mut_position - width_subtractor + 1) / width_divisor) * interior.width(); + + if (right - left > 1.0f) + { + // If the range spans a width of more than one pixel, then use the maximal pixel range + left = floorf(left); + right = ceilf(right); + } + else + { + // If the range spans a pixel or less, make sure that we end up with a range that is one pixel wide, even if the positions span a pixel boundary + left = floorf(left); + right = left + 1; + } + + SLIM_GL_PUSHRECT(); + + float colorRed, colorGreen, colorBlue, colorAlpha = 1.0; + + if (displayBW) { + colorRed = 0; colorGreen = 0; colorBlue = 0; + } else { + colorRed = mut_info.red_; colorGreen = mut_info.green_; colorBlue = mut_info.blue_; + } + + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS_NORECT(); + } + } + } + } + + // Draw any leftovers + SLIM_GL_FINISH(); +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/QtSLiM/QtSLiMIndividualsWidget.cpp b/QtSLiM/QtSLiMIndividualsWidget.cpp index e098d6ba..347a2476 100644 --- a/QtSLiM/QtSLiMIndividualsWidget.cpp +++ b/QtSLiM/QtSLiMIndividualsWidget.cpp @@ -20,6 +20,7 @@ #include "QtSLiMIndividualsWidget.h" #include "QtSLiMWindow.h" +#include "QtSLiMPreferences.h" #include #include @@ -34,37 +35,27 @@ #include -// OpenGL constants -static const int kMaxGLRects = 2000; // 2000 rects -static const int kMaxVertices = kMaxGLRects * 4; // 4 vertices each - - -QtSLiMIndividualsWidget::QtSLiMIndividualsWidget(QWidget *p_parent, Qt::WindowFlags f) : QOpenGLWidget(p_parent, f) +QtSLiMIndividualsWidget::QtSLiMIndividualsWidget(QWidget *p_parent, Qt::WindowFlags f) +#ifndef SLIM_NO_OPENGL + : QOpenGLWidget(p_parent, f) +#else + : QWidget(p_parent, f) +#endif { preferredDisplayMode = PopulationViewDisplayMode::kDisplaySpatialSeparate; // prefer spatial display when possible, fall back to individuals - if (!glArrayVertices) - glArrayVertices = static_cast(malloc(kMaxVertices * 2 * sizeof(float))); // 2 floats per vertex, kMaxVertices vertices + // We support both OpenGL and non-OpenGL display, because some platforms seem + // to have problems with OpenGL (https://github.com/MesserLab/SLiM/issues/462) + QtSLiMPreferencesNotifier &prefsNotifier = QtSLiMPreferencesNotifier::instance(); - if (!glArrayColors) - glArrayColors = static_cast(malloc(kMaxVertices * 4 * sizeof(float))); // 4 floats per color, kMaxVertices colors + connect(&prefsNotifier, &QtSLiMPreferencesNotifier::useOpenGLPrefChanged, this, [this]() { update(); }); } QtSLiMIndividualsWidget::~QtSLiMIndividualsWidget() { - if (glArrayVertices) - { - free(glArrayVertices); - glArrayVertices = nullptr; - } - - if (glArrayColors) - { - free(glArrayColors); - glArrayColors = nullptr; - } } +#ifndef SLIM_NO_OPENGL void QtSLiMIndividualsWidget::initializeGL() { initializeOpenGLFunctions(); @@ -80,8 +71,13 @@ void QtSLiMIndividualsWidget::resizeGL(int w, int h) glLoadIdentity(); glMatrixMode(GL_MODELVIEW); } +#endif +#ifndef SLIM_NO_OPENGL void QtSLiMIndividualsWidget::paintGL() +#else +void QtSLiMIndividualsWidget::paintEvent(QPaintEvent * /* p_paint_event */) +#endif { QPainter painter(this); bool inDarkMode = QtSLiMInDarkMode(); @@ -106,30 +102,20 @@ void QtSLiMIndividualsWidget::paintGL() if ((selectedSubpopCount == 0) || !canDisplayAllIndividuals) { // clear to a shade of gray - painter.beginNativePainting(); - if (inDarkMode) - glColor3f(0.118f, 0.118f, 0.118f); + painter.fillRect(QRect(0, 0, bounds.width(), bounds.height()), QtSLiMColorWithWhite(0.118, 1.0)); else - glColor3f(0.9f, 0.9f, 0.9f); - - glRecti(0, 0, bounds.width(), bounds.height()); + painter.fillRect(QRect(0, 0, bounds.width(), bounds.height()), QtSLiMColorWithWhite(0.9, 1.0)); // display a message if we have too many subpops to show if (!canDisplayAllIndividuals) { - painter.endNativePainting(); - painter.setPen(Qt::darkGray); painter.drawText(bounds, Qt::AlignCenter, "too many subpops\nor individuals\nto display – try\nresizing to make\nmore space"); - - painter.beginNativePainting(); } // Frame our view - drawViewFrameInBounds(bounds); - - painter.endNativePainting(); + qtDrawViewFrameInBounds(bounds, painter); } else { @@ -229,7 +215,10 @@ void QtSLiMIndividualsWidget::paintGL() } // And now draw the tiles themselves - painter.beginNativePainting(); + bool useGL = QtSLiMPreferencesNotifier::instance().useOpenGLPref(); + + if (useGL) + painter.beginNativePainting(); bool clearBackground = true; // used for display mode 2 to prevent repeated clearing @@ -259,12 +248,27 @@ void QtSLiMIndividualsWidget::paintGL() // If we have inset the tileBounds because of aspect ratio considerations // in spatialDisplayBoundsForSubpopulation() (which only happens in 2D), // clear to a shade of gray and frame the overall tileBounds - glColor3f(0.9f, 0.9f, 0.9f); - glRecti(tileBounds.left(), tileBounds.top(), (tileBounds.left() + tileBounds.width()), (tileBounds.top() + tileBounds.height())); - drawViewFrameInBounds(tileBounds); +#ifndef SLIM_NO_OPENGL + if (useGL) + { + glColor3f(0.9f, 0.9f, 0.9f); + glRecti(tileBounds.left(), tileBounds.top(), (tileBounds.left() + tileBounds.width()), (tileBounds.top() + tileBounds.height())); + glDrawViewFrameInBounds(tileBounds); + } + else +#endif + { + painter.fillRect(tileBounds, QtSLiMColorWithWhite(0.9, 1.0)); + qtDrawViewFrameInBounds(tileBounds, painter); + } } - drawSpatialBackgroundInBoundsForSubpopulation(spatialDisplayBounds, subpop, displaySpecies->spatial_dimensionality_); +#ifndef SLIM_NO_OPENGL + if (useGL) + glDrawSpatialBackgroundInBoundsForSubpopulation(spatialDisplayBounds, subpop, displaySpecies->spatial_dimensionality_); + else +#endif + qtDrawSpatialBackgroundInBoundsForSubpopulation(spatialDisplayBounds, subpop, displaySpecies->spatial_dimensionality_, painter); } float forceRGB[4]; @@ -276,8 +280,18 @@ void QtSLiMIndividualsWidget::paintGL() forceColor = forceRGB; } - drawSpatialIndividualsFromSubpopulationInArea(subpop, spatialDisplayBounds, displaySpecies->spatial_dimensionality_, forceColor); - drawViewFrameInBounds(frameBounds); // framed more than once in displayMode 2, which is OK +#ifndef SLIM_NO_OPENGL + if (useGL) + { + glDrawSpatialIndividualsFromSubpopulationInArea(subpop, spatialDisplayBounds, displaySpecies->spatial_dimensionality_, forceColor); + glDrawViewFrameInBounds(frameBounds); // framed more than once in displayMode 2, which is OK + } + else +#endif + { + qtDrawSpatialIndividualsFromSubpopulationInArea(subpop, spatialDisplayBounds, displaySpecies->spatial_dimensionality_, forceColor, painter); + qtDrawViewFrameInBounds(frameBounds, painter); // framed more than once in displayMode 2, which is OK + } if (displayMode == 2) clearBackground = false; @@ -293,23 +307,38 @@ void QtSLiMIndividualsWidget::paintGL() background = backgroundIter->second; int backgroundColor = background.backgroundType; + float bgGray = 0.0; if (backgroundColor == 0) - glColor3f(0.0, 0.0, 0.0); + bgGray = 0.0; else if (backgroundColor == 1) - glColor3f(0.3f, 0.3f, 0.3f); + bgGray = 0.3f; else if (backgroundColor == 2) - glColor3f(1.0, 1.0, 1.0); - - glRecti(tileBounds.left(), tileBounds.top(), (tileBounds.left() + tileBounds.width()), (tileBounds.top() + tileBounds.height())); - - drawViewFrameInBounds(tileBounds); - drawIndividualsFromSubpopulationInArea(subpop, tileBounds, squareSize); + bgGray = 1.0; + +#ifndef SLIM_NO_OPENGL + if (useGL) + { + glColor3f(bgGray, bgGray, bgGray); + glRecti(tileBounds.left(), tileBounds.top(), (tileBounds.left() + tileBounds.width()), (tileBounds.top() + tileBounds.height())); + + glDrawViewFrameInBounds(tileBounds); + glDrawIndividualsFromSubpopulationInArea(subpop, tileBounds, squareSize); + } + else +#endif + { + painter.fillRect(tileBounds, QtSLiMColorWithWhite(bgGray, 1.0)); + + qtDrawViewFrameInBounds(tileBounds, painter); + qtDrawIndividualsFromSubpopulationInArea(subpop, tileBounds, squareSize, painter); + } } } } - painter.endNativePainting(); + if (useGL) + painter.endNativePainting(); } } @@ -571,22 +600,6 @@ QRect QtSLiMIndividualsWidget::spatialDisplayBoundsForSubpopulation(Subpopulatio return spatialDisplayBounds; } -void QtSLiMIndividualsWidget::drawViewFrameInBounds(QRect bounds) -{ - int ox = bounds.left(), oy = bounds.top(); - bool inDarkMode = QtSLiMInDarkMode(); - - if (inDarkMode) - glColor3f(0.067f, 0.067f, 0.067f); - else - glColor3f(0.77f, 0.77f, 0.77f); - - glRecti(ox, oy, ox + 1, oy + bounds.height()); - glRecti(ox + 1, oy, ox + bounds.width() - 1, oy + 1); - glRecti(ox + bounds.width() - 1, oy, ox + bounds.width(), oy + bounds.height()); - glRecti(ox + 1, oy + bounds.height() - 1, ox + bounds.width() - 1, oy + bounds.height()); -} - int QtSLiMIndividualsWidget::squareSizeForSubpopulationInArea(Subpopulation *subpop, QRect bounds) { slim_popsize_t subpopSize = subpop->parent_subpop_size_; @@ -613,147 +626,7 @@ int QtSLiMIndividualsWidget::squareSizeForSubpopulationInArea(Subpopulation *sub return squareSize; } -void QtSLiMIndividualsWidget::drawIndividualsFromSubpopulationInArea(Subpopulation *subpop, QRect bounds, int squareSize) -{ - // - // NOTE this code is parallel to the code in canDisplayIndividualsFromSubpopulation:inArea: and should be maintained in parallel - // - - //QtSLiMWindow *controller = dynamic_cast(window()); - double scalingFactor = 0.8; // used to be controller->fitnessColorScale; - slim_popsize_t subpopSize = subpop->parent_subpop_size_; - int viewColumns = 0, viewRows = 0; - - // our square size is given from above (a consensus based on squareSizeForSubpopulationInArea(); calculate metrics from it - viewColumns = static_cast(floor((bounds.width() - 3) / squareSize)); - viewRows = static_cast(floor((bounds.height() - 3) / squareSize)); - - if (viewColumns * viewRows < subpopSize) - squareSize = 1; - - if (squareSize > 1) - { - int squareSpacing = 0; - - // Convert square area to space between squares if possible - if (squareSize > 2) - { - --squareSize; - ++squareSpacing; - } - if (squareSize > 5) - { - --squareSize; - ++squareSpacing; - } - - double excessSpaceX = bounds.width() - ((squareSize + squareSpacing) * viewColumns - squareSpacing); - double excessSpaceY = bounds.height() - ((squareSize + squareSpacing) * viewRows - squareSpacing); - int offsetX = static_cast(floor(excessSpaceX / 2.0)); - int offsetY = static_cast(floor(excessSpaceY / 2.0)); - - // If we have an empty row at the bottom, then we can use the same value for offsetY as for offsetX, for symmetry - if ((subpopSize - 1) / viewColumns < viewRows - 1) - offsetY = offsetX; - - QRect individualArea(bounds.left() + offsetX, bounds.top() + offsetY, bounds.width() - offsetX, bounds.height() - offsetY); - - int individualArrayIndex, displayListIndex; - float *vertices = nullptr, *colors = nullptr; - - // Set up to draw rects - displayListIndex = 0; - - vertices = glArrayVertices; - glEnableClientState(GL_VERTEX_ARRAY); - glVertexPointer(2, GL_FLOAT, 0, glArrayVertices); - - colors = glArrayColors; - glEnableClientState(GL_COLOR_ARRAY); - glColorPointer(4, GL_FLOAT, 0, glArrayColors); - - for (individualArrayIndex = 0; individualArrayIndex < subpopSize; ++individualArrayIndex) - { - // Figure out the rect to draw in; note we now use individualArrayIndex here, because the hit-testing code doesn't have an easy way to calculate the displayed individual index... - float left = static_cast(individualArea.left() + (individualArrayIndex % viewColumns) * (squareSize + squareSpacing)); - float top = static_cast(individualArea.top() + (individualArrayIndex / viewColumns) * (squareSize + squareSpacing)); - float right = left + squareSize; - float bottom = top + squareSize; - - *(vertices++) = left; - *(vertices++) = top; - *(vertices++) = left; - *(vertices++) = bottom; - *(vertices++) = right; - *(vertices++) = bottom; - *(vertices++) = right; - *(vertices++) = top; - - // dark gray default, for a fitness of NaN; should never happen - float colorRed = 0.3f, colorGreen = 0.3f, colorBlue = 0.3f, colorAlpha = 1.0; - Individual &individual = *subpop->parent_individuals_[static_cast(individualArrayIndex)]; - - if (Individual::s_any_individual_color_set_ && individual.color_set_) - { - colorRed = individual.colorR_ / 255.0F; - colorGreen = individual.colorG_ / 255.0F; - colorBlue = individual.colorB_ / 255.0F; - } - else - { - // use individual trait values to determine color; we use fitness values cached in UpdateFitness, so we don't have to call out to mutationEffect() callbacks - // we use cached_unscaled_fitness_ so individual fitness, unscaled by subpopulation fitness, is used for coloring - double fitness = individual.cached_unscaled_fitness_; - - if (!std::isnan(fitness)) - RGBForFitness(fitness, &colorRed, &colorGreen, &colorBlue, scalingFactor); - } - - for (int j = 0; j < 4; ++j) - { - *(colors++) = colorRed; - *(colors++) = colorGreen; - *(colors++) = colorBlue; - *(colors++) = colorAlpha; - } - - displayListIndex++; - - // If we've filled our buffers, get ready to draw more - if (displayListIndex == kMaxGLRects) - { - // Draw our arrays - glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); - - // And get ready to draw more - vertices = glArrayVertices; - colors = glArrayColors; - displayListIndex = 0; - } - } - - // Draw any leftovers - if (displayListIndex) - { - // Draw our arrays - glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); - } - - glDisableClientState(GL_VERTEX_ARRAY); - glDisableClientState(GL_COLOR_ARRAY); - } - else - { - // This is what we do if we cannot display a subpopulation because there are too many individuals in it to display - glColor3f(0.9f, 0.9f, 1.0f); - - int ox = bounds.left(), oy = bounds.top(); - - glRecti(ox + 1, oy + 1, ox + bounds.width() - 1, oy + bounds.height() - 1); - } -} - -void QtSLiMIndividualsWidget::cacheDisplayBufferForMapForSubpopulation(SpatialMap *background_map, Subpopulation *subpop) +void QtSLiMIndividualsWidget::cacheDisplayBufferForMapForSubpopulation(SpatialMap *background_map, Subpopulation *subpop, bool flipped) { // Cache a display buffer for the given background map. This method should be called only in the 2D "xy" // case; in the 1D case we can't know the maximum width ahead of time, so we just draw rects without caching, @@ -779,7 +652,7 @@ void QtSLiMIndividualsWidget::cacheDisplayBufferForMapForSubpopulation(SpatialMa // the Subpopulation's spatialBounds after it has displayed; it should not happen with a window resize. // The user has no way to change the map or the colormap except to set a whole new map, which will also // result in the old one being freed, so we're already safe in those circumstances. - if (background_map->display_buffer_ && ((background_map->buffer_width_ != max_width) || (background_map->buffer_height_ != max_height))) + if (background_map->display_buffer_ && ((background_map->buffer_width_ != max_width) || (background_map->buffer_height_ != max_height) || (background_map->buffer_flipped_ != flipped))) { free(background_map->display_buffer_); background_map->display_buffer_ = nullptr; @@ -791,7 +664,8 @@ void QtSLiMIndividualsWidget::cacheDisplayBufferForMapForSubpopulation(SpatialMa uint8_t *display_buf = static_cast(malloc(static_cast(max_width * max_height * 3) * sizeof(uint8_t))); background_map->display_buffer_ = display_buf; background_map->buffer_width_ = max_width; - background_map->buffer_height_ = max_height; + background_map->buffer_height_ = max_height; + background_map->buffer_flipped_ = flipped; uint8_t *buf_ptr = display_buf; int64_t xsize = background_map->grid_size_[0]; @@ -801,11 +675,12 @@ void QtSLiMIndividualsWidget::cacheDisplayBufferForMapForSubpopulation(SpatialMa for (int yc = 0; yc < max_height; yc++) { + double y_fraction = (flipped ? (((max_height - 1) - yc) + 0.5) / max_height : (yc + 0.5) / max_height); // pixel center + for (int xc = 0; xc < max_width; xc++) { // Look up the nearest map point and get its value; interpolate if requested double x_fraction = (xc + 0.5) / max_width; // pixel center - double y_fraction = (yc + 0.5) / max_height; // pixel center double value; if (interpolate) @@ -849,516 +724,6 @@ void QtSLiMIndividualsWidget::cacheDisplayBufferForMapForSubpopulation(SpatialMa } } -void QtSLiMIndividualsWidget::_drawBackgroundSpatialMap(SpatialMap *background_map, QRect bounds, Subpopulation *subpop, bool showGridPoints) -{ - // We have a spatial map with a color map, so use it to draw the background - int bounds_x1 = bounds.x(); - int bounds_y1 = bounds.y(); - int bounds_x2 = bounds.x() + bounds.width(); - int bounds_y2 = bounds.y() + bounds.height(); - - //glColor3f(0.0, 0.0, 0.0); - //glRecti(bounds_x1, bounds_y1, bounds_x2, bounds_y2); - - int displayListIndex; - float *vertices = nullptr, *colors = nullptr; - - // Set up to draw rects - displayListIndex = 0; - - vertices = glArrayVertices; - glEnableClientState(GL_VERTEX_ARRAY); - glVertexPointer(2, GL_FLOAT, 0, glArrayVertices); - - colors = glArrayColors; - glEnableClientState(GL_COLOR_ARRAY); - glColorPointer(4, GL_FLOAT, 0, glArrayColors); - - if (background_map->spatiality_ == 1) - { - // This is the spatiality "x" and "y" cases; they are the only 1D spatiality values for which SLiMgui will draw - // In the 1D case we can't cache a display buffer, since we don't know what aspect ratio to use, so we just - // draw rects. Whether those rects are horizontal or vertical will depend on the spatiality of the map. Most - // of the code is identical, though, because of the way we handle dimensions, so we share the two cases here. - bool spatiality_is_x = (background_map->spatiality_string_ == "x"); - int64_t xsize = background_map->grid_size_[0]; - double *values = background_map->values_; - - if (background_map->interpolate_) - { - // Interpolation, so we need to draw every line individually - int min_coord = (spatiality_is_x ? bounds_x1 : bounds_y1); - int max_coord = (spatiality_is_x ? bounds_x2 : bounds_y2); - - for (int xc = min_coord; xc < max_coord; ++xc) - { - double x_fraction = (xc + 0.5 - min_coord) / (max_coord - min_coord); // values evaluated at pixel centers - double x_map = x_fraction * (xsize - 1); - int x1_map = static_cast(floor(x_map)); - int x2_map = static_cast(ceil(x_map)); - double fraction_x2 = x_map - x1_map; - double fraction_x1 = 1.0 - fraction_x2; - double value_x1 = values[x1_map] * fraction_x1; - double value_x2 = values[x2_map] * fraction_x2; - double value = value_x1 + value_x2; - - int x1, x2, y1, y2; - - if (spatiality_is_x) - { - x1 = xc; - x2 = xc + 1; - y1 = bounds_y1; - y2 = bounds_y2; - } - else - { - y1 = (max_coord - 1) - xc + min_coord; // flip for y, to use Cartesian coordinates - y2 = y1 + 1; - x1 = bounds_x1; - x2 = bounds_x2; - } - - float rgb[3]; - - background_map->ColorForValue(value, rgb); - - //glColor3f(red, green, blue); - //glRecti(x1, y1, x2, y2); - - *(vertices++) = x1; - *(vertices++) = y1; - *(vertices++) = x1; - *(vertices++) = y2; - *(vertices++) = x2; - *(vertices++) = y2; - *(vertices++) = x2; - *(vertices++) = y1; - - for (int j = 0; j < 4; ++j) - { - *(colors++) = rgb[0]; - *(colors++) = rgb[1]; - *(colors++) = rgb[2]; - *(colors++) = 1.0; - } - - displayListIndex++; - - // If we've filled our buffers, get ready to draw more - if (displayListIndex == kMaxGLRects) - { - // Draw our arrays - glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); - - // And get ready to draw more - vertices = glArrayVertices; - colors = glArrayColors; - displayListIndex = 0; - } - } - } - else - { - // No interpolation, so we can draw whole grid blocks - for (int xc = 0; xc < xsize; xc++) - { - double value = (spatiality_is_x ? values[xc] : values[(xsize - 1) - xc]); // flip for y, to use Cartesian coordinates - int x1, x2, y1, y2; - - if (spatiality_is_x) - { - x1 = qRound(((xc - 0.5) / (xsize - 1)) * bounds.width() + bounds.x()); - x2 = qRound(((xc + 0.5) / (xsize - 1)) * bounds.width() + bounds.x()); - - if (x1 < bounds_x1) x1 = bounds_x1; - if (x2 > bounds_x2) x2 = bounds_x2; - - y1 = bounds_y1; - y2 = bounds_y2; - } - else - { - y1 = qRound(((xc - 0.5) / (xsize - 1)) * bounds.height() + bounds.y()); - y2 = qRound(((xc + 0.5) / (xsize - 1)) * bounds.height() + bounds.y()); - - if (y1 < bounds_y1) y1 = bounds_y1; - if (y2 > bounds_y2) y2 = bounds_y2; - - x1 = bounds_x1; - x2 = bounds_x2; - } - - float rgb[3]; - - background_map->ColorForValue(value, rgb); - - //glColor3f(red, green, blue); - //glRecti(x1, y1, x2, y2); - - *(vertices++) = x1; - *(vertices++) = y1; - *(vertices++) = x1; - *(vertices++) = y2; - *(vertices++) = x2; - *(vertices++) = y2; - *(vertices++) = x2; - *(vertices++) = y1; - - for (int j = 0; j < 4; ++j) - { - *(colors++) = rgb[0]; - *(colors++) = rgb[1]; - *(colors++) = rgb[2]; - *(colors++) = 1.0; - } - - displayListIndex++; - - // If we've filled our buffers, get ready to draw more - if (displayListIndex == kMaxGLRects) - { - // Draw our arrays - glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); - - // And get ready to draw more - vertices = glArrayVertices; - colors = glArrayColors; - displayListIndex = 0; - } - } - } - } - else // if (background_map->spatiality_ == 2) - { - // This is the spatiality "xy" case; it is the only 2D spatiality for which SLiMgui will draw - - // First, cache the display buffer if needed. If this succeeds, we'll use it. - // It should always succeed, so the tile-drawing code below is dead code, kept for parallelism with the 1D case. - cacheDisplayBufferForMapForSubpopulation(background_map, subpop); - - uint8_t *display_buf = background_map->display_buffer_; - - if (display_buf) - { - // Use a cached display buffer to draw. - // FIXME I think there is a bug here somewhere, the boundaries of the pixels fluctuate oddly when the - // individuals pane is resized, even if the actual area the map is displaying in doesn't change size. - // Maybe try using GL_POINTS? - int buf_width = background_map->buffer_width_; - int buf_height = background_map->buffer_height_; - bool display_full_size = ((bounds.width() == buf_width) && (bounds.height() == buf_height)); - float scale_x = bounds.width() / static_cast(buf_width); - float scale_y = bounds.height() / static_cast(buf_height); - - // Then run through the pixels in the display buffer and draw them; this could be done - // with some sort of OpenGL image-drawing method instead, but it's actually already - // remarkably fast, at least on my machine, and drawing an image with OpenGL seems very - // gross, and I tried it once before and couldn't get it to work well... - for (int yc = 0; yc < buf_height; yc++) - { - // We flip the buffer vertically; it's the simplest way to get it into the right coordinate space - uint8_t *buf_ptr = display_buf + ((buf_height - 1) - yc) * buf_width * 3; - - for (int xc = 0; xc < buf_width; xc++) - { - float red = *(buf_ptr++) / 255.0f; - float green = *(buf_ptr++) / 255.0f; - float blue = *(buf_ptr++) / 255.0f; - float left, right, top, bottom; - - if (display_full_size) - { - left = bounds_x1 + xc; - right = left + 1.0f; - top = bounds_y1 + yc; - bottom = top + 1.0f; - } - else - { - left = bounds_x1 + xc * scale_x; - right = bounds_x1 + (xc + 1) * scale_x; - top = bounds_y1 + yc * scale_y; - bottom = bounds_y1 + (yc + 1) * scale_y; - } - - *(vertices++) = left; - *(vertices++) = top; - *(vertices++) = left; - *(vertices++) = bottom; - *(vertices++) = right; - *(vertices++) = bottom; - *(vertices++) = right; - *(vertices++) = top; - - for (int j = 0; j < 4; ++j) - { - *(colors++) = red; - *(colors++) = green; - *(colors++) = blue; - *(colors++) = 1.0; - } - - displayListIndex++; - - // If we've filled our buffers, get ready to draw more - if (displayListIndex == kMaxGLRects) - { - // Draw our arrays - glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); - - // And get ready to draw more - vertices = glArrayVertices; - colors = glArrayColors; - displayListIndex = 0; - } - } - } - } - else - { - // Draw rects for each map tile, without caching. Not as slow as you might expect, - // but for really big maps it does get cumbersome. This is dead code now, overridden - // by the buffer-drawing code above, which also handles interpolation correctly. - int64_t xsize = background_map->grid_size_[0]; - int64_t ysize = background_map->grid_size_[1]; - double *values = background_map->values_; - int n_colors = background_map->n_colors_; - - for (int yc = 0; yc < ysize; yc++) - { - int y1 = qRound(((yc - 0.5) / (ysize - 1)) * bounds.height() + bounds.y()); - int y2 = qRound(((yc + 0.5) / (ysize - 1)) * bounds.height() + bounds.y()); - - if (y1 < bounds_y1) y1 = bounds_y1; - if (y2 > bounds_y2) y2 = bounds_y2; - - // Flip our display, since our coordinate system is flipped relative to our buffer - double *values_row = values + ((ysize - 1) - yc) * xsize; - - for (int xc = 0; xc < xsize; xc++) - { - double value = *(values_row + xc); - int x1 = qRound(((xc - 0.5) / (xsize - 1)) * bounds.width() + bounds.x()); - int x2 = qRound(((xc + 0.5) / (xsize - 1)) * bounds.width() + bounds.x()); - - if (x1 < bounds_x1) x1 = bounds_x1; - if (x2 > bounds_x2) x2 = bounds_x2; - - float value_fraction = (background_map->colors_min_ < background_map->colors_max_) ? static_cast((value - background_map->colors_min_) / (background_map->colors_max_ - background_map->colors_min_)) : 0.0f; - float color_index = value_fraction * (n_colors - 1); - int color_index_1 = static_cast(floorf(color_index)); - int color_index_2 = static_cast(ceilf(color_index)); - - if (color_index_1 < 0) color_index_1 = 0; - if (color_index_1 >= n_colors) color_index_1 = n_colors - 1; - if (color_index_2 < 0) color_index_2 = 0; - if (color_index_2 >= n_colors) color_index_2 = n_colors - 1; - - float color_2_weight = color_index - color_index_1; - float color_1_weight = 1.0f - color_2_weight; - - float red1 = background_map->red_components_[color_index_1]; - float green1 = background_map->green_components_[color_index_1]; - float blue1 = background_map->blue_components_[color_index_1]; - float red2 = background_map->red_components_[color_index_2]; - float green2 = background_map->green_components_[color_index_2]; - float blue2 = background_map->blue_components_[color_index_2]; - float red = red1 * color_1_weight + red2 * color_2_weight; - float green = green1 * color_1_weight + green2 * color_2_weight; - float blue = blue1 * color_1_weight + blue2 * color_2_weight; - - //glColor3f(red, green, blue); - //glRecti(x1, y1, x2, y2); - - *(vertices++) = x1; - *(vertices++) = y1; - *(vertices++) = x1; - *(vertices++) = y2; - *(vertices++) = x2; - *(vertices++) = y2; - *(vertices++) = x2; - *(vertices++) = y1; - - for (int j = 0; j < 4; ++j) - { - *(colors++) = red; - *(colors++) = green; - *(colors++) = blue; - *(colors++) = 1.0; - } - - displayListIndex++; - - // If we've filled our buffers, get ready to draw more - if (displayListIndex == kMaxGLRects) - { - // Draw our arrays - glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); - - // And get ready to draw more - vertices = glArrayVertices; - colors = glArrayColors; - displayListIndex = 0; - } - - //std::cout << "x = " << x << ", y = " << y << ", value = " << value << ": color_index = " << color_index << ", color_index_1 = " << color_index_1 << ", color_index_2 = " << color_index_2 << ", color_1_weight = " << color_1_weight << ", color_2_weight = " << color_2_weight << ", red = " << red << std::endl; - } - } - } - } - - // Draw any leftovers - if (displayListIndex) - { - // Draw our arrays - glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); - } - - glDisableClientState(GL_VERTEX_ARRAY); - glDisableClientState(GL_COLOR_ARRAY); - - if (showGridPoints) - { - // BCH 9/29/2023 new feature: draw boxes showing where the grid nodes are, since that is rather confusing! - float margin_outer = 5.5f; - float margin_inner = 3.5f; - float spacing = 10.0f; - int64_t xsize = background_map->grid_size_[0]; - int64_t ysize = background_map->grid_size_[1]; - double *values = background_map->values_; - - // require that there is sufficient space that we're not just showing a packed grid of squares - // downsize to small and smaller depictions as needed - if (((xsize - 1) * (margin_outer * 2.0 + spacing) > bounds_x2) || ((ysize - 1) * (margin_outer * 2.0 + spacing) > bounds_y2)) - { - margin_outer = 4.5f; - margin_inner = 2.5f; - spacing = 8.0; - } - if (((xsize - 1) * (margin_outer * 2.0 + spacing) > bounds_x2) || ((ysize - 1) * (margin_outer * 2.0 + spacing) > bounds_y2)) - { - margin_outer = 3.5f; - margin_inner = 1.5f; - spacing = 6.0; - } - if (((xsize - 1) * (margin_outer * 2.0 + spacing) > bounds_x2) || ((ysize - 1) * (margin_outer * 2.0 + spacing) > bounds_y2)) - { - margin_outer = 1.0f; - margin_inner = 0.0f; - spacing = 2.0; - } - - if (((xsize - 1) * (margin_outer * 2.0 + spacing) <= bounds_x2) && ((ysize - 1) * (margin_outer * 2.0 + spacing) <= bounds_y2)) - { - // Set up to draw rects - displayListIndex = 0; - - vertices = glArrayVertices; - glEnableClientState(GL_VERTEX_ARRAY); - glVertexPointer(2, GL_FLOAT, 0, glArrayVertices); - - colors = glArrayColors; - glEnableClientState(GL_COLOR_ARRAY); - glColorPointer(4, GL_FLOAT, 0, glArrayColors); - - // first pass we draw squares to make outlines, second pass we draw the interiors in color - for (int pass = 0; pass <= 1; ++pass) - { - const float margin = ((pass == 0) ? margin_outer : margin_inner); - - if (margin == 0.0) - continue; - - for (int x = 0; x < xsize; ++x) - { - for (int y = 0; y < ysize; ++y) - { - float position_x = x / (float)(xsize - 1); // 0 to 1 - float position_y = y / (float)(ysize - 1); // 0 to 1 - - float centerX = (float)(bounds_x1 + round(position_x * bounds.width())); - float centerY = (float)(bounds_y1 + bounds.height() - round(position_y * bounds.height())); - float left = centerX - margin; - float top = centerY - margin; - float right = centerX + margin; - float bottom = centerY + margin; - - if (left < bounds_x1) - left = bounds_x1; - if (top < bounds_y1) - top = bounds_y1; - if (right > bounds_x2) - right = bounds_x2; - if (bottom > bounds_y2) - bottom = bounds_y2; - - *(vertices++) = left; - *(vertices++) = top; - *(vertices++) = left; - *(vertices++) = bottom; - *(vertices++) = right; - *(vertices++) = bottom; - *(vertices++) = right; - *(vertices++) = top; - - if (pass == 0) - { - for (int j = 0; j < 4; ++j) - { - *(colors++) = 1.0; - *(colors++) = 0.25; - *(colors++) = 0.25; - *(colors++) = 1.0; - } - } - else - { - // look up the map's color at this grid point - float rgb[3]; - double value = values[x + y * xsize]; - - background_map->ColorForValue(value, rgb); - - for (int j = 0; j < 4; ++j) - { - *(colors++) = rgb[0]; - *(colors++) = rgb[1]; - *(colors++) = rgb[2]; - *(colors++) = 1.0; - } - } - - displayListIndex++; - - // If we've filled our buffers, get ready to draw more - if (displayListIndex == kMaxGLRects) - { - // Draw our arrays - glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); - - // And get ready to draw more - vertices = glArrayVertices; - colors = glArrayColors; - displayListIndex = 0; - } - } - } - } - - // Draw any leftovers - if (displayListIndex) - { - // Draw our arrays - glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); - } - - glDisableClientState(GL_VERTEX_ARRAY); - glDisableClientState(GL_COLOR_ARRAY); - } - } -} - void QtSLiMIndividualsWidget::chooseDefaultBackgroundSettingsForSubpopulation(PopulationViewSettings *background, SpatialMap **returnMap, Subpopulation *subpop) { PopulationViewDisplayMode displayMode = displayModeForSubpopulation(subpop); @@ -1409,268 +774,6 @@ void QtSLiMIndividualsWidget::chooseDefaultBackgroundSettingsForSubpopulation(Po } } -void QtSLiMIndividualsWidget::drawSpatialBackgroundInBoundsForSubpopulation(QRect bounds, Subpopulation * subpop, int /* dimensionality */) -{ - auto backgroundIter = subviewSettings.find(subpop->subpopulation_id_); - PopulationViewSettings background; - SpatialMap *background_map = nullptr; - - if (backgroundIter == subviewSettings.end()) - { - // The user has not made a choice, so choose a temporary default. We don't want this choice to "stick", - // so that we can, e.g., begin as black and then change to a spatial map if one is defined. - chooseDefaultBackgroundSettingsForSubpopulation(&background, &background_map, subpop); - } - else - { - // The user has made a choice; verify that it is acceptable, and then use it. - background = backgroundIter->second; - - if (background.backgroundType == 3) - { - SpatialMapMap &spatial_maps = subpop->spatial_maps_; - auto map_iter = spatial_maps.find(background.spatialMapName); - - if (map_iter != spatial_maps.end()) - { - background_map = map_iter->second; - - // if the user somehow managed to choose a map that is not of an acceptable dimensionality, reject it here - if ((background_map->spatiality_string_ != "x") && (background_map->spatiality_string_ != "y") && (background_map->spatiality_string_ != "xy")) - background_map = nullptr; - } - } - - // if we're supposed to use a background map but we couldn't find it, or it's unacceptable, revert to black - if ((background.backgroundType == 3) && !background_map) - background.backgroundType = 0; - } - - if ((background.backgroundType == 3) && background_map) - { - _drawBackgroundSpatialMap(background_map, bounds, subpop, background.showGridPoints); - } - else - { - // No background map, so just clear to the preferred background color - int backgroundColor = background.backgroundType; - - if (backgroundColor == 0) - glColor3f(0.0, 0.0, 0.0); - else if (backgroundColor == 1) - glColor3f(0.3f, 0.3f, 0.3f); - else if (backgroundColor == 2) - glColor3f(1.0, 1.0, 1.0); - else - glColor3f(0.0, 0.0, 0.0); - - glRecti(bounds.x(), bounds.y(), bounds.x() + bounds.width(), bounds.y() + bounds.height()); - } -} - -void QtSLiMIndividualsWidget::drawSpatialIndividualsFromSubpopulationInArea(Subpopulation *subpop, QRect bounds, int dimensionality, float *forceColor) -{ - QtSLiMWindow *controller = dynamic_cast(window()); - double scalingFactor = 0.8; // used to be controller->fitnessColorScale; - slim_popsize_t subpopSize = subpop->parent_subpop_size_; - double bounds_x0 = subpop->bounds_x0_, bounds_x1 = subpop->bounds_x1_; - double bounds_y0 = subpop->bounds_y0_, bounds_y1 = subpop->bounds_y1_; - double bounds_x_size = bounds_x1 - bounds_x0, bounds_y_size = bounds_y1 - bounds_y0; - - QRect individualArea(bounds.x(), bounds.y(), bounds.width() - 1, bounds.height() - 1); - - int individualArrayIndex, displayListIndex; - float *vertices = nullptr, *colors = nullptr; - - // Set up to draw rects - displayListIndex = 0; - - vertices = glArrayVertices; - glEnableClientState(GL_VERTEX_ARRAY); - glVertexPointer(2, GL_FLOAT, 0, glArrayVertices); - - colors = glArrayColors; - glEnableClientState(GL_COLOR_ARRAY); - glColorPointer(4, GL_FLOAT, 0, glArrayColors); - - // First we outline all individuals - if (dimensionality == 1) - srandom(static_cast(controller->community->Tick())); - - for (individualArrayIndex = 0; individualArrayIndex < subpopSize; ++individualArrayIndex) - { - // Figure out the rect to draw in; note we now use individualArrayIndex here, because the hit-testing code doesn't have an easy way to calculate the displayed individual index... - Individual &individual = *subpop->parent_individuals_[static_cast(individualArrayIndex)]; - float position_x, position_y; - - if (dimensionality == 1) - { - position_x = static_cast((individual.spatial_x_ - bounds_x0) / bounds_x_size); - position_y = static_cast(random() / static_cast(INT32_MAX)); - - if ((position_x < 0.0f) || (position_x > 1.0f)) // skip points that are out of bounds - continue; - } - else - { - position_x = static_cast((individual.spatial_x_ - bounds_x0) / bounds_x_size); - position_y = static_cast((individual.spatial_y_ - bounds_y0) / bounds_y_size); - - if ((position_x < 0.0f) || (position_x > 1.0f) || (position_y < 0.0f) || (position_y > 1.0f)) // skip points that are out of bounds - continue; - } - - float centerX = static_cast(individualArea.x() + round(position_x * individualArea.width()) + 0.5f); - float centerY = static_cast(individualArea.y() + individualArea.height() - round(position_y * individualArea.height()) + 0.5f); - - float left = centerX - 2.5f; - float top = centerY - 2.5f; - float right = centerX + 2.5f; - float bottom = centerY + 2.5f; - - if (left < individualArea.x()) left = static_cast(individualArea.x()); - if (top < individualArea.y()) top = static_cast(individualArea.y()); - if (right > individualArea.x() + individualArea.width() + 1) right = static_cast(individualArea.x() + individualArea.width() + 1); - if (bottom > individualArea.y() + individualArea.height() + 1) bottom = static_cast(individualArea.y() + individualArea.height() + 1); - - *(vertices++) = left; - *(vertices++) = top; - *(vertices++) = left; - *(vertices++) = bottom; - *(vertices++) = right; - *(vertices++) = bottom; - *(vertices++) = right; - *(vertices++) = top; - - for (int j = 0; j < 4; ++j) - { - *(colors++) = 0.25; - *(colors++) = 0.25; - *(colors++) = 0.25; - *(colors++) = 1.0; - } - - displayListIndex++; - - // If we've filled our buffers, get ready to draw more - if (displayListIndex == kMaxGLRects) - { - // Draw our arrays - glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); - - // And get ready to draw more - vertices = glArrayVertices; - colors = glArrayColors; - displayListIndex = 0; - } - } - - // Then we draw all individuals - if (dimensionality == 1) - srandom(static_cast(controller->community->Tick())); - - for (individualArrayIndex = 0; individualArrayIndex < subpopSize; ++individualArrayIndex) - { - // Figure out the rect to draw in; note we now use individualArrayIndex here, because the hit-testing code doesn't have an easy way to calculate the displayed individual index... - Individual &individual = *subpop->parent_individuals_[static_cast(individualArrayIndex)]; - float position_x, position_y; - - if (dimensionality == 1) - { - position_x = static_cast((individual.spatial_x_ - bounds_x0) / bounds_x_size); - position_y = static_cast(random() / static_cast(INT32_MAX)); - - if ((position_x < 0.0f) || (position_x > 1.0f)) // skip points that are out of bounds - continue; - } - else - { - position_x = static_cast((individual.spatial_x_ - bounds_x0) / bounds_x_size); - position_y = static_cast((individual.spatial_y_ - bounds_y0) / bounds_y_size); - - if ((position_x < 0.0f) || (position_x > 1.0f) || (position_y < 0.0f) || (position_y > 1.0f)) // skip points that are out of bounds - continue; - } - - float centerX = static_cast(individualArea.x() + round(position_x * individualArea.width()) + 0.5f); - float centerY = static_cast(individualArea.y() + individualArea.height() - round(position_y * individualArea.height()) + 0.5f); - float left = centerX - 1.5f; - float top = centerY - 1.5f; - float right = centerX + 1.5f; - float bottom = centerY + 1.5f; - - // clipping deliberately not done here; because individual rects are 3x3, they will fall at most one pixel - // outside our drawing area, and thus the flaw will be covered by the view frame when it overdraws - - *(vertices++) = left; - *(vertices++) = top; - *(vertices++) = left; - *(vertices++) = bottom; - *(vertices++) = right; - *(vertices++) = bottom; - *(vertices++) = right; - *(vertices++) = top; - - // dark gray default, for a fitness of NaN; should never happen - float colorRed = 0.3f, colorGreen = 0.3f, colorBlue = 0.3f, colorAlpha = 1.0; - - if (Individual::s_any_individual_color_set_ && individual.color_set_) - { - colorRed = individual.colorR_ / 255.0F; - colorGreen = individual.colorG_ / 255.0F; - colorBlue = individual.colorB_ / 255.0F; - } - else if (forceColor) - { - // forceColor is used to make each species draw with a distinctive color in multispecies models in unified display mode - colorRed = forceColor[0]; - colorGreen = forceColor[1]; - colorBlue = forceColor[2]; - } - else - { - // use individual trait values to determine color; we used fitness values cached in UpdateFitness, so we don't have to call out to mutationEffect() callbacks - // we use cached_unscaled_fitness_ so individual fitness, unscaled by subpopulation fitness, is used for coloring - double fitness = individual.cached_unscaled_fitness_; - - if (!std::isnan(fitness)) - RGBForFitness(fitness, &colorRed, &colorGreen, &colorBlue, scalingFactor); - } - - for (int j = 0; j < 4; ++j) - { - *(colors++) = colorRed; - *(colors++) = colorGreen; - *(colors++) = colorBlue; - *(colors++) = colorAlpha; - } - - displayListIndex++; - - // If we've filled our buffers, get ready to draw more - if (displayListIndex == kMaxGLRects) - { - // Draw our arrays - glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); - - // And get ready to draw more - vertices = glArrayVertices; - colors = glArrayColors; - displayListIndex = 0; - } - } - - // Draw any leftovers - if (displayListIndex) - { - // Draw our arrays - glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); - } - - glDisableClientState(GL_VERTEX_ARRAY); - glDisableClientState(GL_COLOR_ARRAY); -} - void QtSLiMIndividualsWidget::runContextMenuAtPoint(QPoint globalPoint, Subpopulation *subpopForEvent) { QtSLiMWindow *controller = dynamic_cast(window()); diff --git a/QtSLiM/QtSLiMIndividualsWidget.h b/QtSLiM/QtSLiMIndividualsWidget.h index 928a0d10..eed90ddc 100644 --- a/QtSLiM/QtSLiMIndividualsWidget.h +++ b/QtSLiM/QtSLiMIndividualsWidget.h @@ -24,8 +24,11 @@ #define GL_SILENCE_DEPRECATION #include + +#ifndef SLIM_NO_OPENGL #include #include +#endif #include "slim_globals.h" #include "subpopulation.h" @@ -46,7 +49,11 @@ typedef enum { kDisplaySpatialUnified } PopulationViewDisplayMode; +#ifndef SLIM_NO_OPENGL class QtSLiMIndividualsWidget : public QOpenGLWidget, protected QOpenGLFunctions +#else +class QtSLiMIndividualsWidget : public QWidget +#endif { Q_OBJECT @@ -63,10 +70,6 @@ class QtSLiMIndividualsWidget : public QOpenGLWidget, protected QOpenGLFunctions // action button tracking support slim_objectid_t actionButtonHighlightSubpopID_ = -1; - // OpenGL buffers - float *glArrayVertices = nullptr; - float *glArrayColors = nullptr; - public: explicit QtSLiMIndividualsWidget(QWidget *p_parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()); virtual ~QtSLiMIndividualsWidget() override; @@ -75,28 +78,40 @@ class QtSLiMIndividualsWidget : public QOpenGLWidget, protected QOpenGLFunctions void runContextMenuAtPoint(QPoint globalPoint, Subpopulation *subpopForEvent); protected: +#ifndef SLIM_NO_OPENGL virtual void initializeGL() override; virtual void resizeGL(int w, int h) override; virtual void paintGL() override; +#else + virtual void paintEvent(QPaintEvent *event) override; +#endif bool canDisplayUnified(std::vector &selectedSubpopulations); PopulationViewDisplayMode displayModeForSubpopulation(Subpopulation *subpopulation); bool canDisplayIndividualsFromSubpopulationInArea(Subpopulation *subpop, QRect bounds); QRect spatialDisplayBoundsForSubpopulation(Subpopulation *subpop, QRect tileBounds); - void drawViewFrameInBounds(QRect bounds); - int squareSizeForSubpopulationInArea(Subpopulation *subpop, QRect bounds); - void drawIndividualsFromSubpopulationInArea(Subpopulation *subpop, QRect bounds, int squareSize); - - void cacheDisplayBufferForMapForSubpopulation(SpatialMap *background_map, Subpopulation *subpop); - void _drawBackgroundSpatialMap(SpatialMap *background_map, QRect bounds, Subpopulation *subpop, bool showGridPoints); + void cacheDisplayBufferForMapForSubpopulation(SpatialMap *background_map, Subpopulation *subpop, bool flipped); void chooseDefaultBackgroundSettingsForSubpopulation(PopulationViewSettings *settings, SpatialMap **returnMap, Subpopulation *subpop); - void drawSpatialBackgroundInBoundsForSubpopulation(QRect bounds, Subpopulation * subpop, int dimensionality); - void drawSpatialIndividualsFromSubpopulationInArea(Subpopulation *subpop, QRect bounds, int dimensionality, float *forceColor); - virtual void contextMenuEvent(QContextMenuEvent *p_event) override; + // OpenGL drawing; this is the primary drawing code +#ifndef SLIM_NO_OPENGL + void glDrawViewFrameInBounds(QRect bounds); + void glDrawIndividualsFromSubpopulationInArea(Subpopulation *subpop, QRect bounds, int squareSize); + void _glDrawBackgroundSpatialMap(SpatialMap *background_map, QRect bounds, Subpopulation *subpop, bool showGridPoints); + void glDrawSpatialBackgroundInBoundsForSubpopulation(QRect bounds, Subpopulation * subpop, int dimensionality); + void glDrawSpatialIndividualsFromSubpopulationInArea(Subpopulation *subpop, QRect bounds, int dimensionality, float *forceColor); +#endif + + // Qt-based drawing, provided as a backup if OpenGL has problems on a given platform + void qtDrawViewFrameInBounds(QRect bounds, QPainter &painter); + void qtDrawIndividualsFromSubpopulationInArea(Subpopulation *subpop, QRect bounds, int squareSize, QPainter &painter); + void _qtDrawBackgroundSpatialMap(SpatialMap *background_map, QRect bounds, Subpopulation *subpop, bool showGridPoints, QPainter &painter); + void qtDrawSpatialBackgroundInBoundsForSubpopulation(QRect bounds, Subpopulation * subpop, int dimensionality, QPainter &painter); + void qtDrawSpatialIndividualsFromSubpopulationInArea(Subpopulation *subpop, QRect bounds, int dimensionality, float *forceColor, QPainter &painter); + virtual void contextMenuEvent(QContextMenuEvent *p_event) override; virtual void mousePressEvent(QMouseEvent *p_event) override; }; diff --git a/QtSLiM/QtSLiMIndividualsWidget_GL.cpp b/QtSLiM/QtSLiMIndividualsWidget_GL.cpp new file mode 100644 index 00000000..1be50816 --- /dev/null +++ b/QtSLiM/QtSLiMIndividualsWidget_GL.cpp @@ -0,0 +1,752 @@ +// +// QtSLiMIndividualsWidget_GL.cpp +// SLiM +// +// Created by Ben Haller on 8/25/2024. +// Copyright (c) 2024 Philipp Messer. All rights reserved. +// A product of the Messer Lab, http://messerlab.org/slim/ +// + +// This file is part of SLiM. +// +// SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +// +// SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with SLiM. If not, see . + + +#ifndef SLIM_NO_OPENGL + +#include "QtSLiMIndividualsWidget.h" +#include "QtSLiMWindow.h" +#include "QtSLiMOpenGL.h" + +#include + +#include +#include +#include +#include + + +// +// OpenGL-based drawing; maintain this in parallel with the Qt-based drawing! +// + +void QtSLiMIndividualsWidget::glDrawViewFrameInBounds(QRect bounds) +{ + int ox = bounds.left(), oy = bounds.top(); + bool inDarkMode = QtSLiMInDarkMode(); + + if (inDarkMode) + glColor3f(0.067f, 0.067f, 0.067f); + else + glColor3f(0.77f, 0.77f, 0.77f); + + glRecti(ox, oy, ox + 1, oy + bounds.height()); + glRecti(ox + 1, oy, ox + bounds.width() - 1, oy + 1); + glRecti(ox + bounds.width() - 1, oy, ox + bounds.width(), oy + bounds.height()); + glRecti(ox + 1, oy + bounds.height() - 1, ox + bounds.width() - 1, oy + bounds.height()); +} + +void QtSLiMIndividualsWidget::glDrawIndividualsFromSubpopulationInArea(Subpopulation *subpop, QRect bounds, int squareSize) +{ + // + // NOTE this code is parallel to the code in canDisplayIndividualsFromSubpopulation:inArea: and should be maintained in parallel + // + + //QtSLiMWindow *controller = dynamic_cast(window()); + double scalingFactor = 0.8; // used to be controller->fitnessColorScale; + slim_popsize_t subpopSize = subpop->parent_subpop_size_; + int viewColumns = 0, viewRows = 0; + + // our square size is given from above (a consensus based on squareSizeForSubpopulationInArea(); calculate metrics from it + viewColumns = static_cast(floor((bounds.width() - 3) / squareSize)); + viewRows = static_cast(floor((bounds.height() - 3) / squareSize)); + + if (viewColumns * viewRows < subpopSize) + squareSize = 1; + + if (squareSize > 1) + { + int squareSpacing = 0; + + // Convert square area to space between squares if possible + if (squareSize > 2) + { + --squareSize; + ++squareSpacing; + } + if (squareSize > 5) + { + --squareSize; + ++squareSpacing; + } + + double excessSpaceX = bounds.width() - ((squareSize + squareSpacing) * viewColumns - squareSpacing); + double excessSpaceY = bounds.height() - ((squareSize + squareSpacing) * viewRows - squareSpacing); + int offsetX = static_cast(floor(excessSpaceX / 2.0)); + int offsetY = static_cast(floor(excessSpaceY / 2.0)); + + // If we have an empty row at the bottom, then we can use the same value for offsetY as for offsetX, for symmetry + if ((subpopSize - 1) / viewColumns < viewRows - 1) + offsetY = offsetX; + + QRect individualArea(bounds.left() + offsetX, bounds.top() + offsetY, bounds.width() - offsetX, bounds.height() - offsetY); + + int individualArrayIndex; + + // Set up to draw rects + SLIM_GL_PREPARE(); + + for (individualArrayIndex = 0; individualArrayIndex < subpopSize; ++individualArrayIndex) + { + // Figure out the rect to draw in; note we now use individualArrayIndex here, because the hit-testing code doesn't have an easy way to calculate the displayed individual index... + float left = static_cast(individualArea.left() + (individualArrayIndex % viewColumns) * (squareSize + squareSpacing)); + float top = static_cast(individualArea.top() + (individualArrayIndex / viewColumns) * (squareSize + squareSpacing)); + float right = left + squareSize; + float bottom = top + squareSize; + + SLIM_GL_PUSHRECT(); + + // dark gray default, for a fitness of NaN; should never happen + float colorRed = 0.3f, colorGreen = 0.3f, colorBlue = 0.3f, colorAlpha = 1.0; + Individual &individual = *subpop->parent_individuals_[static_cast(individualArrayIndex)]; + + if (Individual::s_any_individual_color_set_ && individual.color_set_) + { + colorRed = individual.colorR_ / 255.0F; + colorGreen = individual.colorG_ / 255.0F; + colorBlue = individual.colorB_ / 255.0F; + } + else + { + // use individual trait values to determine color; we use fitness values cached in UpdateFitness, so we don't have to call out to mutationEffect() callbacks + // we use cached_unscaled_fitness_ so individual fitness, unscaled by subpopulation fitness, is used for coloring + double fitness = individual.cached_unscaled_fitness_; + + if (!std::isnan(fitness)) + RGBForFitness(fitness, &colorRed, &colorGreen, &colorBlue, scalingFactor); + } + + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + } + + // Draw any leftovers + SLIM_GL_FINISH(); + } + else + { + // This is what we do if we cannot display a subpopulation because there are too many individuals in it to display + glColor3f(0.9f, 0.9f, 1.0f); + + int ox = bounds.left(), oy = bounds.top(); + + glRecti(ox + 1, oy + 1, ox + bounds.width() - 1, oy + bounds.height() - 1); + } +} + +void QtSLiMIndividualsWidget::_glDrawBackgroundSpatialMap(SpatialMap *background_map, QRect bounds, Subpopulation *subpop, bool showGridPoints) +{ + // We have a spatial map with a color map, so use it to draw the background + int bounds_x1 = bounds.x(); + int bounds_y1 = bounds.y(); + int bounds_x2 = bounds.x() + bounds.width(); + int bounds_y2 = bounds.y() + bounds.height(); + + //glColor3f(0.0, 0.0, 0.0); + //glRecti(bounds_x1, bounds_y1, bounds_x2, bounds_y2); + + { + // Set up to draw rects + SLIM_GL_PREPARE(); + + if (background_map->spatiality_ == 1) + { + // This is the spatiality "x" and "y" cases; they are the only 1D spatiality values for which SLiMgui will draw + // In the 1D case we can't cache a display buffer, since we don't know what aspect ratio to use, so we just + // draw rects. Whether those rects are horizontal or vertical will depend on the spatiality of the map. Most + // of the code is identical, though, because of the way we handle dimensions, so we share the two cases here. + bool spatiality_is_x = (background_map->spatiality_string_ == "x"); + int64_t xsize = background_map->grid_size_[0]; + double *values = background_map->values_; + + if (background_map->interpolate_) + { + // Interpolation, so we need to draw every line individually + int min_coord = (spatiality_is_x ? bounds_x1 : bounds_y1); + int max_coord = (spatiality_is_x ? bounds_x2 : bounds_y2); + + for (int xc = min_coord; xc < max_coord; ++xc) + { + double x_fraction = (xc + 0.5 - min_coord) / (max_coord - min_coord); // values evaluated at pixel centers + double x_map = x_fraction * (xsize - 1); + int x1_map = static_cast(floor(x_map)); + int x2_map = static_cast(ceil(x_map)); + double fraction_x2 = x_map - x1_map; + double fraction_x1 = 1.0 - fraction_x2; + double value_x1 = values[x1_map] * fraction_x1; + double value_x2 = values[x2_map] * fraction_x2; + double value = value_x1 + value_x2; + + float left, right, top, bottom; + + if (spatiality_is_x) + { + left = xc; + right = xc + 1; + top = bounds_y1; + bottom = bounds_y2; + } + else + { + top = (max_coord - 1) - xc + min_coord; // flip for y, to use Cartesian coordinates + bottom = top + 1; + left = bounds_x1; + right = bounds_x2; + } + + float rgb[3]; + background_map->ColorForValue(value, rgb); + + float colorRed = rgb[0]; + float colorGreen = rgb[1]; + float colorBlue = rgb[2]; + float colorAlpha = 1.0; + + //glColor3f(colorRed, colorGreen, colorBlue); + //glRecti(x1, y1, x2, y2); + + SLIM_GL_PUSHRECT(); + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + } + } + else + { + // No interpolation, so we can draw whole grid blocks + for (int xc = 0; xc < xsize; xc++) + { + double value = (spatiality_is_x ? values[xc] : values[(xsize - 1) - xc]); // flip for y, to use Cartesian coordinates + float left, right, top, bottom; + + if (spatiality_is_x) + { + left = qRound(((xc - 0.5) / (xsize - 1)) * bounds.width() + bounds.x()); + right = qRound(((xc + 0.5) / (xsize - 1)) * bounds.width() + bounds.x()); + + if (left < bounds_x1) left = bounds_x1; + if (right > bounds_x2) right = bounds_x2; + + top = bounds_y1; + bottom = bounds_y2; + } + else + { + top = qRound(((xc - 0.5) / (xsize - 1)) * bounds.height() + bounds.y()); + bottom = qRound(((xc + 0.5) / (xsize - 1)) * bounds.height() + bounds.y()); + + if (top < bounds_y1) top = bounds_y1; + if (bottom > bounds_y2) bottom = bounds_y2; + + left = bounds_x1; + right = bounds_x2; + } + + float rgb[3]; + + background_map->ColorForValue(value, rgb); + + float colorRed = rgb[0]; + float colorGreen = rgb[1]; + float colorBlue = rgb[2]; + float colorAlpha = 1.0; + + //glColor3f(colorRed, colorGreen, colorBlue); + //glRecti(x1, y1, x2, y2); + + SLIM_GL_PUSHRECT(); + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + } + } + } + else // if (background_map->spatiality_ == 2) + { + // This is the spatiality "xy" case; it is the only 2D spatiality for which SLiMgui will draw + + // First, cache the display buffer if needed. If this succeeds, we'll use it. + // It should always succeed, so the tile-drawing code below is dead code, kept for parallelism with the 1D case. + cacheDisplayBufferForMapForSubpopulation(background_map, subpop, /* flipped */ false); + + uint8_t *display_buf = background_map->display_buffer_; + + if (display_buf) + { + // Use a cached display buffer to draw. + // FIXME I think there is a bug here somewhere, the boundaries of the pixels fluctuate oddly when the + // individuals pane is resized, even if the actual area the map is displaying in doesn't change size. + // Maybe try using GL_POINTS? + int buf_width = background_map->buffer_width_; + int buf_height = background_map->buffer_height_; + bool display_full_size = ((bounds.width() == buf_width) && (bounds.height() == buf_height)); + float scale_x = bounds.width() / static_cast(buf_width); + float scale_y = bounds.height() / static_cast(buf_height); + + // Then run through the pixels in the display buffer and draw them; this could be done + // with some sort of OpenGL image-drawing method instead, but it's actually already + // remarkably fast, at least on my machine, and drawing an image with OpenGL seems very + // gross, and I tried it once before and couldn't get it to work well... + for (int yc = 0; yc < buf_height; yc++) + { + // We flip the buffer vertically; it's the simplest way to get it into the right coordinate space + uint8_t *buf_ptr = display_buf + ((buf_height - 1) - yc) * buf_width * 3; + + for (int xc = 0; xc < buf_width; xc++) + { + float colorRed = *(buf_ptr++) / 255.0f; + float colorGreen = *(buf_ptr++) / 255.0f; + float colorBlue = *(buf_ptr++) / 255.0f; + float colorAlpha = 1.0; + float left, right, top, bottom; + + if (display_full_size) + { + left = bounds_x1 + xc; + right = left + 1.0f; + top = bounds_y1 + yc; + bottom = top + 1.0f; + } + else + { + left = bounds_x1 + xc * scale_x; + right = bounds_x1 + (xc + 1) * scale_x; + top = bounds_y1 + yc * scale_y; + bottom = bounds_y1 + (yc + 1) * scale_y; + } + + SLIM_GL_PUSHRECT(); + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + } + } + } + else + { + // Draw rects for each map tile, without caching. Not as slow as you might expect, + // but for really big maps it does get cumbersome. This is dead code now, overridden + // by the buffer-drawing code above, which also handles interpolation correctly. + int64_t xsize = background_map->grid_size_[0]; + int64_t ysize = background_map->grid_size_[1]; + double *values = background_map->values_; + int n_colors = background_map->n_colors_; + + for (int yc = 0; yc < ysize; yc++) + { + float top = qRound(((yc - 0.5) / (ysize - 1)) * bounds.height() + bounds.y()); + float bottom = qRound(((yc + 0.5) / (ysize - 1)) * bounds.height() + bounds.y()); + + if (top < bounds_y1) top = bounds_y1; + if (bottom > bounds_y2) bottom = bounds_y2; + + // Flip our display, since our coordinate system is flipped relative to our buffer + double *values_row = values + ((ysize - 1) - yc) * xsize; + + for (int xc = 0; xc < xsize; xc++) + { + double value = *(values_row + xc); + float left = qRound(((xc - 0.5) / (xsize - 1)) * bounds.width() + bounds.x()); + float right = qRound(((xc + 0.5) / (xsize - 1)) * bounds.width() + bounds.x()); + + if (left < bounds_x1) left = bounds_x1; + if (right > bounds_x2) right = bounds_x2; + + float value_fraction = (background_map->colors_min_ < background_map->colors_max_) ? static_cast((value - background_map->colors_min_) / (background_map->colors_max_ - background_map->colors_min_)) : 0.0f; + float color_index = value_fraction * (n_colors - 1); + int color_index_1 = static_cast(floorf(color_index)); + int color_index_2 = static_cast(ceilf(color_index)); + + if (color_index_1 < 0) color_index_1 = 0; + if (color_index_1 >= n_colors) color_index_1 = n_colors - 1; + if (color_index_2 < 0) color_index_2 = 0; + if (color_index_2 >= n_colors) color_index_2 = n_colors - 1; + + float color_2_weight = color_index - color_index_1; + float color_1_weight = 1.0f - color_2_weight; + + float red1 = background_map->red_components_[color_index_1]; + float green1 = background_map->green_components_[color_index_1]; + float blue1 = background_map->blue_components_[color_index_1]; + float red2 = background_map->red_components_[color_index_2]; + float green2 = background_map->green_components_[color_index_2]; + float blue2 = background_map->blue_components_[color_index_2]; + float colorRed = red1 * color_1_weight + red2 * color_2_weight; + float colorGreen = green1 * color_1_weight + green2 * color_2_weight; + float colorBlue = blue1 * color_1_weight + blue2 * color_2_weight; + float colorAlpha = 1.0; + + //glColor3f(red, green, blue); + //glRecti(x1, y1, x2, y2); + + SLIM_GL_PUSHRECT(); + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + + //std::cout << "x = " << x << ", y = " << y << ", value = " << value << ": color_index = " << color_index << ", color_index_1 = " << color_index_1 << ", color_index_2 = " << color_index_2 << ", color_1_weight = " << color_1_weight << ", color_2_weight = " << color_2_weight << ", red = " << red << std::endl; + } + } + } + } + + // Draw any leftovers + SLIM_GL_FINISH(); + } + + if (showGridPoints) + { + // BCH 9/29/2023 new feature: draw boxes showing where the grid nodes are, since that is rather confusing! + float margin_outer = 5.5f; + float margin_inner = 3.5f; + float spacing = 10.0f; + int64_t xsize = background_map->grid_size_[0]; + int64_t ysize = background_map->grid_size_[1]; + double *values = background_map->values_; + + // require that there is sufficient space that we're not just showing a packed grid of squares + // downsize to small and smaller depictions as needed + if (((xsize - 1) * (margin_outer * 2.0 + spacing) > bounds_x2) || ((ysize - 1) * (margin_outer * 2.0 + spacing) > bounds_y2)) + { + margin_outer = 4.5f; + margin_inner = 2.5f; + spacing = 8.0; + } + if (((xsize - 1) * (margin_outer * 2.0 + spacing) > bounds_x2) || ((ysize - 1) * (margin_outer * 2.0 + spacing) > bounds_y2)) + { + margin_outer = 3.5f; + margin_inner = 1.5f; + spacing = 6.0; + } + if (((xsize - 1) * (margin_outer * 2.0 + spacing) > bounds_x2) || ((ysize - 1) * (margin_outer * 2.0 + spacing) > bounds_y2)) + { + margin_outer = 1.0f; + margin_inner = 0.0f; + spacing = 2.0; + } + + if (((xsize - 1) * (margin_outer * 2.0 + spacing) <= bounds_x2) && ((ysize - 1) * (margin_outer * 2.0 + spacing) <= bounds_y2)) + { + // Set up to draw rects + SLIM_GL_PREPARE(); + + // first pass we draw squares to make outlines, second pass we draw the interiors in color + for (int pass = 0; pass <= 1; ++pass) + { + const float margin = ((pass == 0) ? margin_outer : margin_inner); + + if (margin == 0.0) + continue; + + for (int x = 0; x < xsize; ++x) + { + for (int y = 0; y < ysize; ++y) + { + float position_x = x / (float)(xsize - 1); // 0 to 1 + float position_y = y / (float)(ysize - 1); // 0 to 1 + + float centerX = (float)(bounds_x1 + round(position_x * bounds.width())); + float centerY = (float)(bounds_y1 + bounds.height() - round(position_y * bounds.height())); + float left = centerX - margin; + float top = centerY - margin; + float right = centerX + margin; + float bottom = centerY + margin; + + if (left < bounds_x1) + left = bounds_x1; + if (top < bounds_y1) + top = bounds_y1; + if (right > bounds_x2) + right = bounds_x2; + if (bottom > bounds_y2) + bottom = bounds_y2; + + SLIM_GL_PUSHRECT(); + + float colorRed; + float colorGreen; + float colorBlue; + float colorAlpha; + + if (pass == 0) + { + colorRed = 1.0; + colorGreen = 0.25; + colorBlue = 0.25; + colorAlpha = 1.0; + } + else + { + // look up the map's color at this grid point + float rgb[3]; + double value = values[x + y * xsize]; + + background_map->ColorForValue(value, rgb); + + colorRed = rgb[0]; + colorGreen = rgb[1]; + colorBlue = rgb[2]; + colorAlpha = 1.0; + } + + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + } + } + } + + // Draw any leftovers + SLIM_GL_FINISH(); + } + } +} + +void QtSLiMIndividualsWidget::glDrawSpatialBackgroundInBoundsForSubpopulation(QRect bounds, Subpopulation * subpop, int /* dimensionality */) +{ + auto backgroundIter = subviewSettings.find(subpop->subpopulation_id_); + PopulationViewSettings background; + SpatialMap *background_map = nullptr; + + if (backgroundIter == subviewSettings.end()) + { + // The user has not made a choice, so choose a temporary default. We don't want this choice to "stick", + // so that we can, e.g., begin as black and then change to a spatial map if one is defined. + chooseDefaultBackgroundSettingsForSubpopulation(&background, &background_map, subpop); + } + else + { + // The user has made a choice; verify that it is acceptable, and then use it. + background = backgroundIter->second; + + if (background.backgroundType == 3) + { + SpatialMapMap &spatial_maps = subpop->spatial_maps_; + auto map_iter = spatial_maps.find(background.spatialMapName); + + if (map_iter != spatial_maps.end()) + { + background_map = map_iter->second; + + // if the user somehow managed to choose a map that is not of an acceptable dimensionality, reject it here + if ((background_map->spatiality_string_ != "x") && (background_map->spatiality_string_ != "y") && (background_map->spatiality_string_ != "xy")) + background_map = nullptr; + } + } + + // if we're supposed to use a background map but we couldn't find it, or it's unacceptable, revert to black + if ((background.backgroundType == 3) && !background_map) + background.backgroundType = 0; + } + + if ((background.backgroundType == 3) && background_map) + { + _glDrawBackgroundSpatialMap(background_map, bounds, subpop, background.showGridPoints); + } + else + { + // No background map, so just clear to the preferred background color + int backgroundColor = background.backgroundType; + + if (backgroundColor == 0) + glColor3f(0.0, 0.0, 0.0); + else if (backgroundColor == 1) + glColor3f(0.3f, 0.3f, 0.3f); + else if (backgroundColor == 2) + glColor3f(1.0, 1.0, 1.0); + else + glColor3f(0.0, 0.0, 0.0); + + glRecti(bounds.x(), bounds.y(), bounds.x() + bounds.width(), bounds.y() + bounds.height()); + } +} + +void QtSLiMIndividualsWidget::glDrawSpatialIndividualsFromSubpopulationInArea(Subpopulation *subpop, QRect bounds, int dimensionality, float *forceColor) +{ + QtSLiMWindow *controller = dynamic_cast(window()); + double scalingFactor = 0.8; // used to be controller->fitnessColorScale; + slim_popsize_t subpopSize = subpop->parent_subpop_size_; + double bounds_x0 = subpop->bounds_x0_, bounds_x1 = subpop->bounds_x1_; + double bounds_y0 = subpop->bounds_y0_, bounds_y1 = subpop->bounds_y1_; + double bounds_x_size = bounds_x1 - bounds_x0, bounds_y_size = bounds_y1 - bounds_y0; + + QRect individualArea(bounds.x(), bounds.y(), bounds.width() - 1, bounds.height() - 1); + + int individualArrayIndex; + + // Set up to draw rects + SLIM_GL_PREPARE(); + + // First we outline all individuals + if (dimensionality == 1) + srandom(static_cast(controller->community->Tick())); + + for (individualArrayIndex = 0; individualArrayIndex < subpopSize; ++individualArrayIndex) + { + // Figure out the rect to draw in; note we now use individualArrayIndex here, because the hit-testing code doesn't have an easy way to calculate the displayed individual index... + Individual &individual = *subpop->parent_individuals_[static_cast(individualArrayIndex)]; + float position_x, position_y; + + if (dimensionality == 1) + { + position_x = static_cast((individual.spatial_x_ - bounds_x0) / bounds_x_size); + position_y = static_cast(random() / static_cast(INT32_MAX)); + + if ((position_x < 0.0f) || (position_x > 1.0f)) // skip points that are out of bounds + continue; + } + else + { + position_x = static_cast((individual.spatial_x_ - bounds_x0) / bounds_x_size); + position_y = static_cast((individual.spatial_y_ - bounds_y0) / bounds_y_size); + + if ((position_x < 0.0f) || (position_x > 1.0f) || (position_y < 0.0f) || (position_y > 1.0f)) // skip points that are out of bounds + continue; + } + + float centerX = static_cast(individualArea.x() + round(position_x * individualArea.width()) + 0.5f); + float centerY = static_cast(individualArea.y() + individualArea.height() - round(position_y * individualArea.height()) + 0.5f); + + float left = centerX - 2.5f; + float top = centerY - 2.5f; + float right = centerX + 2.5f; + float bottom = centerY + 2.5f; + + if (left < individualArea.x()) left = static_cast(individualArea.x()); + if (top < individualArea.y()) top = static_cast(individualArea.y()); + if (right > individualArea.x() + individualArea.width() + 1) right = static_cast(individualArea.x() + individualArea.width() + 1); + if (bottom > individualArea.y() + individualArea.height() + 1) bottom = static_cast(individualArea.y() + individualArea.height() + 1); + + float colorRed = 0.25; + float colorGreen = 0.25; + float colorBlue = 0.25; + float colorAlpha = 1.0; + + SLIM_GL_PUSHRECT(); + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + } + + // Then we draw all individuals + if (dimensionality == 1) + srandom(static_cast(controller->community->Tick())); + + for (individualArrayIndex = 0; individualArrayIndex < subpopSize; ++individualArrayIndex) + { + // Figure out the rect to draw in; note we now use individualArrayIndex here, because the hit-testing code doesn't have an easy way to calculate the displayed individual index... + Individual &individual = *subpop->parent_individuals_[static_cast(individualArrayIndex)]; + float position_x, position_y; + + if (dimensionality == 1) + { + position_x = static_cast((individual.spatial_x_ - bounds_x0) / bounds_x_size); + position_y = static_cast(random() / static_cast(INT32_MAX)); + + if ((position_x < 0.0f) || (position_x > 1.0f)) // skip points that are out of bounds + continue; + } + else + { + position_x = static_cast((individual.spatial_x_ - bounds_x0) / bounds_x_size); + position_y = static_cast((individual.spatial_y_ - bounds_y0) / bounds_y_size); + + if ((position_x < 0.0f) || (position_x > 1.0f) || (position_y < 0.0f) || (position_y > 1.0f)) // skip points that are out of bounds + continue; + } + + float centerX = static_cast(individualArea.x() + round(position_x * individualArea.width()) + 0.5f); + float centerY = static_cast(individualArea.y() + individualArea.height() - round(position_y * individualArea.height()) + 0.5f); + float left = centerX - 1.5f; + float top = centerY - 1.5f; + float right = centerX + 1.5f; + float bottom = centerY + 1.5f; + + // clipping deliberately not done here; because individual rects are 3x3, they will fall at most one pixel + // outside our drawing area, and thus the flaw will be covered by the view frame when it overdraws + + SLIM_GL_PUSHRECT(); + + // dark gray default, for a fitness of NaN; should never happen + float colorRed = 0.3f, colorGreen = 0.3f, colorBlue = 0.3f, colorAlpha = 1.0; + + if (Individual::s_any_individual_color_set_ && individual.color_set_) + { + colorRed = individual.colorR_ / 255.0F; + colorGreen = individual.colorG_ / 255.0F; + colorBlue = individual.colorB_ / 255.0F; + } + else if (forceColor) + { + // forceColor is used to make each species draw with a distinctive color in multispecies models in unified display mode + colorRed = forceColor[0]; + colorGreen = forceColor[1]; + colorBlue = forceColor[2]; + } + else + { + // use individual trait values to determine color; we used fitness values cached in UpdateFitness, so we don't have to call out to mutationEffect() callbacks + // we use cached_unscaled_fitness_ so individual fitness, unscaled by subpopulation fitness, is used for coloring + double fitness = individual.cached_unscaled_fitness_; + + if (!std::isnan(fitness)) + RGBForFitness(fitness, &colorRed, &colorGreen, &colorBlue, scalingFactor); + } + + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS(); + } + + // Draw any leftovers + SLIM_GL_FINISH(); +} + +#endif + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/QtSLiM/QtSLiMIndividualsWidget_QT.cpp b/QtSLiM/QtSLiMIndividualsWidget_QT.cpp new file mode 100644 index 00000000..ee211105 --- /dev/null +++ b/QtSLiM/QtSLiMIndividualsWidget_QT.cpp @@ -0,0 +1,626 @@ +// +// QtSLiMIndividualsWidget_QT.cpp +// SLiM +// +// Created by Ben Haller on 8/25/2024. +// Copyright (c) 2024 Philipp Messer. All rights reserved. +// A product of the Messer Lab, http://messerlab.org/slim/ +// + +// This file is part of SLiM. +// +// SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +// +// SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with SLiM. If not, see . + + +#include "QtSLiMIndividualsWidget.h" +#include "QtSLiMWindow.h" +#include "QtSLiMExtras.h" +#include "QtSLiMOpenGL_Emulation.h" + +#include + +#include +#include +#include +#include + + +// +// Qt-based drawing; maintain this in parallel with the OpenGL-based drawing! +// + +void QtSLiMIndividualsWidget::qtDrawViewFrameInBounds(QRect bounds, QPainter &painter) +{ + bool inDarkMode = QtSLiMInDarkMode(); + + if (inDarkMode) + QtSLiMFrameRect(bounds, QtSLiMColorWithWhite(0.067, 1.0), painter); + else + QtSLiMFrameRect(bounds, QtSLiMColorWithWhite(0.77, 1.0), painter); +} + +void QtSLiMIndividualsWidget::qtDrawIndividualsFromSubpopulationInArea(Subpopulation *subpop, QRect bounds, int squareSize, QPainter &painter) +{ + // + // NOTE this code is parallel to the code in canDisplayIndividualsFromSubpopulation:inArea: and should be maintained in parallel + // + + //QtSLiMWindow *controller = dynamic_cast(window()); + double scalingFactor = 0.8; // used to be controller->fitnessColorScale; + slim_popsize_t subpopSize = subpop->parent_subpop_size_; + int viewColumns = 0, viewRows = 0; + + // our square size is given from above (a consensus based on squareSizeForSubpopulationInArea(); calculate metrics from it + viewColumns = static_cast(floor((bounds.width() - 3) / squareSize)); + viewRows = static_cast(floor((bounds.height() - 3) / squareSize)); + + if (viewColumns * viewRows < subpopSize) + squareSize = 1; + + if (squareSize > 1) + { + int squareSpacing = 0; + + // Convert square area to space between squares if possible + if (squareSize > 2) + { + --squareSize; + ++squareSpacing; + } + if (squareSize > 5) + { + --squareSize; + ++squareSpacing; + } + + double excessSpaceX = bounds.width() - ((squareSize + squareSpacing) * viewColumns - squareSpacing); + double excessSpaceY = bounds.height() - ((squareSize + squareSpacing) * viewRows - squareSpacing); + int offsetX = static_cast(floor(excessSpaceX / 2.0)); + int offsetY = static_cast(floor(excessSpaceY / 2.0)); + + // If we have an empty row at the bottom, then we can use the same value for offsetY as for offsetX, for symmetry + if ((subpopSize - 1) / viewColumns < viewRows - 1) + offsetY = offsetX; + + QRect individualArea(bounds.left() + offsetX, bounds.top() + offsetY, bounds.width() - offsetX, bounds.height() - offsetY); + + int individualArrayIndex; + + // Set up to draw rects + SLIM_GL_PREPARE(); + + for (individualArrayIndex = 0; individualArrayIndex < subpopSize; ++individualArrayIndex) + { + // Figure out the rect to draw in; note we now use individualArrayIndex here, because the hit-testing code doesn't have an easy way to calculate the displayed individual index... + float left = static_cast(individualArea.left() + (individualArrayIndex % viewColumns) * (squareSize + squareSpacing)); + float top = static_cast(individualArea.top() + (individualArrayIndex / viewColumns) * (squareSize + squareSpacing)); + float right = left + squareSize; + float bottom = top + squareSize; + + SLIM_GL_PUSHRECT(); + + // dark gray default, for a fitness of NaN; should never happen + float colorRed = 0.3f, colorGreen = 0.3f, colorBlue = 0.3f, colorAlpha = 1.0; + Individual &individual = *subpop->parent_individuals_[static_cast(individualArrayIndex)]; + + if (Individual::s_any_individual_color_set_ && individual.color_set_) + { + colorRed = individual.colorR_ / 255.0F; + colorGreen = individual.colorG_ / 255.0F; + colorBlue = individual.colorB_ / 255.0F; + } + else + { + // use individual trait values to determine color; we use fitness values cached in UpdateFitness, so we don't have to call out to mutationEffect() callbacks + // we use cached_unscaled_fitness_ so individual fitness, unscaled by subpopulation fitness, is used for coloring + double fitness = individual.cached_unscaled_fitness_; + + if (!std::isnan(fitness)) + RGBForFitness(fitness, &colorRed, &colorGreen, &colorBlue, scalingFactor); + } + + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS_NORECT(); + } + + // Draw any leftovers + SLIM_GL_FINISH(); + } + else + { + // This is what we do if we cannot display a subpopulation because there are too many individuals in it to display + QRect insetBounds = bounds.adjusted(1, 1, -1, -1); + + painter.fillRect(insetBounds, QtSLiMColorWithRGB(0.9, 0.9, 1.0, 1.0)); + } +} + +void QtSLiMIndividualsWidget::_qtDrawBackgroundSpatialMap(SpatialMap *background_map, QRect bounds, Subpopulation *subpop, bool showGridPoints, QPainter &painter) +{ + // We have a spatial map with a color map, so use it to draw the background + int bounds_x1 = bounds.x(); + int bounds_y1 = bounds.y(); + int bounds_x2 = bounds.x() + bounds.width(); + int bounds_y2 = bounds.y() + bounds.height(); + + { + // Set up to draw rects + SLIM_GL_PREPARE(); + + if (background_map->spatiality_ == 1) + { + // This is the spatiality "x" and "y" cases; they are the only 1D spatiality values for which SLiMgui will draw + // In the 1D case we can't cache a display buffer, since we don't know what aspect ratio to use, so we just + // draw rects. Whether those rects are horizontal or vertical will depend on the spatiality of the map. Most + // of the code is identical, though, because of the way we handle dimensions, so we share the two cases here. + bool spatiality_is_x = (background_map->spatiality_string_ == "x"); + int64_t xsize = background_map->grid_size_[0]; + double *values = background_map->values_; + + if (background_map->interpolate_) + { + // Interpolation, so we need to draw every line individually + int min_coord = (spatiality_is_x ? bounds_x1 : bounds_y1); + int max_coord = (spatiality_is_x ? bounds_x2 : bounds_y2); + + for (int xc = min_coord; xc < max_coord; ++xc) + { + double x_fraction = (xc + 0.5 - min_coord) / (max_coord - min_coord); // values evaluated at pixel centers + double x_map = x_fraction * (xsize - 1); + int x1_map = static_cast(floor(x_map)); + int x2_map = static_cast(ceil(x_map)); + double fraction_x2 = x_map - x1_map; + double fraction_x1 = 1.0 - fraction_x2; + double value_x1 = values[x1_map] * fraction_x1; + double value_x2 = values[x2_map] * fraction_x2; + double value = value_x1 + value_x2; + + float left, right, top, bottom; + + if (spatiality_is_x) + { + left = xc; + right = xc + 1; + top = bounds_y1; + bottom = bounds_y2; + } + else + { + top = (max_coord - 1) - xc + min_coord; // flip for y, to use Cartesian coordinates + bottom = top + 1; + left = bounds_x1; + right = bounds_x2; + } + + float rgb[3]; + background_map->ColorForValue(value, rgb); + + float colorRed = rgb[0]; + float colorGreen = rgb[1]; + float colorBlue = rgb[2]; + float colorAlpha = 1.0; + + SLIM_GL_PUSHRECT(); + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS_NORECT(); + } + } + else + { + // No interpolation, so we can draw whole grid blocks + for (int xc = 0; xc < xsize; xc++) + { + double value = (spatiality_is_x ? values[xc] : values[(xsize - 1) - xc]); // flip for y, to use Cartesian coordinates + float left, right, top, bottom; + + if (spatiality_is_x) + { + left = qRound(((xc - 0.5) / (xsize - 1)) * bounds.width() + bounds.x()); + right = qRound(((xc + 0.5) / (xsize - 1)) * bounds.width() + bounds.x()); + + if (left < bounds_x1) left = bounds_x1; + if (right > bounds_x2) right = bounds_x2; + + top = bounds_y1; + bottom = bounds_y2; + } + else + { + top = qRound(((xc - 0.5) / (xsize - 1)) * bounds.height() + bounds.y()); + bottom = qRound(((xc + 0.5) / (xsize - 1)) * bounds.height() + bounds.y()); + + if (top < bounds_y1) top = bounds_y1; + if (bottom > bounds_y2) bottom = bounds_y2; + + left = bounds_x1; + right = bounds_x2; + } + + float rgb[3]; + + background_map->ColorForValue(value, rgb); + + float colorRed = rgb[0]; + float colorGreen = rgb[1]; + float colorBlue = rgb[2]; + float colorAlpha = 1.0; + + SLIM_GL_PUSHRECT(); + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS_NORECT(); + } + } + } + else // if (background_map->spatiality_ == 2) + { + // This is the spatiality "xy" case; it is the only 2D spatiality for which SLiMgui will draw + + // First, cache the display buffer if needed. If this succeeds, we'll use it. + // It should always succeed, so the tile-drawing code below is dead code, kept for parallelism with the 1D case. + cacheDisplayBufferForMapForSubpopulation(background_map, subpop, /* flipped */ true); + + const uint8_t *display_buf = background_map->display_buffer_; + + if (display_buf) + { + // Use a cached display buffer to draw. + int buf_width = background_map->buffer_width_; + int buf_height = background_map->buffer_height_; + QImage bufferImage(display_buf, buf_width, buf_height, buf_width * 3, QImage::Format_RGB888); + + painter.drawImage(bounds, bufferImage); + } + } + + // Draw any leftovers + SLIM_GL_FINISH(); + } + + if (showGridPoints) + { + // BCH 9/29/2023 new feature: draw boxes showing where the grid nodes are, since that is rather confusing! + float margin_outer = 5.5f; + float margin_inner = 3.5f; + float spacing = 10.0f; + int64_t xsize = background_map->grid_size_[0]; + int64_t ysize = background_map->grid_size_[1]; + double *values = background_map->values_; + + // require that there is sufficient space that we're not just showing a packed grid of squares + // downsize to small and smaller depictions as needed + if (((xsize - 1) * (margin_outer * 2.0 + spacing) > bounds_x2) || ((ysize - 1) * (margin_outer * 2.0 + spacing) > bounds_y2)) + { + margin_outer = 4.5f; + margin_inner = 2.5f; + spacing = 8.0; + } + if (((xsize - 1) * (margin_outer * 2.0 + spacing) > bounds_x2) || ((ysize - 1) * (margin_outer * 2.0 + spacing) > bounds_y2)) + { + margin_outer = 3.5f; + margin_inner = 1.5f; + spacing = 6.0; + } + if (((xsize - 1) * (margin_outer * 2.0 + spacing) > bounds_x2) || ((ysize - 1) * (margin_outer * 2.0 + spacing) > bounds_y2)) + { + margin_outer = 1.0f; + margin_inner = 0.0f; + spacing = 2.0; + } + + if (((xsize - 1) * (margin_outer * 2.0 + spacing) <= bounds_x2) && ((ysize - 1) * (margin_outer * 2.0 + spacing) <= bounds_y2)) + { + // Set up to draw rects + SLIM_GL_PREPARE(); + + // first pass we draw squares to make outlines, second pass we draw the interiors in color + for (int pass = 0; pass <= 1; ++pass) + { + const float margin = ((pass == 0) ? margin_outer : margin_inner); + + if (margin == 0.0) + continue; + + for (int x = 0; x < xsize; ++x) + { + for (int y = 0; y < ysize; ++y) + { + float position_x = x / (float)(xsize - 1); // 0 to 1 + float position_y = y / (float)(ysize - 1); // 0 to 1 + + float centerX = (float)(bounds_x1 + round(position_x * bounds.width())); + float centerY = (float)(bounds_y1 + bounds.height() - round(position_y * bounds.height())); + float left = centerX - margin; + float top = centerY - margin; + float right = centerX + margin; + float bottom = centerY + margin; + + if (left < bounds_x1) + left = bounds_x1; + if (top < bounds_y1) + top = bounds_y1; + if (right > bounds_x2) + right = bounds_x2; + if (bottom > bounds_y2) + bottom = bounds_y2; + + SLIM_GL_PUSHRECT(); + + float colorRed; + float colorGreen; + float colorBlue; + float colorAlpha; + + if (pass == 0) + { + colorRed = 1.0; + colorGreen = 0.25; + colorBlue = 0.25; + colorAlpha = 1.0; + } + else + { + // look up the map's color at this grid point + float rgb[3]; + double value = values[x + y * xsize]; + + background_map->ColorForValue(value, rgb); + + colorRed = rgb[0]; + colorGreen = rgb[1]; + colorBlue = rgb[2]; + colorAlpha = 1.0; + } + + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS_NORECT(); + } + } + } + + // Draw any leftovers + SLIM_GL_FINISH(); + } + } +} + +void QtSLiMIndividualsWidget::qtDrawSpatialBackgroundInBoundsForSubpopulation(QRect bounds, Subpopulation * subpop, int /* dimensionality */, QPainter &painter) +{ + auto backgroundIter = subviewSettings.find(subpop->subpopulation_id_); + PopulationViewSettings background; + SpatialMap *background_map = nullptr; + + if (backgroundIter == subviewSettings.end()) + { + // The user has not made a choice, so choose a temporary default. We don't want this choice to "stick", + // so that we can, e.g., begin as black and then change to a spatial map if one is defined. + chooseDefaultBackgroundSettingsForSubpopulation(&background, &background_map, subpop); + } + else + { + // The user has made a choice; verify that it is acceptable, and then use it. + background = backgroundIter->second; + + if (background.backgroundType == 3) + { + SpatialMapMap &spatial_maps = subpop->spatial_maps_; + auto map_iter = spatial_maps.find(background.spatialMapName); + + if (map_iter != spatial_maps.end()) + { + background_map = map_iter->second; + + // if the user somehow managed to choose a map that is not of an acceptable dimensionality, reject it here + if ((background_map->spatiality_string_ != "x") && (background_map->spatiality_string_ != "y") && (background_map->spatiality_string_ != "xy")) + background_map = nullptr; + } + } + + // if we're supposed to use a background map but we couldn't find it, or it's unacceptable, revert to black + if ((background.backgroundType == 3) && !background_map) + background.backgroundType = 0; + } + + if ((background.backgroundType == 3) && background_map) + { + _qtDrawBackgroundSpatialMap(background_map, bounds, subpop, background.showGridPoints, painter); + } + else + { + // No background map, so just clear to the preferred background color + int backgroundColor = background.backgroundType; + + if (backgroundColor == 0) + painter.fillRect(bounds, Qt::black); + else if (backgroundColor == 1) + painter.fillRect(bounds, QtSLiMColorWithWhite(0.3, 1.0)); + else if (backgroundColor == 2) + painter.fillRect(bounds, Qt::white); + else + painter.fillRect(bounds, Qt::black); + } +} + +void QtSLiMIndividualsWidget::qtDrawSpatialIndividualsFromSubpopulationInArea(Subpopulation *subpop, QRect bounds, int dimensionality, float *forceColor, QPainter &painter) +{ + QtSLiMWindow *controller = dynamic_cast(window()); + double scalingFactor = 0.8; // used to be controller->fitnessColorScale; + slim_popsize_t subpopSize = subpop->parent_subpop_size_; + double bounds_x0 = subpop->bounds_x0_, bounds_x1 = subpop->bounds_x1_; + double bounds_y0 = subpop->bounds_y0_, bounds_y1 = subpop->bounds_y1_; + double bounds_x_size = bounds_x1 - bounds_x0, bounds_y_size = bounds_y1 - bounds_y0; + + QRect individualArea(bounds.x(), bounds.y(), bounds.width() - 1, bounds.height() - 1); + + int individualArrayIndex; + + // Set up to draw rects + SLIM_GL_PREPARE(); + + // First we outline all individuals + if (dimensionality == 1) + srandom(static_cast(controller->community->Tick())); + + for (individualArrayIndex = 0; individualArrayIndex < subpopSize; ++individualArrayIndex) + { + // Figure out the rect to draw in; note we now use individualArrayIndex here, because the hit-testing code doesn't have an easy way to calculate the displayed individual index... + Individual &individual = *subpop->parent_individuals_[static_cast(individualArrayIndex)]; + float position_x, position_y; + + if (dimensionality == 1) + { + position_x = static_cast((individual.spatial_x_ - bounds_x0) / bounds_x_size); + position_y = static_cast(random() / static_cast(INT32_MAX)); + + if ((position_x < 0.0f) || (position_x > 1.0f)) // skip points that are out of bounds + continue; + } + else + { + position_x = static_cast((individual.spatial_x_ - bounds_x0) / bounds_x_size); + position_y = static_cast((individual.spatial_y_ - bounds_y0) / bounds_y_size); + + if ((position_x < 0.0f) || (position_x > 1.0f) || (position_y < 0.0f) || (position_y > 1.0f)) // skip points that are out of bounds + continue; + } + + float centerX = static_cast(individualArea.x() + round(position_x * individualArea.width()) + 0.5f); + float centerY = static_cast(individualArea.y() + individualArea.height() - round(position_y * individualArea.height()) + 0.5f); + + float left = centerX - 2.5f; + float top = centerY - 2.5f; + float right = centerX + 2.5f; + float bottom = centerY + 2.5f; + + if (left < individualArea.x()) left = static_cast(individualArea.x()); + if (top < individualArea.y()) top = static_cast(individualArea.y()); + if (right > individualArea.x() + individualArea.width() + 1) right = static_cast(individualArea.x() + individualArea.width() + 1); + if (bottom > individualArea.y() + individualArea.height() + 1) bottom = static_cast(individualArea.y() + individualArea.height() + 1); + + float colorRed = 0.25; + float colorGreen = 0.25; + float colorBlue = 0.25; + float colorAlpha = 1.0; + + SLIM_GL_PUSHRECT(); + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS_NORECT(); + } + + // Then we draw all individuals + if (dimensionality == 1) + srandom(static_cast(controller->community->Tick())); + + for (individualArrayIndex = 0; individualArrayIndex < subpopSize; ++individualArrayIndex) + { + // Figure out the rect to draw in; note we now use individualArrayIndex here, because the hit-testing code doesn't have an easy way to calculate the displayed individual index... + Individual &individual = *subpop->parent_individuals_[static_cast(individualArrayIndex)]; + float position_x, position_y; + + if (dimensionality == 1) + { + position_x = static_cast((individual.spatial_x_ - bounds_x0) / bounds_x_size); + position_y = static_cast(random() / static_cast(INT32_MAX)); + + if ((position_x < 0.0f) || (position_x > 1.0f)) // skip points that are out of bounds + continue; + } + else + { + position_x = static_cast((individual.spatial_x_ - bounds_x0) / bounds_x_size); + position_y = static_cast((individual.spatial_y_ - bounds_y0) / bounds_y_size); + + if ((position_x < 0.0f) || (position_x > 1.0f) || (position_y < 0.0f) || (position_y > 1.0f)) // skip points that are out of bounds + continue; + } + + float centerX = static_cast(individualArea.x() + round(position_x * individualArea.width()) + 0.5f); + float centerY = static_cast(individualArea.y() + individualArea.height() - round(position_y * individualArea.height()) + 0.5f); + float left = centerX - 1.5f; + float top = centerY - 1.5f; + float right = centerX + 1.5f; + float bottom = centerY + 1.5f; + + // clipping deliberately not done here; because individual rects are 3x3, they will fall at most one pixel + // outside our drawing area, and thus the flaw will be covered by the view frame when it overdraws + + SLIM_GL_PUSHRECT(); + + // dark gray default, for a fitness of NaN; should never happen + float colorRed = 0.3f, colorGreen = 0.3f, colorBlue = 0.3f, colorAlpha = 1.0; + + if (Individual::s_any_individual_color_set_ && individual.color_set_) + { + colorRed = individual.colorR_ / 255.0F; + colorGreen = individual.colorG_ / 255.0F; + colorBlue = individual.colorB_ / 255.0F; + } + else if (forceColor) + { + // forceColor is used to make each species draw with a distinctive color in multispecies models in unified display mode + colorRed = forceColor[0]; + colorGreen = forceColor[1]; + colorBlue = forceColor[2]; + } + else + { + // use individual trait values to determine color; we used fitness values cached in UpdateFitness, so we don't have to call out to mutationEffect() callbacks + // we use cached_unscaled_fitness_ so individual fitness, unscaled by subpopulation fitness, is used for coloring + double fitness = individual.cached_unscaled_fitness_; + + if (!std::isnan(fitness)) + RGBForFitness(fitness, &colorRed, &colorGreen, &colorBlue, scalingFactor); + } + + SLIM_GL_PUSHRECT_COLORS(); + SLIM_GL_CHECKBUFFERS_NORECT(); + } + + // Draw any leftovers + SLIM_GL_FINISH(); +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/QtSLiM/QtSLiMOpenGL.cpp b/QtSLiM/QtSLiMOpenGL.cpp new file mode 100644 index 00000000..57a38bd9 --- /dev/null +++ b/QtSLiM/QtSLiMOpenGL.cpp @@ -0,0 +1,72 @@ +// +// QtSLiMOpenGL.cpp +// SLiM +// +// Created by Ben Haller on 8/25/2024. +// Copyright (c) 2024 Philipp Messer. All rights reserved. +// A product of the Messer Lab, http://messerlab.org/slim/ +// + +// This file is part of SLiM. +// +// SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +// +// SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with SLiM. If not, see . + + +#include "QtSLiMOpenGL.h" + +#include + + +float *glArrayVertices = nullptr; +float *glArrayColors = nullptr; + +void QtSLiM_AllocateGLBuffers(void) +{ + if (!glArrayVertices) + glArrayVertices = static_cast(malloc(kMaxVertices * 2 * sizeof(float))); // 2 floats per vertex, kMaxVertices vertices + + if (!glArrayColors) + glArrayColors = static_cast(malloc(kMaxVertices * 4 * sizeof(float))); // 4 floats per color, kMaxVertices colors +} + +void QtSLiM_FreeGLBuffers(void) +{ + if (glArrayVertices) + { + free(glArrayVertices); + glArrayVertices = nullptr; + } + + if (glArrayColors) + { + free(glArrayColors); + glArrayColors = nullptr; + } +} + + + + + + + + + + + + + + + + + + + + + diff --git a/QtSLiM/QtSLiMOpenGL.h b/QtSLiM/QtSLiMOpenGL.h new file mode 100644 index 00000000..7d84015e --- /dev/null +++ b/QtSLiM/QtSLiMOpenGL.h @@ -0,0 +1,116 @@ +// +// QtSLiMOpenGL.h +// SLiM +// +// Created by Ben Haller on 8/25/2024. +// Copyright (c) 2024 Philipp Messer. All rights reserved. +// A product of the Messer Lab, http://messerlab.org/slim/ +// + +// This file is part of SLiM. +// +// SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +// +// SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with SLiM. If not, see . + +#ifndef QTSLIMOPENGL_H +#define QTSLIMOPENGL_H + + +/* + * This header defines utility macros and functions for drawing with OpenGL. + * This should be included only locally within OpenGL-specific rendering code. + */ + + +// OpenGL buffers for rendering; shared across all rendering code +// These buffers are allocated by QtSLiMAppDelegate at launch +#define kMaxGLRects 4000 // 4000 rects +#define kMaxVertices (kMaxGLRects * 4) // 4 vertices each + +extern float *glArrayVertices; +extern float *glArrayColors; + +extern void QtSLiM_AllocateGLBuffers(void); +extern void QtSLiM_FreeGLBuffers(void); + + +#define SLIM_GL_PREPARE() \ + int displayListIndex = 0; \ + float *vertices = glArrayVertices, *colors = glArrayColors; \ + \ + glEnableClientState(GL_VERTEX_ARRAY); \ + glVertexPointer(2, GL_FLOAT, 0, glArrayVertices); \ + glEnableClientState(GL_COLOR_ARRAY); \ + glColorPointer(4, GL_FLOAT, 0, glArrayColors); + +#define SLIM_GL_DEFCOORDS(rect) \ + float left = static_cast(rect.left()); \ + float top = static_cast(rect.top()); \ + float right = left + static_cast(rect.width()); \ + float bottom = top + static_cast(rect.height()); + +#define SLIM_GL_PUSHRECT() \ + *(vertices++) = left; \ + *(vertices++) = top; \ + *(vertices++) = left; \ + *(vertices++) = bottom; \ + *(vertices++) = right; \ + *(vertices++) = bottom; \ + *(vertices++) = right; \ + *(vertices++) = top; + +#define SLIM_GL_PUSHRECT_COLORS() \ + for (int j = 0; j < 4; ++j) \ + { \ + *(colors++) = colorRed; \ + *(colors++) = colorGreen; \ + *(colors++) = colorBlue; \ + *(colors++) = colorAlpha; \ + } + +#define SLIM_GL_CHECKBUFFERS() \ + displayListIndex++; \ + \ + if (displayListIndex == kMaxGLRects) \ + { \ + glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); \ + \ + vertices = glArrayVertices; \ + colors = glArrayColors; \ + displayListIndex = 0; \ + } + +#define SLIM_GL_FINISH() \ + if (displayListIndex) \ + glDrawArrays(GL_QUADS, 0, 4 * displayListIndex); \ + \ + glDisableClientState(GL_VERTEX_ARRAY); \ + glDisableClientState(GL_COLOR_ARRAY); + + +#endif // QTSLIMOPENGL_H + + + + + + + + + + + + + + + + + + + + diff --git a/QtSLiM/QtSLiMOpenGL_Emulation.h b/QtSLiM/QtSLiMOpenGL_Emulation.h new file mode 100644 index 00000000..2f3134b7 --- /dev/null +++ b/QtSLiM/QtSLiMOpenGL_Emulation.h @@ -0,0 +1,77 @@ +// +// QtSLiMOpenGL_Emulation.h +// SLiM +// +// Created by Ben Haller on 8/25/2024. +// Copyright (c) 2024 Philipp Messer. All rights reserved. +// A product of the Messer Lab, http://messerlab.org/slim/ +// + +// This file is part of SLiM. +// +// SLiM is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +// +// SLiM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with SLiM. If not, see . + +#ifndef QTSLIMOPENGL_EMULATION_H +#define QTSLIMOPENGL_EMULATION_H + + +/* + * This header defines utility macros and functions for drawing with OpenGL, but they are emulated with Qt. + * This should be included only locally within Qt-specific rendering code. See also QtSLiMOpenGL.h. + */ + + +#define SLIM_GL_PREPARE() + +#define SLIM_GL_DEFCOORDS(rect) \ + QRect &RECT_TO_DRAW = rect; + +#define SLIM_GL_PUSHRECT() + +#define SLIM_GL_PUSHRECT_COLORS() + +#define SLIM_GL_CHECKBUFFERS() \ + { \ + QColor COLOR_TO_DRAW; \ + COLOR_TO_DRAW.setRgbF(colorRed, colorGreen, colorBlue, colorAlpha); \ + painter.fillRect(RECT_TO_DRAW, COLOR_TO_DRAW); \ + } + +#define SLIM_GL_CHECKBUFFERS_NORECT() \ +{ \ + QColor COLOR_TO_DRAW; \ + COLOR_TO_DRAW.setRgbF(colorRed, colorGreen, colorBlue, colorAlpha); \ + QRect RECT_TO_DRAW(left, top, right-left, bottom-top); \ + painter.fillRect(RECT_TO_DRAW, COLOR_TO_DRAW); \ +} + +#define SLIM_GL_FINISH() + + +#endif // QTSLIMOPENGL_EMULATION_H + + + + + + + + + + + + + + + + + + + + diff --git a/QtSLiM/QtSLiMPreferences.cpp b/QtSLiM/QtSLiMPreferences.cpp index 545e9cfe..402ae7f0 100644 --- a/QtSLiM/QtSLiMPreferences.cpp +++ b/QtSLiM/QtSLiMPreferences.cpp @@ -35,6 +35,7 @@ static const char *QtSLiMAppStartupAction = "QtSLiMAppStartupAction"; static const char *QtSLiMForceDarkMode = "QtSLiMForceDarkMode"; static const char *QtSLiMForceFusionStyle = "QtSLiMForceFusionStyle"; +static const char *QtSLiMUseOpenGL = "QtSLiMUseOpenGL"; static const char *QtSLiMDisplayFontFamily = "QtSLiMDisplayFontFamily"; static const char *QtSLiMDisplayFontSize = "QtSLiMDisplayFontSize"; static const char *QtSLiMSyntaxHighlightScript = "QtSLiMSyntaxHighlightScript"; @@ -127,6 +128,17 @@ bool QtSLiMPreferencesNotifier::forceFusionStylePref(void) return settings.value(QtSLiMForceFusionStyle, QVariant(false)).toBool(); } +bool QtSLiMPreferencesNotifier::useOpenGLPref(void) +{ +#ifndef SLIM_NO_OPENGL + QSettings settings; + + return settings.value(QtSLiMUseOpenGL, QVariant(true)).toBool(); +#else + return false; +#endif +} + QFont QtSLiMPreferencesNotifier::displayFontPref(double *tabWidth) const { QFont &defaultFont = defaultDisplayFont(); @@ -287,6 +299,16 @@ void QtSLiMPreferencesNotifier::forceFusionStyleToggled() //emit forceFusionStylePrefChanged(); } +void QtSLiMPreferencesNotifier::useOpenGLToggled() +{ + QtSLiMPreferences &prefsUI = QtSLiMPreferences::instance(); + QSettings settings; + + settings.setValue(QtSLiMUseOpenGL, QVariant(prefsUI.ui->useOpenGL->isChecked())); + + emit useOpenGLPrefChanged(); +} + void QtSLiMPreferencesNotifier::fontChanged(const QFont &newFont) { QString fontFamily = newFont.family(); @@ -460,10 +482,23 @@ QtSLiMPreferences::QtSLiMPreferences(QWidget *p_parent) : QDialog(p_parent), ui( connect(ui->resetSuppressedButton, &QPushButton::clicked, notifier, &QtSLiMPreferencesNotifier::resetSuppressedClicked); // handle the user interface display prefs, which are hidden and disconnected on macOS + ui->useOpenGL->setChecked(notifier->useOpenGLPref()); + + connect(ui->useOpenGL, &QCheckBox::toggled, notifier, &QtSLiMPreferencesNotifier::useOpenGLToggled); + #ifdef __APPLE__ - ui->uiAppearanceGroup->setHidden(true); - ui->verticalSpacer_uiAppearance->changeSize(0, 0); - ui->verticalSpacer_uiAppearance->invalidate(); + // This old code hid the UI prefs entirely on macOS +// ui->uiAppearanceGroup->setHidden(true); +// ui->verticalSpacer_uiAppearance->changeSize(0, 0); +// ui->verticalSpacer_uiAppearance->invalidate(); +// ui->verticalLayout->invalidate(); + + // This new code leaves the "Use OpenGL for speed" checkbox visible and hides the rest + ui->requireRelaunchLabel->setHidden(true); + ui->forceDarkMode->setHidden(true); + ui->forceFusionStyle->setHidden(true); + ui->verticalSpacer_requireRelaunch->changeSize(0, 0); + ui->verticalSpacer_requireRelaunch->invalidate(); ui->verticalLayout->invalidate(); #else ui->forceDarkMode->setChecked(notifier->forceDarkModePref()); diff --git a/QtSLiM/QtSLiMPreferences.h b/QtSLiM/QtSLiMPreferences.h index 3d4ff753..724d6bb4 100644 --- a/QtSLiM/QtSLiMPreferences.h +++ b/QtSLiM/QtSLiMPreferences.h @@ -37,6 +37,7 @@ class QtSLiMPreferencesNotifier : public QObject int appStartupPref(void) const; // 0 == do nothing, 1 == create a new window, 2 == run an open panel bool forceDarkModePref(void); bool forceFusionStylePref(void); + bool useOpenGLPref(void); QFont displayFontPref(double *tabWidth = nullptr) const; bool scriptSyntaxHighlightPref(void) const; bool outputSyntaxHighlightPref(void) const; @@ -53,6 +54,7 @@ class QtSLiMPreferencesNotifier : public QObject signals: // Get notified when a pref value changes void appStartupPrefChanged(void); + void useOpenGLPrefChanged(void); void displayFontPrefChanged(void); void scriptSyntaxHighlightPrefChanged(void); void outputSyntaxHighlightPrefChanged(void); @@ -73,6 +75,7 @@ private slots: void startupRadioChanged(); void forceDarkModeToggled(); void forceFusionStyleToggled(); + void useOpenGLToggled(); void fontChanged(const QFont &font); void fontSizeChanged(int newSize); void syntaxHighlightScriptToggled(); diff --git a/QtSLiM/QtSLiMPreferences.ui b/QtSLiM/QtSLiMPreferences.ui index 4ac11e02..37f21a62 100644 --- a/QtSLiM/QtSLiMPreferences.ui +++ b/QtSLiM/QtSLiMPreferences.ui @@ -6,8 +6,8 @@ 0 0 - 282 - 714 + 303 + 747 @@ -125,7 +125,7 @@ User interface appearance: - + 6 @@ -142,9 +142,35 @@ 6 - + + + <html><head/><body><p>use OpenGL for fast display; uncheck this if you experience display problems with the Individuals and Chromosome views</p></body></html> + + + Use OpenGL for fast display + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 8 + + + + + + - <html><head/><body><p><span style=" font-size:11pt; font-style:italic;">Changes require a relaunch to take effect.</span></p></body></html> + <html><head/><body><p><span style=" font-size:11pt; font-style:italic;">Changes below require a relaunch to take effect.</span></p></body></html> diff --git a/VERSIONS b/VERSIONS index f536cd9b..2b0e83c9 100644 --- a/VERSIONS +++ b/VERSIONS @@ -30,6 +30,7 @@ development head (in the master branch): these recommendations are based on Qt6's recommended platforms; earlier platforms should use Qt5 for more details see https://doc.qt.io/qt-6/linux.html, https://doc.qt.io/qt-6/windows.html, https://doc.qt.io/qt-6/macos.html building with CMake will now default to Qt6 if CMake can find it, and fall back to Qt5 otherwise; should be automatic + add a "Use OpenGL for fast display" checkbox in the Preferences panel, making it possible to disable OpenGL when it doesn't work well (#462) version 4.2.2 (Eidos version 3.2.2): diff --git a/core/spatial_map.h b/core/spatial_map.h index 1a96795d..7449aef6 100644 --- a/core/spatial_map.h +++ b/core/spatial_map.h @@ -86,6 +86,7 @@ class SpatialMap : public EidosDictionaryRetained #if defined(SLIMGUI) uint8_t *display_buffer_ = nullptr; // OWNED POINTER: used by SLiMgui, contains RGB values for pixels in the PopulationView int buffer_width_, buffer_height_; // the size of the buffer, in pixels, each of which is 3 x sizeof(uint8_t) + bool buffer_flipped_; // true if flipped (for Qt display) false if not (for GL display) #endif SpatialMap(const SpatialMap&) = delete; // no copying