diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7bb438b08..43bd4115d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -397,6 +397,9 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
endif()
if(MMAPPER_IS_DEBUG)
+ # segfaults in ASAN initializer on 32-bit Ubuntu 18.04.1 with clang 6.0
+ # note: apparently -fsanitize=address can't be used with gnu libc 2.25+
+ # Ubuntu 18.04.1 has libc 2.27.
message(STATUS "Building with Clang address sanitizer")
add_compile_options(-fsanitize=address)
add_link_options(-fsanitize=address)
diff --git a/cmake/MakeAppX.cmake.in b/cmake/MakeAppX.cmake.in
index ae33f413e..1a4f9e57b 100644
--- a/cmake/MakeAppX.cmake.in
+++ b/cmake/MakeAppX.cmake.in
@@ -38,10 +38,14 @@ set(APPX_PDB_SOURCE_PATH "${CPACK_TEMPORARY_DIRECTORY}/${APPX_PDB_FILE_NAME}")
## Prepare Staging Directory
-# Flatten all subdirectories in the staging root for MakeAppx.
+# Flatten all subdirectories in the staging root for MakeAppx, EXCEPT Assets.
file(GLOB staging_items "${CPACK_TEMPORARY_DIRECTORY}/*")
foreach(item ${staging_items})
if(IS_DIRECTORY "${item}")
+ get_filename_component(item_name "${item}" NAME)
+ if(item_name STREQUAL "Assets")
+ continue()
+ endif()
message(STATUS "Flattening directory: ${item}")
file(GLOB subdir_contents "${item}/*")
foreach(subitem ${subdir_contents})
diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html
index ecdff4f04..4fdf58702 100644
--- a/docs/_layouts/default.html
+++ b/docs/_layouts/default.html
@@ -18,6 +18,7 @@
+ Skip to content
-
+
{{ content }}
diff --git a/docs/assets/css/style.css b/docs/assets/css/style.css
index ff3dcf09a..f5cdea367 100644
--- a/docs/assets/css/style.css
+++ b/docs/assets/css/style.css
@@ -1,6 +1,22 @@
/* Minimal MMapper Website Styles */
/* Basic Reset & Body */
+.skip-link {
+ position: absolute;
+ top: -40px;
+ left: 0;
+ background: #cc9933;
+ color: #1a1a1a;
+ padding: 8px;
+ z-index: 100;
+ transition: top 0.3s;
+}
+
+.skip-link:focus,
+.skip-link:focus-visible {
+ top: 0;
+}
+
body {
margin: 0;
padding: 0;
diff --git a/docs/assets/js/download-highlight.js b/docs/assets/js/download-highlight.js
index f0f13552f..a54c19eeb 100644
--- a/docs/assets/js/download-highlight.js
+++ b/docs/assets/js/download-highlight.js
@@ -1,7 +1,7 @@
document.addEventListener('DOMContentLoaded', function() {
let detectedArch = null;
let isChromeOS = false;
- const downloadLinks = document.querySelectorAll('.download-link');
+ const downloadLinks = document.querySelectorAll('.download-link, .platform-link');
// --- Architecture Detection (Best Effort) ---
if (navigator.userAgentData && navigator.userAgentData.architecture) {
@@ -23,17 +23,34 @@ document.addEventListener('DOMContentLoaded', function() {
}
// --- Highlight Download Link ---
+ function addRecommendation(link) {
+ link.classList.add('recommended-download');
+ const recommendation = document.createElement('span');
+ recommendation.textContent = ' Recommended';
+ recommendation.classList.add('recommendation-text');
+
+ // Place recommendation outside the link as per user preference
+ if (link.classList.contains('platform-link')) {
+ recommendation.classList.add('platform-recommendation');
+ }
+ link.parentNode.insertBefore(recommendation, link.nextSibling);
+ }
+
downloadLinks.forEach(link => {
const href = link.href.toLowerCase();
+ const platform = link.getAttribute('data-platform');
// Do not recommend Windows .exe installers
if (href.includes('.exe')) {
return;
}
- // Only recommend .deb for ChromeOS
- if (isChromeOS && (href.includes('appimage') || href.includes('flatpak'))) {
- return;
+ // For ChromeOS, only recommend the Web version
+ if (isChromeOS) {
+ if (platform === 'web') {
+ addRecommendation(link);
+ }
+ return; // Don't recommend anything else on ChromeOS
}
let linkArch = null;
@@ -44,11 +61,7 @@ document.addEventListener('DOMContentLoaded', function() {
}
if (detectedArch && linkArch === detectedArch) {
- link.classList.add('recommended-download');
- const recommendation = document.createElement('span');
- recommendation.textContent = ' Recommended';
- recommendation.classList.add('recommendation-text');
- link.parentNode.insertBefore(recommendation, link.nextSibling);
+ addRecommendation(link);
}
});
});
diff --git a/docs/index.md b/docs/index.md
index 026a9338c..1decde297 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -8,21 +8,26 @@ title: Play MUME with MMapper
MMapper is a game client and graphical mapping tool that enhances the MUME (Multi-Users in Middle Earth) experience. It acts as a bridge between the MUME server and your mud client, analyzing real-time game data and visually displaying your character’s position on the map.
-## Download MMapper
+## Get MMapper
+{: #get-mmapper}
-Choose your operating system to download the latest version {{ site.github.latest_release.tag_name }}:
+Choose your preferred platform to start using MMapper {{ site.github.latest_release.tag_name }}:
diff --git a/docs/linux.md b/docs/linux.md
index 282f21272..c16d1fd43 100644
--- a/docs/linux.md
+++ b/docs/linux.md
@@ -1,9 +1,9 @@
---
layout: default
-title: Download MMapper for Linux
+title: Get MMapper for Linux
---
-## Download MMapper for Linux
+Choose your preferred distribution method below. We recommend using Flathub for a sandboxed environment and easy updates across most Linux distributions.
diff --git a/docs/macos.md b/docs/macos.md
index aba67b4a7..ff62d356a 100644
--- a/docs/macos.md
+++ b/docs/macos.md
@@ -1,9 +1,10 @@
---
layout: default
-title: Download MMapper for macOS
+title: Get MMapper for macOS
---
-## Download MMapper for macOS
+To install MMapper on macOS, download the Disk Image (DMG) file provided below. MMapper is compatible with both Intel and Apple Silicon processors.
+
{% for asset in site.github.latest_release.assets %}
{% if asset.name contains 'sha256' %}
{% elsif asset.name contains 'dmg' %}
diff --git a/docs/windows.md b/docs/windows.md
index 080fe745f..014d2e4ec 100644
--- a/docs/windows.md
+++ b/docs/windows.md
@@ -1,13 +1,14 @@
---
layout: default
-title: Download MMapper for Windows
+title: Get MMapper for Windows
---
-## Download MMapper for Windows
+MMapper can be installed from the Microsoft Store for a seamless experience, including automatic updates. Alternatively, you can download the standalone installer below.
-
+
- Recommended
+
+ Recommended
{% for asset in site.github.latest_release.assets %}
{% if asset.name contains 'sha256' %}
diff --git a/src/display/mapcanvas_gl.cpp b/src/display/mapcanvas_gl.cpp
index e43303076..f869b649e 100644
--- a/src/display/mapcanvas_gl.cpp
+++ b/src/display/mapcanvas_gl.cpp
@@ -186,7 +186,7 @@ void MapCanvas::reportGLVersion()
logMsg("Highest OpenGL:", mmqt::toQByteArrayUtf8(OpenGLConfig::getGLVersionString()));
}
if constexpr (!NO_GLES) {
- logMsg("Highest GLES:", mmqt::toQByteArrayUtf8(OpenGLConfig::getESVersionString()));
+ logMsg("Highest ES:", mmqt::toQByteArrayUtf8(OpenGLConfig::getESVersionString()));
}
logMsg("Display:", QString("%1 DPI").arg(QPaintDevice::devicePixelRatioF()).toUtf8());
diff --git a/src/main.cpp b/src/main.cpp
index f8272195a..7fb09ae4a 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -15,6 +15,7 @@
#include "./opengl/OpenGLConfig.h"
#include "./opengl/OpenGLProber.h"
+#include
#include
#include
@@ -143,7 +144,7 @@ static bool setSurfaceFormat()
if constexpr (CURRENT_PLATFORM == PlatformEnum::Wasm) {
msg += "WebGL 2.0";
} else {
- msg += "GLES 3.0";
+ msg += "ES 3.0";
}
}
msg += " support to run.";
@@ -151,13 +152,20 @@ static bool setSurfaceFormat()
return false;
}
OpenGLConfig::setBackendType(probeResult.backendType);
- OpenGLConfig::setIsCompat(probeResult.isCompat);
QSurfaceFormat::setDefaultFormat(probeResult.format);
return true;
}
int main(int argc, char **argv)
{
+#ifndef Q_OS_WASM
+ for (int i = 1; i < argc; ++i) {
+ if (std::strcmp(argv[i], "--probe") == 0) {
+ return OpenGLProber::runSurveyMode(argc, argv);
+ }
+ }
+#endif
+
setHighDpiScaleFactorRoundingPolicy();
setEnteredMain();
if constexpr (IS_DEBUG_BUILD) {
diff --git a/src/opengl/OpenGL.cpp b/src/opengl/OpenGL.cpp
index 36c7ded8f..b315ce5ad 100644
--- a/src/opengl/OpenGL.cpp
+++ b/src/opengl/OpenGL.cpp
@@ -35,7 +35,7 @@ OpenGL::OpenGL()
case OpenGLProber::BackendType::GL:
m_opengl = Legacy::Functions::alloc(m_uboManager);
break;
- case OpenGLProber::BackendType::GLES:
+ case OpenGLProber::BackendType::ES:
m_opengl = Legacy::Functions::alloc(m_uboManager);
break;
case OpenGLProber::BackendType::None:
@@ -94,12 +94,12 @@ UniqueMesh OpenGL::createPointBatch(const std::vector &batch)
UniqueMesh OpenGL::createPlainLineBatch(const std::vector &batch)
{
- return getFunctions().createPlainBatch(DrawModeEnum::LINES, batch);
+ return getFunctions().createPlainLineBatch(batch);
}
UniqueMesh OpenGL::createColoredLineBatch(const std::vector &batch)
{
- return getFunctions().createColoredBatch(DrawModeEnum::LINES, batch);
+ return getFunctions().createColoredLineBatch(batch);
}
UniqueMesh OpenGL::createPlainTriBatch(const std::vector &batch)
@@ -165,14 +165,22 @@ void OpenGL::renderPlain(const DrawModeEnum type,
const std::vector &verts,
const GLRenderState &state)
{
- getFunctions().renderPlain(type, verts, state);
+ if (type == DrawModeEnum::LINES) {
+ getFunctions().renderPlainLines(verts, state);
+ } else {
+ getFunctions().renderPlain(type, verts, state);
+ }
}
void OpenGL::renderColored(const DrawModeEnum type,
const std::vector &verts,
const GLRenderState &state)
{
- getFunctions().renderColored(type, verts, state);
+ if (type == DrawModeEnum::LINES) {
+ getFunctions().renderColoredLines(verts, state);
+ } else {
+ getFunctions().renderColored(type, verts, state);
+ }
}
void OpenGL::renderPoints(const std::vector &verts, const GLRenderState &state)
diff --git a/src/opengl/OpenGLConfig.cpp b/src/opengl/OpenGLConfig.cpp
index b5068bf44..0a301e1c7 100644
--- a/src/opengl/OpenGLConfig.cpp
+++ b/src/opengl/OpenGLConfig.cpp
@@ -6,7 +6,6 @@
namespace OpenGLConfig {
static OpenGLProber::BackendType g_backendType;
-static bool g_isCompat = false;
static std::string g_glVersionString;
static std::string g_esVersionString;
static int g_maxSamples = 0;
@@ -21,16 +20,6 @@ void setBackendType(OpenGLProber::BackendType type)
g_backendType = type;
}
-NODISCARD bool getIsCompat()
-{
- return g_isCompat;
-}
-
-void setIsCompat(bool isCompat)
-{
- g_isCompat = isCompat;
-}
-
NODISCARD std::string getGLVersionString()
{
return g_glVersionString;
diff --git a/src/opengl/OpenGLConfig.h b/src/opengl/OpenGLConfig.h
index 4fe59a23d..e0a13bd52 100644
--- a/src/opengl/OpenGLConfig.h
+++ b/src/opengl/OpenGLConfig.h
@@ -11,9 +11,6 @@ namespace OpenGLConfig {
NODISCARD extern OpenGLProber::BackendType getBackendType();
extern void setBackendType(OpenGLProber::BackendType type);
-NODISCARD extern bool getIsCompat();
-extern void setIsCompat(bool isCompat);
-
NODISCARD extern std::string getGLVersionString();
extern void setGLVersionString(const std::string &versionString);
diff --git a/src/opengl/OpenGLProber.cpp b/src/opengl/OpenGLProber.cpp
index 06721d1dd..54c5e5282 100644
--- a/src/opengl/OpenGLProber.cpp
+++ b/src/opengl/OpenGLProber.cpp
@@ -4,317 +4,219 @@
#include "OpenGLProber.h"
#include "../global/ConfigConsts.h"
+#include "../global/TextUtils.h"
#include "../global/logging.h"
#include "OpenGLConfig.h"
-#include
-#include
-
+#include
+#include
+#include
+#include
+#ifndef Q_OS_WASM
+#include
#include
+#include
+#include
+#endif
+#include
#ifdef WIN32
#include
-extern "C" {
// Prefer discrete nVidia and AMD GPUs by default on Windows
-__declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001;
-__declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
-}
+extern "C" __declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001;
+extern "C" __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
#endif
-namespace { // anonymous
-
-struct GLVersion
-{
- int major = 0;
- int minor = 0;
-
- NODISCARD bool operator>(const GLVersion &other) const
- {
- return (major > other.major) || (major == other.major && minor > other.minor);
- }
-
- NODISCARD bool operator<(const GLVersion &other) const
- {
- return (major < other.major) || (major == other.major && minor < other.minor);
- }
-};
+namespace {
-inline std::ostream &operator<<(std::ostream &os, const GLVersion &version)
+OpenGLProber::ProbeResult getFallbackResult()
{
- os << version.major << "." << version.minor;
- return os;
+ OpenGLProber::ProbeResult result;
+ result.backendType = OpenGLProber::BackendType::GL;
+ result.format.setRenderableType(QSurfaceFormat::OpenGL);
+ result.format.setVersion(3, 3);
+ result.format.setProfile(QSurfaceFormat::CoreProfile);
+ result.format.setDepthBufferSize(24);
+ result.highestVersionString = "Fallback";
+ return result;
}
-struct GLContextCheckResult
-{
- bool valid = false;
- GLVersion version = {0, 0};
- bool isCore = false;
- bool isCompat = false;
- bool isDeprecated = false;
- bool isDebug = false;
-};
+} // namespace
-NODISCARD GLContextCheckResult checkContext(QSurfaceFormat format,
- GLVersion version,
- QSurfaceFormat::OpenGLContextProfile profile)
+OpenGLProber::ProbeResult OpenGLProber::probe()
{
- GLContextCheckResult result;
- QOpenGLContext context;
- context.setFormat(format);
- if (!context.create()) {
- MMLOG_DEBUG() << "[GL Check] context.create() failed for requested " << version
- << (profile == QSurfaceFormat::CoreProfile ? " Core" : " Compat");
- return result;
+#ifdef Q_OS_WASM
+ MMLOG_DEBUG() << "Probing for OpenGL support (Wasm/WebGL 2.0)...";
+ ProbeResult result;
+ result.backendType = BackendType::ES;
+ result.format.setRenderableType(QSurfaceFormat::OpenGLES);
+ result.format.setVersion(3, 0);
+ result.format.setDepthBufferSize(24);
+ result.highestVersionString = "WebGL 2.0";
+ OpenGLConfig::setESVersionString(result.highestVersionString);
+ return result;
+#else
+ if constexpr (NO_OPENGL && NO_GLES) {
+ return {};
}
- QSurfaceFormat actualFormat = context.format();
+ MMLOG_DEBUG() << "Probing for OpenGL support via --probe subprocess...";
- result.isCore = (actualFormat.profile() & QSurfaceFormat::CoreProfile);
- result.isCompat = (actualFormat.profile() & QSurfaceFormat::CompatibilityProfile);
- result.isDeprecated = (actualFormat.options() & QSurfaceFormat::DeprecatedFunctions);
- result.isDebug = (actualFormat.options() & QSurfaceFormat::DebugContext);
- result.version = GLVersion{actualFormat.majorVersion(), actualFormat.minorVersion()};
+ const QString selfPath = QCoreApplication::applicationFilePath();
+ QProcess process;
- context.doneCurrent();
+ QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
+ env.insert("ASAN_OPTIONS", "detect_leaks=0");
+ process.setProcessEnvironment(env);
- // Check if the actual OpenGL context meets the minimum requirements
- bool profileOk = false;
-
- if (version < GLVersion{3, 2}) {
- if (profile == QSurfaceFormat::CoreProfile) {
- // Core profile did not exist before GL 3.2
- profileOk = false;
- } else {
- // Must be compatibility-like
- profileOk = result.isCompat || result.isDeprecated;
- }
- } else {
- // For GL 3.2+
- if (profile == QSurfaceFormat::CoreProfile) {
- profileOk = result.isCore;
- } else if (profile == QSurfaceFormat::CompatibilityProfile) {
- profileOk = result.isCompat && result.isDeprecated;
- }
+ QStringList args = {"--probe"};
+ const QString platform = QGuiApplication::platformName();
+ if (!platform.isEmpty()) {
+ args << "-platform" << platform;
}
-
- // If the profile is ok, we consider the context valid even if the version is lower than requested.
- if (profileOk) {
- result.valid = true;
- MMLOG_DEBUG() << "[GL Probe] GL " << result.version << (result.isCore ? " Core" : " Compat")
- << " is valid";
+ process.start(selfPath, args);
+ if (!process.waitForFinished(5000)) {
+ MMLOG_ERROR() << "OpenGL probe subprocess timed out or failed to start. Using fallback.";
+ process.kill();
+ return getFallbackResult();
}
- return result;
-}
-NODISCARD std::string formatGLVersionString(const GLContextCheckResult &result)
-{
- std::ostringstream oss;
- oss << "GL" << result.version;
- if (!result.isDeprecated && result.version > GLVersion{3, 1}) {
- oss << "core";
- }
- return oss.str();
+ const QByteArray stdoutData = process.readAllStandardOutput();
+ const QByteArray stderrData = process.readAllStandardError();
+ return parseSurveyResult(stdoutData, stderrData);
+#endif
}
-NODISCARD std::optional probeCore(QSurfaceFormat &testFormat,
- std::vector &coreVersions,
- QSurfaceFormat::FormatOptions optionsCoreOnly)
+OpenGLProber::ProbeResult OpenGLProber::parseSurveyResult(const QByteArray &stdoutData,
+ const QByteArray &stderrData)
{
- std::optional coreResult = std::nullopt;
- for (auto it = coreVersions.begin(); it != coreVersions.end();) {
- const auto &version = *it;
- testFormat.setVersion(version.major, version.minor);
- testFormat.setProfile(QSurfaceFormat::CoreProfile);
- testFormat.setOptions(optionsCoreOnly);
+ const qsizetype start = stdoutData.indexOf('{');
+ const qsizetype end = stdoutData.lastIndexOf('}');
- GLContextCheckResult contextCheckResult = checkContext(testFormat,
- version,
- QSurfaceFormat::CoreProfile);
- if (contextCheckResult.valid) {
- coreResult = contextCheckResult;
- MMLOG_DEBUG() << "[GL Probe] Found highest supported Core version: "
- << contextCheckResult.version;
- break;
- } else {
- it = coreVersions.erase(it);
- }
+ QJsonDocument doc;
+ if (start != -1 && end != -1 && end > start) {
+ doc = QJsonDocument::fromJson(stdoutData.mid(start, end - start + 1));
}
- return coreResult;
-}
-
-NODISCARD std::optional probeCompat(
- QSurfaceFormat &format,
- std::vector versions,
- QSurfaceFormat::FormatOptions options,
- std::optional coreResult)
-{
- std::optional compatResult = std::nullopt;
- std::vector compatVersionsToTest = versions;
- if (coreResult) {
- // Filter compatVersions to only include versions <= coreResult
- compatVersionsToTest.erase(std::remove_if(compatVersionsToTest.begin(),
- compatVersionsToTest.end(),
- [resultVer = coreResult->version](
- const GLVersion &ver) {
- return ver > resultVer;
- }),
- compatVersionsToTest.end());
+ if (doc.isNull() || !doc.isObject()) {
+ MMLOG_WARNING() << "OpenGL survey returned invalid JSON. Using fallback."
+ << "\n stdout:" << stdoutData.toStdString()
+ << "\n stderr:" << stderrData.toStdString();
+ return getFallbackResult();
}
- for (const auto &version : compatVersionsToTest) {
- format.setVersion(version.major, version.minor);
- format.setProfile(QSurfaceFormat::CompatibilityProfile);
- format.setOptions(options);
- GLContextCheckResult contextCheckResult = checkContext(format,
- version,
- QSurfaceFormat::CompatibilityProfile);
- if (contextCheckResult.valid) {
- compatResult = contextCheckResult;
- MMLOG_DEBUG() << "[GL Probe] Found highest supported Compat version: "
- << compatResult->version;
- break;
- }
- }
- return compatResult;
-}
+ QJsonObject obj = doc.object();
+ QString backend = obj["backend"].toString();
+ const bool debugSupported = obj["debug"].toBool();
-NODISCARD std::string getHighestGLVersion(std::optional coreResult,
- std::optional compatResult)
-{
- std::string highestGLVersion;
- if (coreResult && compatResult) {
- if (coreResult->version > compatResult->version) {
- highestGLVersion = formatGLVersionString(*coreResult);
- } else {
- highestGLVersion = formatGLVersionString(*compatResult);
- }
- } else if (coreResult) {
- highestGLVersion = formatGLVersionString(*coreResult);
- } else if (compatResult) {
- highestGLVersion = formatGLVersionString(*compatResult);
- } else {
- highestGLVersion = "Fallback";
+ ProbeResult result;
+ result.debugSupported = debugSupported;
+ if (debugSupported) {
+ result.format.setOption(QSurfaceFormat::DebugContext);
}
- return highestGLVersion;
-}
-NODISCARD QSurfaceFormat getOptimalFormat(std::optional result)
-{
- QSurfaceFormat format;
- format.setRenderableType(QSurfaceFormat::OpenGL);
- format.setDepthBufferSize(24);
- if (result) {
- format.setVersion(result->version.major, result->version.minor);
- format.setProfile(result->isCore ? QSurfaceFormat::CoreProfile
- : QSurfaceFormat::CompatibilityProfile);
- QSurfaceFormat::FormatOptions options;
- if (result->isDebug) {
- options |= QSurfaceFormat::DebugContext;
- }
- if (result->isCompat) {
- options |= QSurfaceFormat::DeprecatedFunctions;
- }
- format.setOptions(options);
- MMLOG_INFO() << "[GL Probe] Optimal running format determined: GL " << format.majorVersion()
- << "." << format.minorVersion()
- << " Profile: " << (result->isCore ? "Core" : "Compat")
- << (result->isDebug ? " (Debug)" : " (NO Debug)");
+ if (backend == "GL") {
+ result.backendType = BackendType::GL;
+ int major = obj["major"].toInt();
+ int minor = obj["minor"].toInt();
+ result.format.setRenderableType(QSurfaceFormat::OpenGL);
+ result.format.setVersion(major, minor);
+ result.format.setProfile(QSurfaceFormat::CoreProfile);
+ result.format.setDepthBufferSize(24);
+ result.highestVersionString = mmqt::toStdStringUtf8(
+ QString("GL%1.%2").arg(major).arg(minor));
+ OpenGLConfig::setGLVersionString(result.highestVersionString);
+ } else if (backend == "ES") {
+ result.backendType = BackendType::ES;
+ int major = obj["major"].toInt();
+ int minor = obj["minor"].toInt();
+ result.format.setRenderableType(QSurfaceFormat::OpenGLES);
+ result.format.setVersion(major, minor);
+ result.format.setDepthBufferSize(24);
+ result.highestVersionString = mmqt::toStdStringUtf8(
+ QString("ES%1.%2").arg(major).arg(minor));
+ OpenGLConfig::setESVersionString(result.highestVersionString);
} else {
- // Fallback for optimal running format if no context was found at all
- format.setVersion(3, 3);
- format.setProfile(QSurfaceFormat::CoreProfile);
- format.setOptions(QSurfaceFormat::DebugContext);
- MMLOG_ERROR() << "[GL Probe] No suitable GL context found for running format.";
- }
- return format;
-}
-
-OpenGLProber::ProbeResult probeOpenGL()
-{
- if constexpr (NO_OPENGL) {
+ MMLOG_DEBUG() << "No suitable backend found by survey.";
return {};
}
- MMLOG_DEBUG() << "Probing for OpenGL support...";
-
- QSurfaceFormat format;
- format.setRenderableType(QSurfaceFormat::OpenGL);
- format.setDepthBufferSize(24);
-
- QSurfaceFormat::FormatOptions optionsCompat = QSurfaceFormat::DebugContext
- | QSurfaceFormat::DeprecatedFunctions;
- QSurfaceFormat::FormatOptions optionsCore = QSurfaceFormat::DebugContext;
-
- // Define lists of versions to try.
- std::vector versions
- = {{4, 6}, {4, 5}, {4, 4}, {4, 3}, {4, 2}, {4, 1}, {4, 0}, {3, 3}, {3, 2}};
-
- std::optional coreResult = probeCore(format, versions, optionsCore);
- std::optional compatResult = probeCompat(format,
- versions,
- optionsCompat,
- coreResult);
-
- OpenGLProber::ProbeResult result;
- if (compatResult || coreResult) {
- result.backendType = OpenGLProber::BackendType::GL;
- result.highestVersionString = getHighestGLVersion(coreResult, compatResult);
- OpenGLConfig::setGLVersionString(result.highestVersionString);
- result.format = getOptimalFormat(compatResult ? compatResult : coreResult);
- result.isCompat = (compatResult && compatResult->isCompat);
- }
+ MMLOG_INFO() << "[GL Probe] Survey determined:" << result.highestVersionString.c_str();
return result;
}
-OpenGLProber::ProbeResult probeOpenGLES()
+#ifndef Q_OS_WASM
+int OpenGLProber::runSurveyMode(int argc, char **argv)
{
- if constexpr (NO_GLES) {
- return {};
- }
+ QGuiApplication app(argc, argv);
- MMLOG_DEBUG() << "Probing for OpenGL ES support...";
+ QJsonObject result;
- std::vector glesVersions = {{3, 2}, {3, 1}, {3, 0}};
+ struct GLVersion
+ {
+ int major;
+ int minor;
+ };
- for (const auto &version : glesVersions) {
+ // Try OpenGL Core versions from highest to lowest
+ const std::vector coreVersions
+ = {{4, 6}, {4, 5}, {4, 4}, {4, 3}, {4, 2}, {4, 1}, {4, 0}, {3, 3}};
+
+ bool foundBackend = false;
+ for (const auto &v : coreVersions) {
QSurfaceFormat format;
- format.setRenderableType(QSurfaceFormat::OpenGLES);
- format.setVersion(version.major, version.minor);
+ format.setRenderableType(QSurfaceFormat::OpenGL);
+ format.setVersion(v.major, v.minor);
+ format.setProfile(QSurfaceFormat::CoreProfile);
+ format.setOption(QSurfaceFormat::DebugContext);
QOpenGLContext context;
context.setFormat(format);
if (context.create()) {
- MMLOG_DEBUG() << "[GL Probe] Found highest supported GLES version: " << version;
- OpenGLProber::ProbeResult result;
- result.backendType = OpenGLProber::BackendType::GLES;
- result.format = format;
- std::ostringstream oss;
- oss << "ES" << version;
- result.highestVersionString = oss.str();
- OpenGLConfig::setESVersionString(result.highestVersionString);
- return result;
+ const QSurfaceFormat actual = context.format();
+ if (actual.profile() == QSurfaceFormat::CoreProfile
+ && (actual.majorVersion() > v.major
+ || (actual.majorVersion() == v.major && actual.minorVersion() >= v.minor))) {
+ result["backend"] = "GL";
+ result["major"] = actual.majorVersion();
+ result["minor"] = actual.minorVersion();
+ result["debug"] = actual.testOption(QSurfaceFormat::DebugContext);
+ foundBackend = true;
+ break;
+ }
}
}
- MMLOG_DEBUG() << "No suitable GLES context found.";
- return {};
-}
-
-} // namespace
-
-OpenGLProber::ProbeResult OpenGLProber::probe()
-{
- auto glResult = probeOpenGL();
- auto glesResult = probeOpenGLES();
- if (glResult.backendType != BackendType::None) {
- return glResult;
+ if (!foundBackend) {
+ const std::vector glesVersions = {{3, 2}, {3, 1}, {3, 0}};
+ for (const auto &v : glesVersions) {
+ QSurfaceFormat format;
+ format.setRenderableType(QSurfaceFormat::OpenGLES);
+ format.setVersion(v.major, v.minor);
+ format.setOption(QSurfaceFormat::DebugContext);
+
+ QOpenGLContext context;
+ context.setFormat(format);
+ if (context.create()) {
+ const QSurfaceFormat actual = context.format();
+ result["backend"] = "ES";
+ result["major"] = actual.majorVersion();
+ result["minor"] = actual.minorVersion();
+ result["debug"] = actual.testOption(QSurfaceFormat::DebugContext);
+ foundBackend = true;
+ break;
+ }
+ }
}
- if (glesResult.backendType != BackendType::None) {
- return glesResult;
+
+ if (result.isEmpty()) {
+ result["backend"] = "None";
}
- MMLOG_DEBUG() << "No suitable backend found.";
- return {};
+
+ const QByteArray output = QJsonDocument(result).toJson(QJsonDocument::Compact);
+ fprintf(stdout, "\n%s\n", output.constData());
+ fflush(stdout);
+ return 0;
}
+#endif
diff --git a/src/opengl/OpenGLProber.h b/src/opengl/OpenGLProber.h
index 9e71a83ae..159afb9fc 100644
--- a/src/opengl/OpenGLProber.h
+++ b/src/opengl/OpenGLProber.h
@@ -7,19 +7,20 @@
#include
+#include
#include
class NODISCARD OpenGLProber final
{
public:
- enum class BackendType { None, GL, GLES };
+ enum class BackendType { None, GL, ES };
struct ProbeResult
{
BackendType backendType = BackendType::None;
QSurfaceFormat format;
std::string highestVersionString = "Unknown";
- bool isCompat = false;
+ bool debugSupported = false;
};
public:
@@ -28,4 +29,11 @@ class NODISCARD OpenGLProber final
DTOR(OpenGLProber) = default;
NODISCARD ProbeResult probe();
+
+ NODISCARD ProbeResult parseSurveyResult(const QByteArray &stdoutData,
+ const QByteArray &stderrData = {});
+
+#ifndef Q_OS_WASM
+ NODISCARD static int runSurveyMode(int argc, char **argv);
+#endif
};
diff --git a/src/opengl/OpenGLTypes.h b/src/opengl/OpenGLTypes.h
index 1fff18550..b35bd5b4a 100644
--- a/src/opengl/OpenGLTypes.h
+++ b/src/opengl/OpenGLTypes.h
@@ -139,9 +139,9 @@ enum class NODISCARD DrawModeEnum {
struct NODISCARD LineParams final
{
- float width = 1.f;
- LineParams() = default;
+ float width = 1.0f;
+ LineParams() = default;
explicit LineParams(const float width_)
: width{width_}
{}
diff --git a/src/opengl/legacy/AbstractShaderProgram.cpp b/src/opengl/legacy/AbstractShaderProgram.cpp
index 1adecd1c5..a59f5468a 100644
--- a/src/opengl/legacy/AbstractShaderProgram.cpp
+++ b/src/opengl/legacy/AbstractShaderProgram.cpp
@@ -171,4 +171,10 @@ void AbstractShaderProgram::setViewport(const char *const name, const Viewport &
setUniform4iv(location, 1, glm::value_ptr(viewport));
}
+void AbstractShaderProgram::setFloat(const char *const name, const float value)
+{
+ const GLint location = getUniformLocation(name);
+ setUniform1fv(location, 1, &value);
+}
+
} // namespace Legacy
diff --git a/src/opengl/legacy/AbstractShaderProgram.h b/src/opengl/legacy/AbstractShaderProgram.h
index 7765d047e..85b57c98a 100644
--- a/src/opengl/legacy/AbstractShaderProgram.h
+++ b/src/opengl/legacy/AbstractShaderProgram.h
@@ -92,6 +92,7 @@ struct NODISCARD AbstractShaderProgram
void setMatrix(const char *name, const glm::mat4 &m);
void setTexture(const char *name, int textureUnit);
void setViewport(const char *name, const Viewport &input_viewport);
+ void setFloat(const char *name, float value);
};
} // namespace Legacy
diff --git a/src/opengl/legacy/Binders.cpp b/src/opengl/legacy/Binders.cpp
index 398c60a4d..91b8cac06 100644
--- a/src/opengl/legacy/Binders.cpp
+++ b/src/opengl/legacy/Binders.cpp
@@ -106,22 +106,9 @@ DepthBinder::~DepthBinder()
}
}
-LineParamsBinder::LineParamsBinder(Functions &functions, const LineParams &lineParams)
- : m_functions{functions}
- , m_lineParams{lineParams}
-{
- m_functions.glLineWidth(m_lineParams.width);
-}
+LineParamsBinder::LineParamsBinder(Functions & /*functions*/, const LineParams & /*lineParams*/) {}
-LineParamsBinder::~LineParamsBinder()
-{
- const auto &width = m_lineParams.width;
- const float ONE = 1.f;
- static_assert(sizeof(ONE) == sizeof(width));
- if (!utils::equals(&width, &ONE)) {
- m_functions.glLineWidth(ONE);
- }
-}
+LineParamsBinder::~LineParamsBinder() {}
PointSizeBinder::PointSizeBinder(Functions &functions, const std::optional &pointSize)
: m_functions{functions}
diff --git a/src/opengl/legacy/Binders.h b/src/opengl/legacy/Binders.h
index 3da993741..06aaf238c 100644
--- a/src/opengl/legacy/Binders.h
+++ b/src/opengl/legacy/Binders.h
@@ -56,10 +56,6 @@ struct NODISCARD DepthBinder final
struct NODISCARD LineParamsBinder
{
-private:
- Functions &m_functions;
- const LineParams &m_lineParams;
-
public:
explicit LineParamsBinder(Functions &functions, const LineParams &lineParams);
~LineParamsBinder();
diff --git a/src/opengl/legacy/FunctionsES30.cpp b/src/opengl/legacy/FunctionsES30.cpp
index 3de292be3..b9b9894a7 100644
--- a/src/opengl/legacy/FunctionsES30.cpp
+++ b/src/opengl/legacy/FunctionsES30.cpp
@@ -4,14 +4,7 @@ namespace Legacy {
FunctionsES30::FunctionsES30(Badge badge, UboManager &uboManager)
: Functions(badge, uboManager)
-{
- assert(!OpenGLConfig::getIsCompat());
-}
-
-bool FunctionsES30::virt_canRenderQuads()
-{
- return false;
-}
+{}
std::optional FunctionsES30::virt_toGLenum(const DrawModeEnum mode)
{
diff --git a/src/opengl/legacy/FunctionsES30.h b/src/opengl/legacy/FunctionsES30.h
index 984f3dbe7..035d1e047 100644
--- a/src/opengl/legacy/FunctionsES30.h
+++ b/src/opengl/legacy/FunctionsES30.h
@@ -17,7 +17,6 @@ class FunctionsES30 final : public Functions
private:
void virt_enableProgramPointSize(bool enable) override;
NODISCARD const char *virt_getShaderVersion() const override;
- NODISCARD bool virt_canRenderQuads() override;
NODISCARD std::optional virt_toGLenum(DrawModeEnum mode) override;
};
diff --git a/src/opengl/legacy/FunctionsGL33.cpp b/src/opengl/legacy/FunctionsGL33.cpp
index 7d5cc0644..c8d65cdf6 100644
--- a/src/opengl/legacy/FunctionsGL33.cpp
+++ b/src/opengl/legacy/FunctionsGL33.cpp
@@ -1,7 +1,6 @@
#include "FunctionsGL33.h"
#include "../../global/ConfigConsts.h"
-#include "../OpenGLConfig.h"
#include
@@ -11,11 +10,6 @@ FunctionsGL33::FunctionsGL33(Badge badge, UboManager &uboManager)
: Functions(badge, uboManager)
{}
-bool FunctionsGL33::virt_canRenderQuads()
-{
- return OpenGLConfig::getIsCompat();
-}
-
std::optional FunctionsGL33::virt_toGLenum(const DrawModeEnum mode)
{
switch (mode) {
@@ -26,11 +20,6 @@ std::optional FunctionsGL33::virt_toGLenum(const DrawModeEnum mode)
case DrawModeEnum::TRIANGLES:
return GL_TRIANGLES;
case DrawModeEnum::QUADS:
-#ifndef MMAPPER_NO_OPENGL
- return canRenderQuads() ? std::make_optional(GL_QUADS) : std::nullopt;
-#else
- FALLTHROUGH;
-#endif
case DrawModeEnum::INSTANCED_QUADS:
case DrawModeEnum::INVALID:
break;
@@ -49,14 +38,8 @@ void FunctionsGL33::virt_enableProgramPointSize(const bool enable)
#ifndef MMAPPER_NO_OPENGL
if (enable) {
Base::glEnable(GL_PROGRAM_POINT_SIZE);
- if (OpenGLConfig::getIsCompat()) {
- Base::glEnable(GL_POINT_SPRITE);
- }
} else {
Base::glDisable(GL_PROGRAM_POINT_SIZE);
- if (OpenGLConfig::getIsCompat()) {
- Base::glDisable(GL_POINT_SPRITE);
- }
}
#endif
}
diff --git a/src/opengl/legacy/FunctionsGL33.h b/src/opengl/legacy/FunctionsGL33.h
index ab4926a1b..355407fce 100644
--- a/src/opengl/legacy/FunctionsGL33.h
+++ b/src/opengl/legacy/FunctionsGL33.h
@@ -17,7 +17,6 @@ class FunctionsGL33 final : public Functions
private:
void virt_enableProgramPointSize(bool enable) override;
NODISCARD const char *virt_getShaderVersion() const override;
- NODISCARD bool virt_canRenderQuads() override;
NODISCARD std::optional virt_toGLenum(DrawModeEnum mode) override;
};
diff --git a/src/opengl/legacy/Legacy.cpp b/src/opengl/legacy/Legacy.cpp
index c8cb265d1..1648ccbbf 100644
--- a/src/opengl/legacy/Legacy.cpp
+++ b/src/opengl/legacy/Legacy.cpp
@@ -112,9 +112,21 @@ UniqueMesh Functions::createPointBatch(const std::vector &batch)
return createUniqueMesh(shared_from_this(), DrawModeEnum::POINTS, batch, prog);
}
+UniqueMesh Functions::createPlainLineBatch(const std::vector &batch)
+{
+ const auto &prog = getShaderPrograms().getLineUColorShader();
+ return createUniqueMesh(shared_from_this(), DrawModeEnum::LINES, batch, prog);
+}
+
+UniqueMesh Functions::createColoredLineBatch(const std::vector &batch)
+{
+ const auto &prog = getShaderPrograms().getLineAColorShader();
+ return createUniqueMesh(shared_from_this(), DrawModeEnum::LINES, batch, prog);
+}
+
UniqueMesh Functions::createPlainBatch(const DrawModeEnum mode, const std::vector &batch)
{
- assert(static_cast(mode) >= VERTS_PER_LINE);
+ assert(static_cast(mode) >= VERTS_PER_TRI);
const auto &prog = getShaderPrograms().getPlainUColorShader();
return createUniqueMesh(shared_from_this(), mode, batch, prog);
}
@@ -122,7 +134,7 @@ UniqueMesh Functions::createPlainBatch(const DrawModeEnum mode, const std::vecto
UniqueMesh Functions::createColoredBatch(const DrawModeEnum mode,
const std::vector &batch)
{
- assert(static_cast(mode) >= VERTS_PER_LINE);
+ assert(static_cast(mode) >= VERTS_PER_TRI);
const auto &prog = getShaderPrograms().getPlainAColorShader();
return createUniqueMesh(shared_from_this(), mode, batch, prog);
}
@@ -201,11 +213,29 @@ static void renderImmediate(const SharedFunctions &sharedFunctions,
sharedFunctions->clearVbo(vbo.get());
}
+void Functions::renderPlainLines(const std::vector &verts, const GLRenderState &state)
+{
+ const auto &shared = shared_from_this();
+ const auto &prog = getShaderPrograms().getLineUColorShader();
+ renderImmediate(shared, DrawModeEnum::LINES, verts, prog, state);
+}
+
+void Functions::renderColoredLines(const std::vector &verts, const GLRenderState &state)
+{
+ const auto &shared = shared_from_this();
+ const auto &prog = getShaderPrograms().getLineAColorShader();
+ renderImmediate(shared,
+ DrawModeEnum::LINES,
+ verts,
+ prog,
+ state);
+}
+
void Functions::renderPlain(const DrawModeEnum mode,
const std::vector &verts,
const GLRenderState &state)
{
- assert(static_cast(mode) >= VERTS_PER_LINE);
+ assert(static_cast(mode) >= VERTS_PER_TRI);
const auto &shared = shared_from_this();
const auto &prog = getShaderPrograms().getPlainUColorShader();
renderImmediate(shared, mode, verts, prog, state);
@@ -215,7 +245,7 @@ void Functions::renderColored(const DrawModeEnum mode,
const std::vector &verts,
const GLRenderState &state)
{
- assert(static_cast(mode) >= VERTS_PER_LINE);
+ assert(static_cast(mode) >= VERTS_PER_TRI);
const auto &prog = getShaderPrograms().getPlainAColorShader();
renderImmediate(shared_from_this(), mode, verts, prog, state);
}
diff --git a/src/opengl/legacy/Legacy.h b/src/opengl/legacy/Legacy.h
index d6f67065f..f5d249bdd 100644
--- a/src/opengl/legacy/Legacy.h
+++ b/src/opengl/legacy/Legacy.h
@@ -259,15 +259,6 @@ class NODISCARD Functions : protected QOpenGLExtraFunctions,
using Base::glVertexAttribDivisor;
using Base::glVertexAttribPointer;
-public:
- void glLineWidth(const GLfloat lineWidth)
- {
- // REVISIT: Only width 1 is guaranteed to be supported for core profiles
- if (OpenGLConfig::getIsCompat()) {
- Base::glLineWidth(lineWidth);
- }
- }
-
public:
void glViewport(const GLint x, const GLint y, const GLsizei width, const GLsizei height)
{
@@ -331,7 +322,6 @@ class NODISCARD Functions : protected QOpenGLExtraFunctions,
NODISCARD const char *getShaderVersion() const { return virt_getShaderVersion(); }
protected:
- NODISCARD virtual bool virt_canRenderQuads() = 0;
NODISCARD virtual std::optional virt_toGLenum(DrawModeEnum mode) = 0;
virtual void virt_enableProgramPointSize(bool enable) = 0;
NODISCARD virtual const char *virt_getShaderVersion() const = 0;
@@ -340,9 +330,6 @@ class NODISCARD Functions : protected QOpenGLExtraFunctions,
public:
NODISCARD static const char *getUniformBlockName(SharedVboEnum block);
- /// platform-specific (ES vs GL)
- NODISCARD bool canRenderQuads() { return virt_canRenderQuads(); }
-
/// platform-specific (ES vs GL)
NODISCARD std::optional toGLenum(DrawModeEnum mode) { return virt_toGLenum(mode); }
@@ -412,7 +399,7 @@ class NODISCARD Functions : protected QOpenGLExtraFunctions,
const std::vector &batch,
const BufferUsageEnum usage = BufferUsageEnum::DYNAMIC_DRAW)
{
- if (mode == DrawModeEnum::QUADS && !canRenderQuads()) {
+ if (mode == DrawModeEnum::QUADS) {
return std::pair(DrawModeEnum::TRIANGLES,
setVbo(GL_ARRAY_BUFFER, vbo, convertQuadsToTris(batch), usage));
}
@@ -428,6 +415,8 @@ class NODISCARD Functions : protected QOpenGLExtraFunctions,
public:
NODISCARD UniqueMesh createPointBatch(const std::vector &batch);
+ NODISCARD UniqueMesh createPlainLineBatch(const std::vector &batch);
+ NODISCARD UniqueMesh createColoredLineBatch(const std::vector &batch);
public:
NODISCARD UniqueMesh createPlainBatch(DrawModeEnum mode, const std::vector &batch);
@@ -450,6 +439,8 @@ class NODISCARD Functions : protected QOpenGLExtraFunctions,
public:
void renderPoints(const std::vector &verts, const GLRenderState &state);
+ void renderPlainLines(const std::vector &verts, const GLRenderState &state);
+ void renderColoredLines(const std::vector &verts, const GLRenderState &state);
void renderPlain(DrawModeEnum mode,
const std::vector &verts,
diff --git a/src/opengl/legacy/Meshes.h b/src/opengl/legacy/Meshes.h
index 2b4bc27b2..37ead3d75 100644
--- a/src/opengl/legacy/Meshes.h
+++ b/src/opengl/legacy/Meshes.h
@@ -63,6 +63,99 @@ class NODISCARD PlainMesh final : public SimpleMesh
+class NODISCARD LineMesh final : public SimpleMesh
+{
+public:
+ using Base = SimpleMesh;
+ using Base::Base;
+
+private:
+ struct NODISCARD Attribs final
+ {
+ GLuint vert1Pos = INVALID_ATTRIB_LOCATION;
+ GLuint vert2Pos = INVALID_ATTRIB_LOCATION;
+
+ NODISCARD static Attribs getLocations(AbstractShaderProgram &shader)
+ {
+ Attribs result;
+ result.vert1Pos = shader.getAttribLocation("aVert1");
+ result.vert2Pos = shader.getAttribLocation("aVert2");
+ return result;
+ }
+ };
+
+ std::optional m_boundAttribs;
+
+ void virt_bind() override
+ {
+ const auto vertSize = static_cast(sizeof(VertexType_));
+ Functions &gl = Base::m_functions;
+ const auto attribs = Attribs::getLocations(Base::m_program);
+ gl.glBindBuffer(GL_ARRAY_BUFFER, Base::m_vbo.get());
+ // aVert1
+ gl.enableAttrib(attribs.vert1Pos, 3, GL_FLOAT, GL_FALSE, 2 * vertSize, nullptr);
+ // aVert2
+ gl.enableAttrib(attribs.vert2Pos, 3, GL_FLOAT, GL_FALSE, 2 * vertSize, VPO_PTR(vertSize));
+
+ // instancing: one line per 4 vertices (quad)
+ gl.glVertexAttribDivisor(attribs.vert1Pos, 1);
+ gl.glVertexAttribDivisor(attribs.vert2Pos, 1);
+
+ m_boundAttribs = attribs;
+ }
+
+ void virt_render(const GLRenderState &renderState) override
+ {
+ if (Base::isEmpty()) {
+ return;
+ }
+
+ Functions &gl = Base::m_functions;
+ gl.checkError();
+
+ const glm::mat4 mvp = renderState.mvp.value_or(gl.getProjectionMatrix());
+ auto programUnbinder = Base::m_program.bind();
+ Base::m_program.setUniforms(mvp, renderState.uniforms);
+ Base::m_program.setViewport("uViewport", gl.getPhysicalViewport());
+ Base::m_program.setFloat("uLineWidth",
+ renderState.lineParams.width * gl.getDevicePixelRatio());
+
+ RenderStateBinder renderStateBinder(gl, gl.getTexLookup(), renderState);
+
+ gl.glBindVertexArray(Base::m_vao.get());
+ RAIICallback vaoUnbinder([&gl]() {
+ gl.glBindVertexArray(0);
+ gl.checkError();
+ });
+
+ auto attribUnbinder = Base::bindAttribs();
+
+ const GLsizei numInstances = Base::m_numVerts / 2;
+ gl.glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, numInstances);
+ }
+
+ void virt_unbind() override
+ {
+ if (!m_boundAttribs) {
+ return;
+ }
+ auto &attribs = m_boundAttribs.value();
+ Functions &gl = Base::m_functions;
+ gl.glDisableVertexAttribArray(attribs.vert1Pos);
+ gl.glDisableVertexAttribArray(attribs.vert2Pos);
+ gl.glVertexAttribDivisor(attribs.vert1Pos, 0);
+ gl.glVertexAttribDivisor(attribs.vert2Pos, 0);
+ gl.glBindBuffer(GL_ARRAY_BUFFER, 0);
+ m_boundAttribs.reset();
+ }
+
+ NODISCARD static const void *VPO_PTR(size_t offset)
+ {
+ return reinterpret_cast(offset);
+ }
+};
+
// Per-vertex color
// flat-shaded in MMapper, due to glShadeModel(GL_FLAT)
template
@@ -117,7 +210,114 @@ class NODISCARD ColoredMesh final : public SimpleMesh
+class NODISCARD ColoredLineMesh final : public SimpleMesh
+{
+public:
+ using Base = SimpleMesh;
+ using Base::Base;
+
+private:
+ struct NODISCARD Attribs final
+ {
+ GLuint colorPos = INVALID_ATTRIB_LOCATION;
+ GLuint vert1Pos = INVALID_ATTRIB_LOCATION;
+ GLuint vert2Pos = INVALID_ATTRIB_LOCATION;
+
+ NODISCARD static Attribs getLocations(AbstractShaderProgram &shader)
+ {
+ Attribs result;
+ result.colorPos = shader.getAttribLocation("aColor");
+ result.vert1Pos = shader.getAttribLocation("aVert1");
+ result.vert2Pos = shader.getAttribLocation("aVert2");
+ return result;
+ }
+ };
+
+ std::optional m_boundAttribs;
+
+ void virt_bind() override
+ {
+ const auto vertSize = static_cast(sizeof(VertexType_));
+ Functions &gl = Base::m_functions;
+ const auto attribs = Attribs::getLocations(Base::m_program);
+ gl.glBindBuffer(GL_ARRAY_BUFFER, Base::m_vbo.get());
+
+ // We assume the color comes from the first vertex of the segment
+ // aColor
+ gl.enableAttrib(attribs.colorPos, 4, GL_UNSIGNED_BYTE, GL_TRUE, 2 * vertSize, VPO(color));
+ // aVert1
+ gl.enableAttrib(attribs.vert1Pos, 3, GL_FLOAT, GL_FALSE, 2 * vertSize, VPO(vert));
+ // aVert2
+ gl.enableAttrib(attribs.vert2Pos,
+ 3,
+ GL_FLOAT,
+ GL_FALSE,
+ 2 * vertSize,
+ VPO_PTR(VPO(vert), vertSize));
+
+ // instancing
+ gl.glVertexAttribDivisor(attribs.colorPos, 1);
+ gl.glVertexAttribDivisor(attribs.vert1Pos, 1);
+ gl.glVertexAttribDivisor(attribs.vert2Pos, 1);
+
+ m_boundAttribs = attribs;
+ }
+
+ void virt_render(const GLRenderState &renderState) override
+ {
+ if (Base::isEmpty()) {
+ return;
+ }
+
+ Functions &gl = Base::m_functions;
+ gl.checkError();
+
+ const glm::mat4 mvp = renderState.mvp.value_or(gl.getProjectionMatrix());
+ auto programUnbinder = Base::m_program.bind();
+ Base::m_program.setUniforms(mvp, renderState.uniforms);
+ Base::m_program.setViewport("uViewport", gl.getPhysicalViewport());
+ Base::m_program.setFloat("uLineWidth",
+ renderState.lineParams.width * gl.getDevicePixelRatio());
+
+ RenderStateBinder renderStateBinder(gl, gl.getTexLookup(), renderState);
+
+ gl.glBindVertexArray(Base::m_vao.get());
+ RAIICallback vaoUnbinder([&gl]() {
+ gl.glBindVertexArray(0);
+ gl.checkError();
+ });
+
+ auto attribUnbinder = Base::bindAttribs();
+
+ const GLsizei numInstances = Base::m_numVerts / 2;
+ gl.glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, numInstances);
+ }
+
+ void virt_unbind() override
+ {
+ if (!m_boundAttribs) {
+ return;
+ }
+ auto &attribs = m_boundAttribs.value();
+ Functions &gl = Base::m_functions;
+ gl.glDisableVertexAttribArray(attribs.colorPos);
+ gl.glDisableVertexAttribArray(attribs.vert1Pos);
+ gl.glDisableVertexAttribArray(attribs.vert2Pos);
+ gl.glVertexAttribDivisor(attribs.colorPos, 0);
+ gl.glVertexAttribDivisor(attribs.vert1Pos, 0);
+ gl.glVertexAttribDivisor(attribs.vert2Pos, 0);
+ gl.glBindBuffer(GL_ARRAY_BUFFER, 0);
+ m_boundAttribs.reset();
+ }
+
+ NODISCARD static const void *VPO_PTR(const void *base, size_t offset)
+ {
+ return static_cast(base) + offset;
+ }
+};
// Textured mesh with color modulated by uniform
template
diff --git a/src/opengl/legacy/Shaders.cpp b/src/opengl/legacy/Shaders.cpp
index 8b2e8eca1..3d49d52ec 100644
--- a/src/opengl/legacy/Shaders.cpp
+++ b/src/opengl/legacy/Shaders.cpp
@@ -30,6 +30,8 @@ namespace Legacy {
AColorPlainShader::~AColorPlainShader() = default;
UColorPlainShader::~UColorPlainShader() = default;
+AColorLineShader::~AColorLineShader() = default;
+UColorLineShader::~UColorLineShader() = default;
AColorTexturedShader::~AColorTexturedShader() = default;
UColorTexturedShader::~UColorTexturedShader() = default;
@@ -48,6 +50,8 @@ void ShaderPrograms::early_init()
{
std::ignore = getPlainAColorShader();
std::ignore = getPlainUColorShader();
+ std::ignore = getLineAColorShader();
+ std::ignore = getLineUColorShader();
std::ignore = getTexturedAColorShader();
std::ignore = getTexturedUColorShader();
@@ -67,6 +71,8 @@ void ShaderPrograms::resetAll()
{
m_aColorShader.reset();
m_uColorShader.reset();
+ m_aLineShader.reset();
+ m_uLineShader.reset();
m_aTexturedShader.reset();
m_uTexturedShader.reset();
@@ -121,6 +127,16 @@ const std::shared_ptr &ShaderPrograms::getPlainUColorShader()
return getInitialized(m_uColorShader, getFunctions(), "plain/ucolor");
}
+const std::shared_ptr &ShaderPrograms::getLineAColorShader()
+{
+ return getInitialized(m_aLineShader, getFunctions(), "line/acolor");
+}
+
+const std::shared_ptr &ShaderPrograms::getLineUColorShader()
+{
+ return getInitialized(m_uLineShader, getFunctions(), "line/ucolor");
+}
+
const std::shared_ptr &ShaderPrograms::getTexturedAColorShader()
{
return getInitialized(m_aTexturedShader, getFunctions(), "tex/acolor");
diff --git a/src/opengl/legacy/Shaders.h b/src/opengl/legacy/Shaders.h
index f83ddfe46..a869db54e 100644
--- a/src/opengl/legacy/Shaders.h
+++ b/src/opengl/legacy/Shaders.h
@@ -23,6 +23,36 @@ struct NODISCARD AColorPlainShader final : public AbstractShaderProgram
}
};
+struct NODISCARD AColorLineShader final : public AbstractShaderProgram
+{
+public:
+ using AbstractShaderProgram::AbstractShaderProgram;
+
+ ~AColorLineShader() final;
+
+private:
+ void virt_setUniforms(const glm::mat4 &mvp, const GLRenderState::Uniforms &uniforms) final
+ {
+ setColor("uColor", uniforms.color);
+ setMatrix("uMVP", mvp);
+ }
+};
+
+struct NODISCARD UColorLineShader final : public AbstractShaderProgram
+{
+public:
+ using AbstractShaderProgram::AbstractShaderProgram;
+
+ ~UColorLineShader() final;
+
+private:
+ void virt_setUniforms(const glm::mat4 &mvp, const GLRenderState::Uniforms &uniforms) final
+ {
+ setColor("uColor", uniforms.color);
+ setMatrix("uMVP", mvp);
+ }
+};
+
struct NODISCARD UColorPlainShader final : public AbstractShaderProgram
{
public:
@@ -219,6 +249,8 @@ struct NODISCARD ShaderPrograms final
private:
std::shared_ptr m_aColorShader;
std::shared_ptr m_uColorShader;
+ std::shared_ptr m_aLineShader;
+ std::shared_ptr m_uLineShader;
std::shared_ptr m_aTexturedShader;
std::shared_ptr m_uTexturedShader;
@@ -253,6 +285,8 @@ struct NODISCARD ShaderPrograms final
NODISCARD const std::shared_ptr &getPlainAColorShader();
// uniform color (aka "Plain")
NODISCARD const std::shared_ptr &getPlainUColorShader();
+ NODISCARD const std::shared_ptr &getLineAColorShader();
+ NODISCARD const std::shared_ptr &getLineUColorShader();
// attribute color + textured (aka "ColoredTextured")
NODISCARD const std::shared_ptr &getTexturedAColorShader();
// uniform color + textured (aka "Textured")
diff --git a/src/opengl/legacy/SimpleMesh.h b/src/opengl/legacy/SimpleMesh.h
index 765318e6c..9b03e73fc 100644
--- a/src/opengl/legacy/SimpleMesh.h
+++ b/src/opengl/legacy/SimpleMesh.h
@@ -166,7 +166,7 @@ class NODISCARD SimpleMesh : public IRenderable
}
private:
- void virt_render(const GLRenderState &renderState) final
+ void virt_render(const GLRenderState &renderState) override
{
if (isEmpty()) {
return;
diff --git a/src/resources/mmapper2.qrc b/src/resources/mmapper2.qrc
index c9d4c0e84..1854f1359 100644
--- a/src/resources/mmapper2.qrc
+++ b/src/resources/mmapper2.qrc
@@ -213,6 +213,10 @@
pixmaps/wall-west.png
shaders/legacy/font/frag.glsl
shaders/legacy/font/vert.glsl
+ shaders/legacy/line/acolor/frag.glsl
+ shaders/legacy/line/acolor/vert.glsl
+ shaders/legacy/line/ucolor/frag.glsl
+ shaders/legacy/line/ucolor/vert.glsl
shaders/legacy/room/tex/acolor/frag.glsl
shaders/legacy/room/tex/acolor/vert.glsl
shaders/legacy/plain/acolor/frag.glsl
diff --git a/src/resources/shaders/legacy/line/acolor/frag.glsl b/src/resources/shaders/legacy/line/acolor/frag.glsl
new file mode 100644
index 000000000..da5f0a04e
--- /dev/null
+++ b/src/resources/shaders/legacy/line/acolor/frag.glsl
@@ -0,0 +1,10 @@
+uniform vec4 uColor;
+
+in vec4 vColor;
+
+out vec4 vFragmentColor;
+
+void main()
+{
+ vFragmentColor = vColor * uColor;
+}
diff --git a/src/resources/shaders/legacy/line/acolor/vert.glsl b/src/resources/shaders/legacy/line/acolor/vert.glsl
new file mode 100644
index 000000000..dae99a4ed
--- /dev/null
+++ b/src/resources/shaders/legacy/line/acolor/vert.glsl
@@ -0,0 +1,44 @@
+uniform mat4 uMVP;
+uniform float uLineWidth;
+uniform ivec4 uViewport; // x, y, width, height
+
+layout(location = 0) in vec4 aColor;
+layout(location = 1) in vec3 aVert1;
+layout(location = 2) in vec3 aVert2;
+
+out vec4 vColor;
+
+void main()
+{
+ vColor = aColor;
+
+ vec4 clip1 = uMVP * vec4(aVert1, 1.0);
+ vec4 clip2 = uMVP * vec4(aVert2, 1.0);
+
+ vec2 ndc1 = clip1.xy / clip1.w;
+ vec2 ndc2 = clip2.xy / clip2.w;
+
+ vec2 screen1 = (ndc1 + 1.0) * 0.5 * vec2(uViewport.zw);
+ vec2 screen2 = (ndc2 + 1.0) * 0.5 * vec2(uViewport.zw);
+
+ vec2 dir = screen2 - screen1;
+ float len = length(dir);
+ if (len < 0.0001) {
+ dir = vec2(1.0, 0.0);
+ } else {
+ dir /= len;
+ }
+ vec2 normal = vec2(-dir.y, dir.x);
+
+ vec2 offset = normal * uLineWidth * 0.5;
+
+ vec2 pos;
+ float z;
+ if (gl_VertexID == 0) { pos = screen1 + offset; z = clip1.z / clip1.w; }
+ else if (gl_VertexID == 1) { pos = screen1 - offset; z = clip1.z / clip1.w; }
+ else if (gl_VertexID == 2) { pos = screen2 - offset; z = clip2.z / clip2.w; }
+ else { pos = screen2 + offset; z = clip2.z / clip2.w; }
+
+ vec2 final_ndc = (pos / vec2(uViewport.zw)) * 2.0 - 1.0;
+ gl_Position = vec4(final_ndc, z, 1.0);
+}
diff --git a/src/resources/shaders/legacy/line/ucolor/frag.glsl b/src/resources/shaders/legacy/line/ucolor/frag.glsl
new file mode 100644
index 000000000..a393adc9f
--- /dev/null
+++ b/src/resources/shaders/legacy/line/ucolor/frag.glsl
@@ -0,0 +1,8 @@
+uniform vec4 uColor;
+
+out vec4 vFragmentColor;
+
+void main()
+{
+ vFragmentColor = uColor;
+}
diff --git a/src/resources/shaders/legacy/line/ucolor/vert.glsl b/src/resources/shaders/legacy/line/ucolor/vert.glsl
new file mode 100644
index 000000000..72d82e5ca
--- /dev/null
+++ b/src/resources/shaders/legacy/line/ucolor/vert.glsl
@@ -0,0 +1,39 @@
+uniform mat4 uMVP;
+uniform float uLineWidth;
+uniform ivec4 uViewport; // x, y, width, height
+
+layout(location = 0) in vec3 aVert1;
+layout(location = 1) in vec3 aVert2;
+
+void main()
+{
+ vec4 clip1 = uMVP * vec4(aVert1, 1.0);
+ vec4 clip2 = uMVP * vec4(aVert2, 1.0);
+
+ vec2 ndc1 = clip1.xy / clip1.w;
+ vec2 ndc2 = clip2.xy / clip2.w;
+
+ vec2 screen1 = (ndc1 + 1.0) * 0.5 * vec2(uViewport.zw);
+ vec2 screen2 = (ndc2 + 1.0) * 0.5 * vec2(uViewport.zw);
+
+ vec2 dir = screen2 - screen1;
+ float len = length(dir);
+ if (len < 0.0001) {
+ dir = vec2(1.0, 0.0);
+ } else {
+ dir /= len;
+ }
+ vec2 normal = vec2(-dir.y, dir.x);
+
+ vec2 offset = normal * uLineWidth * 0.5;
+
+ vec2 pos;
+ float z;
+ if (gl_VertexID == 0) { pos = screen1 + offset; z = clip1.z / clip1.w; }
+ else if (gl_VertexID == 1) { pos = screen1 - offset; z = clip1.z / clip1.w; }
+ else if (gl_VertexID == 2) { pos = screen2 - offset; z = clip2.z / clip2.w; }
+ else { pos = screen2 + offset; z = clip2.z / clip2.w; }
+
+ vec2 final_ndc = (pos / vec2(uViewport.zw)) * 2.0 - 1.0;
+ gl_Position = vec4(final_ndc, z, 1.0);
+}
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index c6d58ad24..ae21920db 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -335,3 +335,18 @@ set_target_properties(
COMPILE_FLAGS "${WARNING_FLAGS}"
)
add_test(NAME TestHotkeyManager COMMAND TestHotkeyManager)
+
+# OpenGL
+set(TestOpenGL_SRCS testopengl.cpp)
+add_executable(TestOpenGL ${TestOpenGL_SRCS} ../src/opengl/OpenGLProber.cpp ../src/opengl/OpenGLConfig.cpp)
+target_include_directories(TestOpenGL PRIVATE ../src)
+add_dependencies(TestOpenGL mm_global)
+target_link_libraries(TestOpenGL mm_global Qt6::Test Qt6::Gui Qt6::OpenGL coverage_config)
+set_target_properties(
+ TestOpenGL PROPERTIES
+ CXX_STANDARD 17
+ CXX_STANDARD_REQUIRED ON
+ CXX_EXTENSIONS OFF
+ COMPILE_FLAGS "${WARNING_FLAGS}"
+)
+add_test(NAME TestOpenGL COMMAND TestOpenGL)
diff --git a/tests/testopengl.cpp b/tests/testopengl.cpp
new file mode 100644
index 000000000..c48331084
--- /dev/null
+++ b/tests/testopengl.cpp
@@ -0,0 +1,88 @@
+#include
+#include
+
+#include
+
+class TestOpenGL : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void testParseSurveyResult_GL()
+ {
+ OpenGLProber prober;
+ QByteArray json = R"({"backend": "GL", "major": 4, "minor": 5, "debug": true})";
+ auto result = prober.parseSurveyResult(json);
+
+ QCOMPARE(result.backendType, OpenGLProber::BackendType::GL);
+ QCOMPARE(result.format.majorVersion(), 4);
+ QCOMPARE(result.format.minorVersion(), 5);
+ QCOMPARE(result.format.profile(), QSurfaceFormat::CoreProfile);
+ QCOMPARE(result.format.testOption(QSurfaceFormat::DebugContext), true);
+ QCOMPARE(result.highestVersionString, std::string("GL4.5"));
+
+ QCOMPARE(OpenGLConfig::getGLVersionString(), std::string("GL4.5"));
+ }
+
+ void testParseSurveyResult_ES()
+ {
+ OpenGLProber prober;
+ QByteArray json = R"({"backend": "ES", "major": 3, "minor": 1, "debug": false})";
+ auto result = prober.parseSurveyResult(json);
+
+ QCOMPARE(result.backendType, OpenGLProber::BackendType::ES);
+ QCOMPARE(result.format.majorVersion(), 3);
+ QCOMPARE(result.format.minorVersion(), 1);
+ QCOMPARE(result.format.renderableType(), QSurfaceFormat::OpenGLES);
+ QCOMPARE(result.format.testOption(QSurfaceFormat::DebugContext), false);
+ QCOMPARE(result.highestVersionString, std::string("ES3.1"));
+
+ QCOMPARE(OpenGLConfig::getESVersionString(), std::string("ES3.1"));
+ }
+
+ void testParseSurveyResult_Invalid()
+ {
+ OpenGLProber prober;
+ QByteArray json = R"({"backend": "None"})";
+ auto result = prober.parseSurveyResult(json);
+ QCOMPARE(result.backendType, OpenGLProber::BackendType::None);
+
+ json = "garbage";
+ result = prober.parseSurveyResult(json);
+ // Fallback is GL 3.3 Core
+ QCOMPARE(result.backendType, OpenGLProber::BackendType::GL);
+ QCOMPARE(result.highestVersionString, std::string("Fallback"));
+ }
+
+ void testParseSurveyResult_Robustness()
+ {
+ OpenGLProber prober;
+ QByteArray json = R"(
+Some random log message
+More noise
+{"backend": "GL", "major": 4, "minor": 6}
+Sanitizer output: leak detected
+)";
+ auto result = prober.parseSurveyResult(json);
+
+ QCOMPARE(result.backendType, OpenGLProber::BackendType::GL);
+ QCOMPARE(result.format.majorVersion(), 4);
+ QCOMPARE(result.format.minorVersion(), 6);
+ QCOMPARE(result.highestVersionString, std::string("GL4.6"));
+ }
+
+ void testGetBackendType()
+ {
+ OpenGLConfig::setBackendType(OpenGLProber::BackendType::ES);
+ QCOMPARE(OpenGLConfig::getBackendType(), OpenGLProber::BackendType::ES);
+ }
+
+ void testMaxSamples()
+ {
+ OpenGLConfig::setMaxSamples(4);
+ QCOMPARE(OpenGLConfig::getMaxSamples(), 4);
+ }
+};
+
+QTEST_MAIN(TestOpenGL)
+#include "testopengl.moc"